From 7ac7df9ea36f6635e9c70970088c03e5fc3cb47b 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, 24 Oct 2019 16:48:29 +0300 Subject: [PATCH 01/29] Fix test news --- apps/news/urls/back.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/news/urls/back.py b/apps/news/urls/back.py index 4ab11727..9cc3d94a 100644 --- a/apps/news/urls/back.py +++ b/apps/news/urls/back.py @@ -8,7 +8,7 @@ app_name = 'news' urlpatterns = [ path('', views.NewsBackOfficeLCView.as_view(), name='list-create'), path('/', views.NewsBackOfficeRUDView.as_view(), - name='gallery-retrieve-update-destroy'), + name='retrieve-update-destroy'), path('/gallery/', views.NewsBackOfficeGalleryListView.as_view(), name='gallery-list'), path('/gallery//', views.NewsBackOfficeGalleryCreateDestroyView.as_view(), From 851ba7f9ddf43b7a26e15553480da25a09d2930b 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, 25 Oct 2019 10:14:50 +0300 Subject: [PATCH 02/29] Test edit --- apps/comment/tests.py | 44 ++++++++++++++++++++++++++++++++++---- apps/comment/views/back.py | 2 +- apps/utils/permissions.py | 35 ++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/apps/comment/tests.py b/apps/comment/tests.py index 9b060f4e..87b7d32f 100644 --- a/apps/comment/tests.py +++ b/apps/comment/tests.py @@ -5,8 +5,9 @@ from django.urls import reverse from django.contrib.contenttypes.models import ContentType from http.cookies import SimpleCookie from account.models import Role, User, UserRole +from account.serializers.common import UserSerializer from comment.models import Comment - +import json class CommentModeratorPermissionTests(BasePermissionTests): def setUp(self): @@ -28,18 +29,53 @@ class CommentModeratorPermissionTests(BasePermissionTests): ) self.userRole.save() - content_type = ContentType.objects.get(app_label='location', model='country') + self.content_type = ContentType.objects.get(app_label='location', model='country') self.user_test = get_tokens_for_user() self.comment = Comment.objects.create(text='Test comment', mark=1, user=self.user_test["user"], - object_id= self.country_ru.pk, - content_type_id=content_type.id, + object_id=self.country_ru.pk, + content_type_id=self.content_type.id, country=self.country_ru ) self.comment.save() self.url = reverse('back:comment:comment-crud', kwargs={"id": self.comment.id}) + def test_post(self): + self.url = reverse('back:comment:comment-list-create') + + comment = { + "text": "Test comment POST", + "user_id": self.user_test["user"].id, + "object_id": self.country_ru.pk, + "content_type_id": self.content_type.id, + "country_id": self.country_ru.id + } + # + # response = self.client.post(self.url, format='json', data=comment) + # self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + json_user = json.dumps(self.moderator) + user = UserSerializer(data=self.moderator) + user.is_valid() + u_data = user.data + self.assertFalse(user.is_valid()) + # comment = { + # "text": "Test comment POST moder", + # "user": user, + # "object_id": self.country_ru.pk, + # "content_type_id": self.content_type.id, + # "country_id": self.country_ru.id + # } + # # + # tokens = User.create_jwt_tokens(self.moderator) + # self.client.cookies = SimpleCookie( + # {'access_token': tokens.get('access_token'), + # 'refresh_token': tokens.get('access_token')}) + # + # response = self.client.post(self.url, format='json', data=comment) + # self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # self.assertTrue(True) def test_put_moderator(self): tokens = User.create_jwt_tokens(self.moderator) diff --git a/apps/comment/views/back.py b/apps/comment/views/back.py index 2895fdbe..25c10a62 100644 --- a/apps/comment/views/back.py +++ b/apps/comment/views/back.py @@ -8,7 +8,7 @@ class CommentLstView(generics.ListCreateAPIView): """Comment list create view.""" serializer_class = serializers.CommentBaseSerializer queryset = models.Comment.objects.all() - permission_classes = [permissions.IsAuthenticatedOrReadOnly,] + permission_classes = [permissions.IsAuthenticatedOrReadOnly|IsCommentModerator] class CommentRUDView(generics.RetrieveUpdateDestroyAPIView): diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 45d978a0..aee2ab57 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -72,6 +72,20 @@ class IsStandardUser(IsGuest): Object-level permission to only allow owners of an object to edit it. Assumes the model instance has an `owner` attribute. """ + def has_permission(self, request, view): + rules = [ + super().has_permission(request, view) + ] + + # and request.user.email_confirmed, + if hasattr(request, 'user'): + rules = [ + request.user.is_authenticated, + super().has_permission(request, view) + ] + + return any(rules) + def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request rules = [ @@ -131,6 +145,27 @@ class IsCommentModerator(IsStandardUser): Object-level permission to only allow owners of an object to edit it. Assumes the model instance has an `owner` attribute. """ + + def has_permission(self, request, view): + rules = [ + super().has_permission(request, view) + ] + + # and request.user.email_confirmed, + if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'): + # Read permissions are allowed to any request. + + role = Role.objects.filter(role=Role.COMMENTS_MODERATOR, + country_id=request.data.country_id) \ + .first() # 'Comments moderator' + + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists(), + super().has_permission(request, view) + ] + + return any(rules) + def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request. role = Role.objects.filter(role=Role.COMMENTS_MODERATOR, From 046d0c5fe677ece42bce72efa32804e9f4c2287b 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, 25 Oct 2019 10:59:31 +0300 Subject: [PATCH 03/29] Fix country and comment role --- apps/comment/serializers/back.py | 2 +- apps/comment/tests.py | 58 +++++++++++++-------------- apps/comment/views/back.py | 2 +- apps/utils/permissions.py | 20 +++++++++ apps/utils/tests/tests_permissions.py | 3 +- 5 files changed, 51 insertions(+), 34 deletions(-) diff --git a/apps/comment/serializers/back.py b/apps/comment/serializers/back.py index d0cd47c8..325086c0 100644 --- a/apps/comment/serializers/back.py +++ b/apps/comment/serializers/back.py @@ -6,4 +6,4 @@ from rest_framework import serializers class CommentBaseSerializer(serializers.ModelSerializer): class Meta: model = models.Comment - fields = ('id', 'text', 'mark', 'user') \ No newline at end of file + fields = ('id', 'text', 'mark', 'user', 'object_id', 'content_type') \ No newline at end of file diff --git a/apps/comment/tests.py b/apps/comment/tests.py index 87b7d32f..e91ee2f4 100644 --- a/apps/comment/tests.py +++ b/apps/comment/tests.py @@ -5,9 +5,8 @@ from django.urls import reverse from django.contrib.contenttypes.models import ContentType from http.cookies import SimpleCookie from account.models import Role, User, UserRole -from account.serializers.common import UserSerializer from comment.models import Comment -import json + class CommentModeratorPermissionTests(BasePermissionTests): def setUp(self): @@ -46,36 +45,30 @@ class CommentModeratorPermissionTests(BasePermissionTests): comment = { "text": "Test comment POST", - "user_id": self.user_test["user"].id, + "user": self.user_test["user"].id, "object_id": self.country_ru.pk, - "content_type_id": self.content_type.id, + "content_type": self.content_type.id, "country_id": self.country_ru.id } - # - # response = self.client.post(self.url, format='json', data=comment) - # self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) - json_user = json.dumps(self.moderator) - user = UserSerializer(data=self.moderator) - user.is_valid() - u_data = user.data - self.assertFalse(user.is_valid()) - # comment = { - # "text": "Test comment POST moder", - # "user": user, - # "object_id": self.country_ru.pk, - # "content_type_id": self.content_type.id, - # "country_id": self.country_ru.id - # } - # # - # tokens = User.create_jwt_tokens(self.moderator) - # self.client.cookies = SimpleCookie( - # {'access_token': tokens.get('access_token'), - # 'refresh_token': tokens.get('access_token')}) - # - # response = self.client.post(self.url, format='json', data=comment) - # self.assertEqual(response.status_code, status.HTTP_201_CREATED) - # self.assertTrue(True) + response = self.client.post(self.url, format='json', data=comment) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + + comment = { + "text": "Test comment POST moder", + "user": self.moderator.id, + "object_id": self.country_ru.id, + "content_type": self.content_type.id, + "country_id": self.country_ru.id + } + + tokens = User.create_jwt_tokens(self.moderator) + self.client.cookies = SimpleCookie( + {'access_token': tokens.get('access_token'), + 'refresh_token': tokens.get('access_token')}) + + response = self.client.post(self.url, format='json', data=comment) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_put_moderator(self): tokens = User.create_jwt_tokens(self.moderator) @@ -87,7 +80,9 @@ class CommentModeratorPermissionTests(BasePermissionTests): "id": self.comment.id, "text": "test text moderator", "mark": 1, - "user": self.moderator.id + "user": self.moderator.id, + "object_id": self.comment.country_id, + "content_type": self.content_type.id } response = self.client.put(self.url, data=data, format='json') @@ -134,9 +129,10 @@ class CommentModeratorPermissionTests(BasePermissionTests): "id": self.comment.id, "text": "test text moderator", "mark": 1, - "user": super_user.id + "user": super_user.id, + "object_id": self.country_ru.id, + "content_type": self.content_type.id, } - response = self.client.put(self.url, data=data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/apps/comment/views/back.py b/apps/comment/views/back.py index 25c10a62..8d836177 100644 --- a/apps/comment/views/back.py +++ b/apps/comment/views/back.py @@ -8,7 +8,7 @@ class CommentLstView(generics.ListCreateAPIView): """Comment list create view.""" serializer_class = serializers.CommentBaseSerializer queryset = models.Comment.objects.all() - permission_classes = [permissions.IsAuthenticatedOrReadOnly|IsCommentModerator] + permission_classes = [permissions.IsAuthenticatedOrReadOnly|IsCountryAdmin|IsCommentModerator] class CommentRUDView(generics.RetrieveUpdateDestroyAPIView): diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index aee2ab57..8ad1ae32 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -126,6 +126,26 @@ class IsCountryAdmin(IsStandardUser): Object-level permission to only allow owners of an object to edit it. Assumes the model instance has an `owner` attribute. """ + + def has_permission(self, request, view): + rules = [ + super().has_permission(request, view) + ] + + # and request.user.email_confirmed, + if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'): + # Read permissions are allowed to any request. + + role = Role.objects.filter(role=Role.COUNTRY_ADMIN, + country_id=request.data.country_id) \ + .first() # 'Comments moderator' + + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists(), + super().has_permission(request, view) + ] + return any(rules) + def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request. role = Role.objects.filter(role=Role.COUNTRY_ADMIN, diff --git a/apps/utils/tests/tests_permissions.py b/apps/utils/tests/tests_permissions.py index edc1a5d7..3bba7b7d 100644 --- a/apps/utils/tests/tests_permissions.py +++ b/apps/utils/tests/tests_permissions.py @@ -9,10 +9,11 @@ class BasePermissionTests(APITestCase): title='Russia', locale='ru-RU' ) + self.lang.save() self.country_ru = Country.objects.get( name={"en-GB": "Russian"} ) - + self.country_ru.save() From 7f4b46dbf83e989bad971831090d086f0896f3c3 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, 25 Oct 2019 12:42:01 +0300 Subject: [PATCH 04/29] Fix country admin --- apps/comment/tests.py | 2 +- apps/comment/views/back.py | 5 +++-- apps/location/serializers/back.py | 1 + apps/location/tests.py | 5 ----- apps/location/views/back.py | 4 ++-- apps/utils/permissions.py | 28 +++++++++++++++++++++++----- 6 files changed, 30 insertions(+), 15 deletions(-) diff --git a/apps/comment/tests.py b/apps/comment/tests.py index e91ee2f4..786f68d3 100644 --- a/apps/comment/tests.py +++ b/apps/comment/tests.py @@ -90,7 +90,7 @@ class CommentModeratorPermissionTests(BasePermissionTests): def test_get(self): response = self.client.get(self.url, format='json') - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(response.status_code, status.HTTP_200_OK) def test_put_other_user(self): other_user = User.objects.create_user(username='test', diff --git a/apps/comment/views/back.py b/apps/comment/views/back.py index 8d836177..3b96cbd2 100644 --- a/apps/comment/views/back.py +++ b/apps/comment/views/back.py @@ -8,12 +8,13 @@ class CommentLstView(generics.ListCreateAPIView): """Comment list create view.""" serializer_class = serializers.CommentBaseSerializer queryset = models.Comment.objects.all() - permission_classes = [permissions.IsAuthenticatedOrReadOnly|IsCountryAdmin|IsCommentModerator] + permission_classes = [permissions.IsAuthenticatedOrReadOnly| IsCommentModerator|IsCountryAdmin] class CommentRUDView(generics.RetrieveUpdateDestroyAPIView): """Comment RUD view.""" serializer_class = serializers.CommentBaseSerializer queryset = models.Comment.objects.all() - permission_classes = [IsCountryAdmin|IsCommentModerator] + + permission_classes = [IsCountryAdmin | IsCommentModerator] lookup_field = 'id' diff --git a/apps/location/serializers/back.py b/apps/location/serializers/back.py index f25aacf6..c178f7fd 100644 --- a/apps/location/serializers/back.py +++ b/apps/location/serializers/back.py @@ -16,4 +16,5 @@ class CountryBackSerializer(common.CountrySerializer): 'code', 'svg_image', 'name', + 'country_id' ] diff --git a/apps/location/tests.py b/apps/location/tests.py index cb574036..eed68071 100644 --- a/apps/location/tests.py +++ b/apps/location/tests.py @@ -19,11 +19,6 @@ class BaseTestCase(APITestCase): self.user = User.objects.create_user( username=self.username, email=self.email, password=self.password) - # get tokens - - # self.user.is_superuser = True - # self.user.save() - tokkens = User.create_jwt_tokens(self.user) self.client.cookies = SimpleCookie( {'access_token': tokkens.get('access_token'), diff --git a/apps/location/views/back.py b/apps/location/views/back.py index cb8246a4..1cdd91da 100644 --- a/apps/location/views/back.py +++ b/apps/location/views/back.py @@ -4,7 +4,7 @@ from rest_framework import generics from location import models, serializers from location.views import common from utils.permissions import IsCountryAdmin - +from rest_framework.permissions import IsAuthenticatedOrReadOnly # Address class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView): """Create view for model Address.""" @@ -50,7 +50,7 @@ class CountryListCreateView(generics.ListCreateAPIView): queryset = models.Country.objects.all() serializer_class = serializers.CountryBackSerializer pagination_class = None - permission_classes = [IsCountryAdmin] + permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] class CountryRUDView(generics.RetrieveUpdateDestroyAPIView): """RUD view for model Country.""" diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 8ad1ae32..2a10200c 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -56,7 +56,15 @@ class IsGuest(permissions.IsAuthenticatedOrReadOnly): Object-level permission to only allow owners of an object to edit it. """ def has_permission(self, request, view): - return request.user.is_authenticated + rules = [ + request.method in permissions.SAFE_METHODS + ] + # if hasattr(request, 'user.is_superuser'): + # rules = [ + # request.user.is_superuser, + # request.method in permissions.SAFE_METHODS + # ] + return any(rules) def has_object_permission(self, request, view, obj): @@ -131,7 +139,6 @@ class IsCountryAdmin(IsStandardUser): rules = [ super().has_permission(request, view) ] - # and request.user.email_confirmed, if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'): # Read permissions are allowed to any request. @@ -153,9 +160,20 @@ class IsCountryAdmin(IsStandardUser): .first() # 'Comments moderator' rules = [ - UserRole.objects.filter(user=request.user, role=role).exists(), - super().has_object_permission(request, view, obj), - ] + super().has_object_permission(request, view, obj) + ] + # and request.user.email_confirmed, + if hasattr(request, 'user') and request.user.is_authenticated: + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists(), + super().has_object_permission(request, view, obj), + ] + + if hasattr(request.data, 'user'): + rules = [ + UserRole.objects.filter(user=request.data.user, role=role).exists(), + super().has_object_permission(request, view, obj), + ] return any(rules) From 7f23f0e891455324d1832a44e0053f3ffdadb489 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 05/29] Establishment each hour reindexing --- apps/establishment/models.py | 5 +++++ apps/establishment/tasks.py | 12 ++++++++++-- apps/search_indexes/documents/establishment.py | 2 +- project/settings/base.py | 10 +++++++++- requirements/base.txt | 3 ++- 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 304ea2a6..4a6a95e4 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -120,6 +120,11 @@ class EstablishmentQuerySet(models.QuerySet): def with_type_related(self): return self.prefetch_related('establishment_subtypes') + def with_es_related(self): + """Return qs with related for ES indexing objects.""" + return self.select_related('address', 'establishment_type', 'address__city', 'address__city__country').\ + prefetch_related('tags', 'schedule') + def search(self, value, locale=None): """Search text in JSON fields.""" if locale is not None: diff --git a/apps/establishment/tasks.py b/apps/establishment/tasks.py index cf23a7e6..978f29ef 100644 --- a/apps/establishment/tasks.py +++ b/apps/establishment/tasks.py @@ -1,10 +1,13 @@ """Establishment app tasks.""" import logging + from celery import shared_task +from django.core import management +from django_elasticsearch_dsl.management.commands import search_index + from establishment import models from location.models import Country - logger = logging.getLogger(__name__) @@ -12,10 +15,15 @@ logger = logging.getLogger(__name__) def recalculate_price_levels_by_country(country_id): try: country = Country.objects.get(pk=country_id) - except Country.DoesNotExist as ex: + except Country.DoesNotExist as _: logger.error(f'ESTABLISHMENT. Country does not exist. ID {country_id}') else: qs = models.Establishment.objects.filter(address__city__country=country) for establishment in qs: establishment.recalculate_price_level(low_price=country.low_price, high_price=country.high_price) + +@shared_task +def rebuild_establishment_indices(): + management.call_command(search_index.Command(), action='rebuild', models=[models.Establishment.__name__], + force=True) diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 5d858321..665fc85d 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -106,4 +106,4 @@ class EstablishmentDocument(Document): ) def get_queryset(self): - return super().get_queryset().published() + return super().get_queryset().published().with_es_related() diff --git a/project/settings/base.py b/project/settings/base.py index f9524f94..8a65c1bb 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -13,6 +13,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/ import os import sys from datetime import timedelta +from celery.schedules import crontab # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -98,7 +99,8 @@ EXTERNAL_APPS = [ 'timezone_field', 'storages', 'sorl.thumbnail', - 'timezonefinder' + 'timezonefinder', + 'django_celery_beat', ] @@ -324,6 +326,12 @@ CELERY_ACCEPT_CONTENT = ['application/json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = TIME_ZONE +CELERY_BEAT_SCHEDULE = { + 'send-summary-every-hour': { + 'task': 'establishment.tasks.rebuild_establishment_indices', + 'schedule': crontab(hour=1), + }, +} # Django FCM (Firebase push notifications) FCM_DJANGO_SETTINGS = { diff --git a/requirements/base.txt b/requirements/base.txt index c95055de..19c29b7d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -44,4 +44,5 @@ sorl-thumbnail==12.5.0 # temp solution redis==3.2.0 amqp>=2.4.0 -celery==4.3.0rc2 \ No newline at end of file +celery==4.3.0rc2 +django-celery-beat==1.5.0 \ No newline at end of file From dd0b5951328fa09ea47ab1e221950b28b082110e Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 06/29] Review fixes --- apps/establishment/tasks.py | 4 +++- project/settings/base.py | 8 -------- requirements/base.txt | 3 +-- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/apps/establishment/tasks.py b/apps/establishment/tasks.py index 978f29ef..b7a39d2d 100644 --- a/apps/establishment/tasks.py +++ b/apps/establishment/tasks.py @@ -2,6 +2,8 @@ import logging from celery import shared_task +from celery.schedules import crontab +from celery.task import periodic_task from django.core import management from django_elasticsearch_dsl.management.commands import search_index @@ -23,7 +25,7 @@ def recalculate_price_levels_by_country(country_id): establishment.recalculate_price_level(low_price=country.low_price, high_price=country.high_price) -@shared_task +@periodic_task(run_every=crontab(minute=60)) def rebuild_establishment_indices(): management.call_command(search_index.Command(), action='rebuild', models=[models.Establishment.__name__], force=True) diff --git a/project/settings/base.py b/project/settings/base.py index 8a65c1bb..a8334839 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -13,7 +13,6 @@ https://docs.djangoproject.com/en/2.2/ref/settings/ import os import sys from datetime import timedelta -from celery.schedules import crontab # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -100,7 +99,6 @@ EXTERNAL_APPS = [ 'storages', 'sorl.thumbnail', 'timezonefinder', - 'django_celery_beat', ] @@ -326,12 +324,6 @@ CELERY_ACCEPT_CONTENT = ['application/json'] CELERY_TASK_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json' CELERY_TIMEZONE = TIME_ZONE -CELERY_BEAT_SCHEDULE = { - 'send-summary-every-hour': { - 'task': 'establishment.tasks.rebuild_establishment_indices', - 'schedule': crontab(hour=1), - }, -} # Django FCM (Firebase push notifications) FCM_DJANGO_SETTINGS = { diff --git a/requirements/base.txt b/requirements/base.txt index 19c29b7d..c95055de 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -44,5 +44,4 @@ sorl-thumbnail==12.5.0 # temp solution redis==3.2.0 amqp>=2.4.0 -celery==4.3.0rc2 -django-celery-beat==1.5.0 \ No newline at end of file +celery==4.3.0rc2 \ No newline at end of file From 4eaa600fc1baee60176e9a37937670420d331e1a Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 07/29] Fix all tests --- apps/establishment/tasks.py | 2 +- apps/utils/tests/tests_translated.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/establishment/tasks.py b/apps/establishment/tasks.py index b7a39d2d..fdc10933 100644 --- a/apps/establishment/tasks.py +++ b/apps/establishment/tasks.py @@ -25,7 +25,7 @@ def recalculate_price_levels_by_country(country_id): establishment.recalculate_price_level(low_price=country.low_price, high_price=country.high_price) -@periodic_task(run_every=crontab(minute=60)) +@periodic_task(run_every=crontab(minute=59)) def rebuild_establishment_indices(): management.call_command(search_index.Command(), action='rebuild', models=[models.Establishment.__name__], force=True) diff --git a/apps/utils/tests/tests_translated.py b/apps/utils/tests/tests_translated.py index 4569ecf2..557c8b5d 100644 --- a/apps/utils/tests/tests_translated.py +++ b/apps/utils/tests/tests_translated.py @@ -9,6 +9,7 @@ from account.models import User from news.models import News, NewsType from establishment.models import Establishment, EstablishmentType, Employee +from location.models import Country class BaseTestCase(APITestCase): @@ -39,7 +40,13 @@ class TranslateFieldTests(BaseTestCase): self.news_type = NewsType.objects.create(name="Test news type") self.news_type.save() + + self.country_ru = Country.objects.get( + name={"en-GB": "Russian"} + ) + self.news_item = News.objects.create( + id=8, created_by=self.user, modified_by=self.user, title={ @@ -52,6 +59,7 @@ class TranslateFieldTests(BaseTestCase): news_type=self.news_type, slug='test', state=News.PUBLISHED, + country=self.country_ru, ) self.news_item.save() From b7831b97393f3ace0a5b4375e593af11b3941a79 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, 25 Oct 2019 15:25:35 +0300 Subject: [PATCH 08/29] Fix IsContentPageManager --- apps/location/views/back.py | 19 ++++++++++++------- apps/news/tests.py | 16 ++++++++++++++++ apps/utils/permissions.py | 26 ++++++++++++++++++++------ 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/apps/location/views/back.py b/apps/location/views/back.py index 1cdd91da..bb64ff72 100644 --- a/apps/location/views/back.py +++ b/apps/location/views/back.py @@ -6,42 +6,46 @@ from location.views import common from utils.permissions import IsCountryAdmin from rest_framework.permissions import IsAuthenticatedOrReadOnly # Address + + class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView): """Create view for model Address.""" serializer_class = serializers.AddressDetailSerializer queryset = models.Address.objects.all() - permission_classes = [IsCountryAdmin] + permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView): """RUD view for model Address.""" serializer_class = serializers.AddressDetailSerializer queryset = models.Address.objects.all() - permission_classes = [IsCountryAdmin] + permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] # City class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView): """Create view for model City.""" serializer_class = serializers.CitySerializer - permission_classes = [IsCountryAdmin] + permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] + class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView): """RUD view for model City.""" serializer_class = serializers.CitySerializer - permission_classes = [IsCountryAdmin] + permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] # Region class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView): """Create view for model Region""" serializer_class = serializers.RegionSerializer - permission_classes = [IsCountryAdmin] + permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] + class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView): """Retrieve view for model Region""" serializer_class = serializers.RegionSerializer - permission_classes = [IsCountryAdmin] + permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] # Country @@ -52,8 +56,9 @@ class CountryListCreateView(generics.ListCreateAPIView): pagination_class = None permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] + class CountryRUDView(generics.RetrieveUpdateDestroyAPIView): """RUD view for model Country.""" serializer_class = serializers.CountryBackSerializer - permission_classes = [IsCountryAdmin] + permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] queryset = models.Country.objects.all() \ No newline at end of file diff --git a/apps/news/tests.py b/apps/news/tests.py index 115763e5..77dbca8e 100644 --- a/apps/news/tests.py +++ b/apps/news/tests.py @@ -66,6 +66,22 @@ class NewsTestCase(BaseTestCase): def setUp(self): super().setUp() + def test_news_post(self): + test_news ={ + "title": {"en-GB": "Test news POST"}, + "news_type_id": self.test_news_type.id, + "description": {"en-GB": "Description test news"}, + "start": datetime.now() + timedelta(hours=-2), + "end": datetime.now() + timedelta(hours=2), + "state": News.PUBLISHED, + "slug": 'test-news-slug_post', + "country_id": self.country_ru.id, + } + + url = reverse("back:news:list-create") + response = self.client.post(url, data=test_news, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + def test_web_news(self): response = self.client.get(reverse('web:news:list')) self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 2a10200c..7ee7811b 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -57,13 +57,9 @@ class IsGuest(permissions.IsAuthenticatedOrReadOnly): """ def has_permission(self, request, view): rules = [ + request.user.is_superuser, request.method in permissions.SAFE_METHODS ] - # if hasattr(request, 'user.is_superuser'): - # rules = [ - # request.user.is_superuser, - # request.method in permissions.SAFE_METHODS - # ] return any(rules) def has_object_permission(self, request, view, obj): @@ -114,6 +110,24 @@ class IsContentPageManager(IsStandardUser): Object-level permission to only allow owners of an object to edit it. Assumes the model instance has an `owner` attribute. """ + + def has_permission(self, request, view): + rules = [ + super().has_permission(request, view) + ] + # and request.user.email_confirmed, + if hasattr(request, 'user'): + role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, + country_id=request.country_id)\ + .first() # 'Comments moderator' + + rules = [ + UserRole.objects.filter(user=request.user, role=role).exists(), + # and obj.user != request.user, + super().has_permission(request, view) + ] + return any(rules) + def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request. @@ -134,8 +148,8 @@ class IsCountryAdmin(IsStandardUser): Object-level permission to only allow owners of an object to edit it. Assumes the model instance has an `owner` attribute. """ - def has_permission(self, request, view): + rules = [ super().has_permission(request, view) ] From a38fed847a0f7601e320046629e2184f97689a61 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, 25 Oct 2019 15:51:07 +0300 Subject: [PATCH 09/29] Fix roles --- apps/utils/permissions.py | 87 ++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/apps/utils/permissions.py b/apps/utils/permissions.py index 7ee7811b..86a4be6f 100644 --- a/apps/utils/permissions.py +++ b/apps/utils/permissions.py @@ -20,8 +20,8 @@ class IsAuthenticatedAndTokenIsValid(permissions.BasePermission): access_token = request.COOKIES.get('access_token') if user.is_authenticated and access_token: access_token = AccessToken(access_token) - valid_tokens = user.access_tokens.valid()\ - .by_jti(jti=access_token.payload.get('jti')) + valid_tokens = user.access_tokens.valid() \ + .by_jti(jti=access_token.payload.get('jti')) return valid_tokens.exists() else: return False @@ -31,13 +31,14 @@ class IsRefreshTokenValid(permissions.BasePermission): """ Check if user has a valid refresh token and authenticated """ + def has_permission(self, request, view): """Check permissions by refresh token and default REST permission IsAuthenticated""" refresh_token = request.COOKIES.get('refresh_token') if refresh_token: refresh_token = GMRefreshToken(refresh_token) - refresh_token_qs = JWTRefreshToken.objects.valid()\ - .by_jti(jti=refresh_token.payload.get('jti')) + refresh_token_qs = JWTRefreshToken.objects.valid() \ + .by_jti(jti=refresh_token.payload.get('jti')) return refresh_token_qs.exists() else: return False @@ -55,6 +56,7 @@ class IsGuest(permissions.IsAuthenticatedOrReadOnly): """ Object-level permission to only allow owners of an object to edit it. """ + def has_permission(self, request, view): rules = [ request.user.is_superuser, @@ -63,7 +65,6 @@ class IsGuest(permissions.IsAuthenticatedOrReadOnly): return any(rules) def has_object_permission(self, request, view, obj): - rules = [ request.user.is_superuser, request.method in permissions.SAFE_METHODS @@ -76,6 +77,7 @@ class IsStandardUser(IsGuest): Object-level permission to only allow owners of an object to edit it. Assumes the model instance has an `owner` attribute. """ + def has_permission(self, request, view): rules = [ super().has_permission(request, view) @@ -118,7 +120,7 @@ class IsContentPageManager(IsStandardUser): # and request.user.email_confirmed, if hasattr(request, 'user'): role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, - country_id=request.country_id)\ + country_id=request.country_id) \ .first() # 'Comments moderator' rules = [ @@ -132,7 +134,7 @@ class IsContentPageManager(IsStandardUser): # Read permissions are allowed to any request. role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER, - country_id=obj.country_id)\ + country_id=obj.country_id) \ .first() # 'Comments moderator' rules = [ @@ -148,6 +150,7 @@ class IsCountryAdmin(IsStandardUser): Object-level permission to only allow owners of an object to edit it. Assumes the model instance has an `owner` attribute. """ + def has_permission(self, request, view): rules = [ @@ -174,8 +177,8 @@ class IsCountryAdmin(IsStandardUser): .first() # 'Comments moderator' rules = [ - super().has_object_permission(request, view, obj) - ] + super().has_object_permission(request, view, obj) + ] # and request.user.email_confirmed, if hasattr(request, 'user') and request.user.is_authenticated: rules = [ @@ -221,7 +224,7 @@ class IsCommentModerator(IsStandardUser): def has_object_permission(self, request, view, obj): # Read permissions are allowed to any request. role = Role.objects.filter(role=Role.COMMENTS_MODERATOR, - country_id=obj.country_id)\ + country_id=obj.country_id) \ .first() # 'Comments moderator' rules = [ @@ -234,10 +237,28 @@ class IsCommentModerator(IsStandardUser): class IsEstablishmentManager(IsStandardUser): - def has_object_permission(self, request, view, obj): - role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER)\ + def has_permission(self, request, view): + rules = [ + super().has_permission(request, view) + ] + + # and request.user.email_confirmed, + if hasattr(request.data, 'user') and hasattr(request.data, 'establishment_id'): + role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \ .first() # 'Comments moderator' + rules = [ + UserRole.objects.filter(user=request.user, role=role, + establishment_id=request.data.establishment_id + ).exists(), + super().has_permission(request, view) + ] + return any(rules) + + def has_object_permission(self, request, view, obj): + role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \ + .first() # 'Comments moderator' + rules = [ UserRole.objects.filter(user=request.user, role=role, establishment_id=obj.establishment_id @@ -250,11 +271,28 @@ class IsEstablishmentManager(IsStandardUser): class IsReviewerManager(IsStandardUser): - def has_object_permission(self, request, view, obj): + def has_permission(self, request, view): + rules = [ + super().has_permission(request, view) + ] + # and request.user.email_confirmed, + if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'): + role = Role.objects.filter(role=Role.REVIEWER_MANGER) \ + .first() # 'Comments moderator' + + rules = [ + UserRole.objects.filter(user=request.user, role=role, + establishment_id=request.data.country_id + ).exists(), + super().has_permission(request, view) + ] + return any(rules) + + def has_object_permission(self, request, view, obj): role = Role.objects.filter(role=Role.REVIEWER_MANGER, - country_id=obj.country_id)\ - .first() + country_id=obj.country_id) \ + .first() rules = [ UserRole.objects.filter(user=request.user, role=role).exists(), @@ -266,8 +304,25 @@ class IsReviewerManager(IsStandardUser): class IsRestaurantReviewer(IsStandardUser): - def has_object_permission(self, request, view, obj): + def has_permission(self, request, view): + rules = [ + super().has_permission(request, view) + ] + # 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' + + rules = [ + UserRole.objects.filter(user=request.user, role=role, + establishment_id=request.data.object_id + ).exists(), + super().has_permission(request, view) + ] + return any(rules) + + def has_object_permission(self, request, view, obj): content_type = ContentType.objects.get(app_lable='establishment', model='establishment') From d4fe5ebd2ddb4376f2998201e271b93ec2fe761f Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Fri, 25 Oct 2019 16:06:52 +0300 Subject: [PATCH 10/29] search by type & subtype --- apps/news/serializers.py | 1 + .../search_indexes/documents/establishment.py | 21 +++++++++++++------ apps/search_indexes/serializers.py | 2 +- apps/search_indexes/views.py | 9 ++++++++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 2b4e98b6..27d50977 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -97,6 +97,7 @@ class CropImageSerializer(serializers.Serializer): class NewsImageSerializer(serializers.ModelSerializer): """Serializer for returning crop images of news image.""" + orientation_display = serializers.CharField(source='get_orientation_display', read_only=True) original_url = serializers.URLField(source='image.url') diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 5d858321..5d26ee59 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -10,6 +10,16 @@ EstablishmentIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, EstablishmentIndex.settings(number_of_shards=1, number_of_replicas=1) +# todo: check & refactor +class ObjectField(fields.ObjectField): + + def get_value_from_instance(self, *args, **kwargs): + value = super(ObjectField, self).get_value_from_instance(*args, **kwargs) + if value == {}: + return None + return value + + @EstablishmentIndex.doc_type class EstablishmentDocument(Document): """Establishment document.""" @@ -22,14 +32,13 @@ class EstablishmentDocument(Document): 'id': fields.IntegerField(), 'name': fields.ObjectField(attr='name_indexing', properties=OBJECT_FIELD_PROPERTIES), + 'index_name': fields.KeywordField(attr='index_name'), }) establishment_subtypes = fields.ObjectField( properties={ 'id': fields.IntegerField(), - 'name': fields.ObjectField(attr='name_indexing', - properties={ - 'id': fields.IntegerField(), - }), + 'name': fields.ObjectField(attr='name_indexing'), + 'index_name': fields.KeywordField(attr='index_name'), }, multi=True) works_evening = fields.ListField(fields.IntegerField( @@ -54,7 +63,7 @@ class EstablishmentDocument(Document): 'closed_at': fields.KeywordField(attr='closed_at_str'), } )) - address = fields.ObjectField( + address = ObjectField( properties={ 'id': fields.IntegerField(), 'street_name_1': fields.TextField( @@ -82,7 +91,7 @@ class EstablishmentDocument(Document): ), } ), - } + }, ) # todo: need to fix # collections = fields.ObjectField( diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index d4ab2dbf..cb1fe735 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -75,7 +75,7 @@ class NewsDocumentSerializer(DocumentSerializer): class EstablishmentDocumentSerializer(DocumentSerializer): """Establishment document serializer.""" - address = AddressDocumentSerializer() + address = AddressDocumentSerializer(allow_null=True) tags = TagsDocumentSerializer(many=True) schedule = ScheduleDocumentSerializer(many=True, allow_null=True) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 25205bac..fd1c5b7a 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -124,6 +124,15 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): 'establishment_subtypes': { 'field': 'establishment_subtypes.id' }, + 'type': { + 'field': 'establishment_type.index_name' + }, + 'subtype': { + 'field': 'establishment_subtypes.index_name', + 'lookups': [ + constants.LOOKUP_QUERY_IN, + ], + }, 'works_noon': { 'field': 'works_noon', 'lookups': [ From 7d0acba51b04b4156bd07c4e45a0a956509b6377 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 25 Oct 2019 16:18:16 +0300 Subject: [PATCH 11/29] added to same_theme and should_read preview_image_url, added UniqueConstraint to NewsGallery model --- .../migrations/0029_auto_20191025_1241.py | 18 +++++++++++++++ apps/news/models.py | 2 +- apps/news/serializers.py | 22 +++++++++++-------- apps/news/views.py | 2 +- 4 files changed, 33 insertions(+), 11 deletions(-) create mode 100644 apps/news/migrations/0029_auto_20191025_1241.py diff --git a/apps/news/migrations/0029_auto_20191025_1241.py b/apps/news/migrations/0029_auto_20191025_1241.py new file mode 100644 index 00000000..8e5bcba4 --- /dev/null +++ b/apps/news/migrations/0029_auto_20191025_1241.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-10-25 12:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('gallery', '0003_auto_20191003_1228'), + ('news', '0028_auto_20191024_1649'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='newsgallery', + unique_together={('news', 'image'), ('news', 'is_main')}, + ), + ] diff --git a/apps/news/models.py b/apps/news/models.py index dbb2f5bf..9b7190e4 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -235,4 +235,4 @@ class NewsGallery(models.Model): """NewsGallery meta class.""" verbose_name = _('news gallery') verbose_name_plural = _('news galleries') - unique_together = ('news', 'is_main') + unique_together = (('news', 'is_main'), ('news', 'image')) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 27d50977..1389d20e 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -150,6 +150,17 @@ class NewsBaseSerializer(ProjectModelSerializer): ) +class NewsSimilarListSerializer(NewsBaseSerializer): + """List serializer for News model.""" + preview_image_url = serializers.URLField() + + class Meta(NewsBaseSerializer.Meta): + """Meta class.""" + fields = NewsBaseSerializer.Meta.fields + ( + 'preview_image_url', + ) + + class NewsListSerializer(NewsBaseSerializer): """List serializer for News model.""" @@ -192,8 +203,8 @@ class NewsDetailSerializer(NewsBaseSerializer): class NewsDetailWebSerializer(NewsDetailSerializer): """News detail serializer for web users..""" - same_theme = NewsBaseSerializer(many=True, read_only=True) - should_read = NewsBaseSerializer(many=True, read_only=True) + same_theme = NewsSimilarListSerializer(many=True, read_only=True) + should_read = NewsSimilarListSerializer(many=True, read_only=True) agenda = AgendaSerializer() banner = NewsBannerSerializer() @@ -266,7 +277,6 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer): """Override validate method.""" news_pk = self.get_request_kwargs().get('pk') image_id = self.get_request_kwargs().get('image_id') - is_main = attrs.get('is_main') news_qs = models.News.objects.filter(pk=news_pk) image_qs = Image.objects.filter(id=image_id) @@ -279,12 +289,6 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer): news = news_qs.first() image = image_qs.first() - if news.news_gallery.filter(image=image).exists(): - raise serializers.ValidationError({'detail': _('Image is already added')}) - - if is_main and news.news_gallery.main_image().exists(): - raise serializers.ValidationError({'detail': _('Main image is already added')}) - attrs['news'] = news attrs['image'] = image diff --git a/apps/news/views.py b/apps/news/views.py index 9cd5d969..9cbe6b53 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -119,7 +119,7 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, return gallery def create(self, request, *args, **kwargs): - """Override create method""" + """Overridden create method""" super().create(request, *args, **kwargs) return Response(status=status.HTTP_201_CREATED) From e1e0f1e997028a838eb4a965a84455a2a9fbdca7 Mon Sep 17 00:00:00 2001 From: Semyon Yekhmenin Date: Fri, 25 Oct 2019 13:41:53 +0000 Subject: [PATCH 12/29] Added index_name in employee position --- .../migrations/0044_position_index_name.py | 18 ++++++++++++++++++ apps/establishment/models.py | 3 +++ apps/establishment/serializers/common.py | 3 ++- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 apps/establishment/migrations/0044_position_index_name.py diff --git a/apps/establishment/migrations/0044_position_index_name.py b/apps/establishment/migrations/0044_position_index_name.py new file mode 100644 index 00000000..0bf423d1 --- /dev/null +++ b/apps/establishment/migrations/0044_position_index_name.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-10-24 14:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0043_establishment_currency'), + ] + + operations = [ + migrations.AddField( + model_name='position', + name='index_name', + field=models.CharField(db_index=True, max_length=255, null=True, unique=True, verbose_name='Index name'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 4a6a95e4..9648c6b0 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -497,6 +497,9 @@ class Position(BaseAttributes, TranslatedFieldsMixin): priority = models.IntegerField(unique=True, null=True, default=None) + index_name = models.CharField(max_length=255, db_index=True, unique=True, + null=True, verbose_name=_('Index name')) + class Meta: """Meta class.""" diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 2846c5c8..14be142a 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -147,12 +147,13 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer): position_translated = serializers.CharField(source='position.name_translated') awards = AwardSerializer(source='employee.awards', many=True) priority = serializers.IntegerField(source='position.priority') + position_index_name = serializers.CharField(source='position.index_name') class Meta: """Meta class.""" model = models.Employee - fields = ('id', 'name', 'position_translated', 'awards', 'priority') + fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name') class EstablishmentBaseSerializer(ProjectModelSerializer): From e3616ee7f8c8ec4ce49e2dd115cdad819967284d Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 25 Oct 2019 17:31:59 +0300 Subject: [PATCH 13/29] change news_preview geometry string on 300x2260 --- project/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/settings/base.py b/project/settings/base.py index a8334839..fb542fce 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -360,7 +360,7 @@ THUMBNAIL_DEFAULT_OPTIONS = { THUMBNAIL_QUALITY = 85 THUMBNAIL_DEBUG = False SORL_THUMBNAIL_ALIASES = { - 'news_preview': {'geometry_string': '100x100', 'crop': 'center'}, + 'news_preview': {'geometry_string': '300x260', 'crop': 'center'}, 'news_promo_horizontal_web': {'geometry_string': '1900x600', 'crop': 'center'}, 'news_promo_horizontal_mobile': {'geometry_string': '375x260', 'crop': 'center'}, 'news_tile_horizontal_web': {'geometry_string': '300x275', 'crop': 'center'}, From 37d745eeaac036033d9a42e1bc2271aa62b296ff Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sat, 26 Oct 2019 18:54:22 +0300 Subject: [PATCH 14/29] added endpoints, filters, serializers and extend model Product, ProductType, ProductSubtype --- apps/establishment/admin.py | 8 +++- apps/establishment/models.py | 5 ++ apps/product/admin.py | 18 +++++++ apps/product/filters.py | 34 +++++++++++++ apps/product/models.py | 71 ++++++++++++++++++++++++---- apps/product/serializers/__init__.py | 3 ++ apps/product/serializers/common.py | 54 +++++++++++++++++++++ apps/product/urls/common.py | 10 ++++ apps/product/urls/web.py | 7 +++ apps/product/views/__init__.py | 4 ++ apps/product/views/common.py | 20 ++++++++ project/settings/base.py | 1 + project/urls/web.py | 1 + 13 files changed, 227 insertions(+), 9 deletions(-) create mode 100644 apps/product/admin.py create mode 100644 apps/product/filters.py diff --git a/apps/establishment/admin.py b/apps/establishment/admin.py index 50c21b90..66f0aee1 100644 --- a/apps/establishment/admin.py +++ b/apps/establishment/admin.py @@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _ from comment.models import Comment from establishment import models from main.models import Award +from product.models import Product from review import models as review_models @@ -46,13 +47,18 @@ class CommentInline(GenericTabularInline): extra = 0 +class ProductInline(admin.TabularInline): + model = Product + extra = 0 + + @admin.register(models.Establishment) class EstablishmentAdmin(admin.ModelAdmin): """Establishment admin.""" list_display = ['id', '__str__', 'image_tag', ] inlines = [ AwardInline, ContactPhoneInline, ContactEmailInline, - ReviewInline, CommentInline] + ReviewInline, CommentInline, ProductInline] @admin.register(models.Position) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 9648c6b0..0ca2463f 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -486,6 +486,11 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): """ return self.id + @property + def wines(self): + """Return list products with type wine""" + return self.products.wines() + class Position(BaseAttributes, TranslatedFieldsMixin): """Position model.""" diff --git a/apps/product/admin.py b/apps/product/admin.py new file mode 100644 index 00000000..b3dbc0cd --- /dev/null +++ b/apps/product/admin.py @@ -0,0 +1,18 @@ +"""Product admin conf.""" +from django.contrib import admin +from .models import Product, ProductType, ProductSubType + + +@admin.register(Product) +class ProductAdmin(admin.ModelAdmin): + """Admin page for model Product.""" + + +@admin.register(ProductType) +class ProductTypeAdmin(admin.ModelAdmin): + """Admin page for model ProductType.""" + + +@admin.register(ProductSubType) +class ProductSubTypeAdmin(admin.ModelAdmin): + """Admin page for model ProductSubType.""" diff --git a/apps/product/filters.py b/apps/product/filters.py new file mode 100644 index 00000000..9318b943 --- /dev/null +++ b/apps/product/filters.py @@ -0,0 +1,34 @@ +"""Filters for app Product.""" +from django.core.validators import EMPTY_VALUES +from django_filters import rest_framework as filters + +from product import models + + +class ProductListFilterSet(filters.FilterSet): + """Product filter set.""" + + establishment_id = filters.NumberFilter() + type = filters.ChoiceFilter(method='by_type', + choices=models.ProductType.INDEX_NAME_TYPES) + subtype = filters.ChoiceFilter(method='by_subtype', + choices=models.ProductSubType.INDEX_NAME_TYPES) + + class Meta: + """Meta class.""" + model = models.Product + fields = [ + 'establishment_id', + 'type', + 'subtype', + ] + + def by_type(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_type(value) + return queryset + + def by_subtype(self, queryset, name, value): + if value not in EMPTY_VALUES: + return queryset.by_subtype(value) + return queryset diff --git a/apps/product/models.py b/apps/product/models.py index 41f0c7c6..d629d663 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -1,5 +1,6 @@ """Product app models.""" from django.db import models +from django.core.exceptions import ValidationError from django.contrib.postgres.fields import JSONField from django.utils.translation import gettext_lazy as _ from utils.models import (BaseAttributes, ProjectBaseMixin, @@ -9,9 +10,23 @@ from utils.models import (BaseAttributes, ProjectBaseMixin, class ProductType(TranslatedFieldsMixin, ProjectBaseMixin): """ProductType model.""" + STR_FIELD_NAME = 'name' + + # INDEX NAME CHOICES + FOOD = 'food' + WINE = 'wine' + LIQUOR = 'liquor' + + INDEX_NAME_TYPES = ( + (FOOD, _('Food')), + (WINE, _('Wine')), + (LIQUOR, _('Liquor')), + ) + name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Name'), help_text='{"en-GB":"some text"}') - index_name = models.CharField(max_length=50, unique=True, db_index=True, + index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES, + unique=True, db_index=True, verbose_name=_('Index name')) use_subtypes = models.BooleanField(_('Use subtypes'), default=True) @@ -25,19 +40,35 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin): class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin): """ProductSubtype model.""" + STR_FIELD_NAME = 'name' + + # INDEX NAME CHOICES + RUM = 'rum' + OTHER = 'other' + + INDEX_NAME_TYPES = ( + (RUM, _('Rum')), + (OTHER, _('Other')), + ) + product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE, related_name='subtypes', verbose_name=_('Product type')) name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Name'), help_text='{"en-GB":"some text"}') - index_name = models.CharField(max_length=50, unique=True, db_index=True, + index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES, + unique=True, db_index=True, verbose_name=_('Index name')) class Meta: """Meta class.""" - verbose_name = _('Product type') - verbose_name_plural = _('Product types') + verbose_name = _('Product subtype') + verbose_name_plural = _('Product subtypes') + + def clean_fields(self, exclude=None): + if not self.product_type.use_subtypes: + raise ValidationError(_('Product type is not use subtypes.')) class ProductManager(models.Manager): @@ -47,16 +78,33 @@ class ProductManager(models.Manager): class ProductQuerySet(models.QuerySet): """Product queryset.""" + def with_base_related(self): + return self.select_related('country', 'product_type', 'establishment') \ + .prefetch_related('product_type__subtypes') + def common(self): return self.filter(category=self.model.COMMON) def online(self): return self.filter(category=self.model.ONLINE) + def wines(self): + return self.filter(type__index_name=ProductType.WINE) + + def by_type(self, type: str): + """Filter by type.""" + return self.filter(product_type__index_name=type) + + def by_subtype(self, subtype: str): + """Filter by subtype.""" + return self.filter(subtypes__index_name=subtype) + class Product(TranslatedFieldsMixin, BaseAttributes): """Product models.""" + STR_FIELD_NAME = 'name' + COMMON = 0 ONLINE = 1 @@ -75,10 +123,17 @@ class Product(TranslatedFieldsMixin, BaseAttributes): country = models.ForeignKey('location.Country', on_delete=models.PROTECT, verbose_name=_('Country')) available = models.BooleanField(_('Available'), default=True) - type = models.ForeignKey(ProductType, on_delete=models.PROTECT, - related_name='products', verbose_name=_('Type')) - subtypes = models.ManyToManyField(ProductSubType, related_name='products', + product_type = models.ForeignKey(ProductType, on_delete=models.PROTECT, + related_name='products', verbose_name=_('Type')) + subtypes = models.ManyToManyField(ProductSubType, blank=True, + related_name='products', verbose_name=_('Subtypes')) + establishment = models.ForeignKey('establishment.Establishment', + on_delete=models.PROTECT, + related_name='products', + verbose_name=_('establishment')) + public_mark = models.PositiveIntegerField(blank=True, null=True, default=None, + verbose_name=_('public mark'),) objects = ProductManager.from_queryset(ProductQuerySet)() @@ -93,7 +148,7 @@ class OnlineProductManager(ProductManager): """Extended manger for OnlineProduct model.""" def get_queryset(self): - """Overrided get_queryset method.""" + """Overridden get_queryset method.""" return super().get_queryset().online() diff --git a/apps/product/serializers/__init__.py b/apps/product/serializers/__init__.py index e69de29b..c564831e 100644 --- a/apps/product/serializers/__init__.py +++ b/apps/product/serializers/__init__.py @@ -0,0 +1,3 @@ +from .common import * +from .web import * +from .mobile import * diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index 236cd38b..f4f02b4d 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -1 +1,55 @@ +"""Product app serializers.""" from rest_framework import serializers +from utils.serializers import TranslatedField +from product.models import Product, ProductSubType, ProductType + + +class ProductSubTypeBaseSerializer(serializers.ModelSerializer): + """ProductSubType base serializer""" + name_translated = TranslatedField() + index_name_display = serializers.CharField(source='get_index_name_display') + + class Meta: + model = ProductSubType + fields = [ + 'id', + 'name_translated', + 'index_name_display', + ] + + +class ProductTypeBaseSerializer(serializers.ModelSerializer): + """ProductType base serializer""" + name_translated = TranslatedField() + index_name_display = serializers.CharField(source='get_index_name_display') + + class Meta: + model = ProductType + fields = [ + 'id', + 'name_translated', + 'index_name_display', + ] + + +class ProductBaseSerializer(serializers.ModelSerializer): + """Product base serializer.""" + name_translated = TranslatedField() + description_translated = TranslatedField() + category_display = serializers.CharField(source='get_category_display') + product_type = ProductTypeBaseSerializer() + subtypes = ProductSubTypeBaseSerializer(many=True) + + class Meta: + """Meta class.""" + model = Product + fields = [ + 'id', + 'name_translated', + 'category_display', + 'description_translated', + 'available', + 'product_type', + 'subtypes', + 'public_mark', + ] diff --git a/apps/product/urls/common.py b/apps/product/urls/common.py index e69de29b..57abf4f0 100644 --- a/apps/product/urls/common.py +++ b/apps/product/urls/common.py @@ -0,0 +1,10 @@ +"""Product url patterns.""" +from django.urls import path + +from product import views + +app_name = 'product' + +urlpatterns = [ + path('', views.ProductListView.as_view(), name='list') +] diff --git a/apps/product/urls/web.py b/apps/product/urls/web.py index e69de29b..116c7b0b 100644 --- a/apps/product/urls/web.py +++ b/apps/product/urls/web.py @@ -0,0 +1,7 @@ +"""Product web url patterns.""" +from product.urls.common import urlpatterns as common_urlpatterns + +urlpatterns = [ +] + +urlpatterns.extend(common_urlpatterns) diff --git a/apps/product/views/__init__.py b/apps/product/views/__init__.py index e69de29b..6f4a8001 100644 --- a/apps/product/views/__init__.py +++ b/apps/product/views/__init__.py @@ -0,0 +1,4 @@ +from .back import * +from .common import * +from .mobile import * +from .web import * diff --git a/apps/product/views/common.py b/apps/product/views/common.py index e69de29b..403781e4 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -0,0 +1,20 @@ +"""Product app views.""" +from rest_framework import generics, permissions +from product.models import Product +from product import serializers +from product import filters + + +class ProductBaseView(generics.GenericAPIView): + """Product base view""" + + def get_queryset(self): + """Override get_queryset method.""" + return Product.objects.with_base_related() + + +class ProductListView(ProductBaseView, generics.ListAPIView): + """List view for model Product.""" + permission_classes = (permissions.AllowAny, ) + serializer_class = serializers.ProductBaseSerializer + filter_class = filters.ProductListFilterSet diff --git a/project/settings/base.py b/project/settings/base.py index fb542fce..85274993 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -75,6 +75,7 @@ PROJECT_APPS = [ 'favorites.apps.FavoritesConfig', 'rating.apps.RatingConfig', 'tag.apps.TagConfig', + 'product.apps.ProductConfig', ] EXTERNAL_APPS = [ diff --git a/project/urls/web.py b/project/urls/web.py index 77a06961..86f7eac2 100644 --- a/project/urls/web.py +++ b/project/urls/web.py @@ -35,4 +35,5 @@ urlpatterns = [ path('comments/', include('comment.urls.web')), path('favorites/', include('favorites.urls')), path('timetables/', include('timetable.urls.web')), + path('products/', include('product.urls.web')), ] From e41b539ed71cd85bfdde57be38319bf49581a5c5 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sun, 27 Oct 2019 18:42:51 +0300 Subject: [PATCH 15/29] added wine region --- apps/location/admin.py | 10 +++++++ apps/location/models.py | 43 +++++++++++++++++++++++++++++ apps/location/serializers/common.py | 30 ++++++++++++++++++++ apps/product/models.py | 11 ++++++++ apps/product/serializers/common.py | 3 ++ 5 files changed, 97 insertions(+) diff --git a/apps/location/admin.py b/apps/location/admin.py index a7610a65..adb355ac 100644 --- a/apps/location/admin.py +++ b/apps/location/admin.py @@ -19,6 +19,16 @@ class CityAdmin(admin.ModelAdmin): """City admin.""" +@admin.register(models.WineRegion) +class WineRegionAdmin(admin.ModelAdmin): + """WineRegion admin.""" + + +@admin.register(models.WineAppellation) +class WineAppellationAdmin(admin.ModelAdmin): + """WineAppellation admin.""" + + @admin.register(models.Address) class AddressAdmin(admin.OSMGeoAdmin): """Address admin.""" diff --git a/apps/location/models.py b/apps/location/models.py index da645de6..ba171c27 100644 --- a/apps/location/models.py +++ b/apps/location/models.py @@ -131,6 +131,49 @@ class Address(models.Model): return self.city.country_id +class WineRegionQuerySet(models.QuerySet): + """Wine region queryset.""" + + +class WineRegion(TranslatedFieldsMixin, models.Model): + """Wine region model.""" + STR_FIELD_NAME = 'name' + + name = TJSONField(verbose_name=_('Name'), + help_text='{"en-GB":"some text"}') + country = models.ForeignKey(Country, on_delete=models.PROTECT, + verbose_name=_('country')) + + objects = WineRegionQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name_plural = _('wine regions') + verbose_name = _('wine region') + + +class WineAppellationQuerySet(models.QuerySet): + """Wine appellation queryset.""" + + +class WineAppellation(TranslatedFieldsMixin, models.Model): + """Wine appellation model.""" + STR_FIELD_NAME = 'name' + + name = TJSONField(verbose_name=_('Name'), + help_text='{"en-GB":"some text"}') + wine_region = models.ForeignKey(WineRegion, on_delete=models.PROTECT, + related_name='appellations', + verbose_name=_('wine region')) + + objects = WineAppellationQuerySet.as_manager() + + class Meta: + """Meta class.""" + verbose_name_plural = _('wine appellations') + verbose_name = _('wine appellation') + + # todo: Make recalculate price levels @receiver(post_save, sender=Country) def run_recalculate_price_levels(sender, instance, **kwargs): diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index 87d0df4e..cfc14355 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -148,3 +148,33 @@ class AddressDetailSerializer(AddressBaseSerializer): 'city_id', 'city', ) + + +class WineAppellationBaseSerializer(serializers.ModelSerializer): + """Wine appellations.""" + name_translated = TranslatedField() + + class Meta: + """Meta class.""" + model = models.WineAppellation + fields = [ + 'id', + 'name_translated', + ] + + +class WineRegionBaseSerializer(serializers.ModelSerializer): + """Wine region serializer.""" + name_translated = TranslatedField() + country = CountrySerializer() + appellations = WineAppellationBaseSerializer(many=True) + + class Meta: + """Meta class.""" + model = models.WineRegion + fields = [ + 'id', + 'name_translated', + 'country', + 'appellations', + ] diff --git a/apps/product/models.py b/apps/product/models.py index d629d663..2f65c16a 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -134,6 +134,10 @@ class Product(TranslatedFieldsMixin, BaseAttributes): verbose_name=_('establishment')) public_mark = models.PositiveIntegerField(blank=True, null=True, default=None, verbose_name=_('public mark'),) + wine_region = models.ForeignKey('location.WineRegion', on_delete=models.PROTECT, + related_name='wines', + blank=True, null=True, + verbose_name=_('wine region')) objects = ProductManager.from_queryset(ProductQuerySet)() @@ -143,6 +147,13 @@ class Product(TranslatedFieldsMixin, BaseAttributes): verbose_name = _('Product') verbose_name_plural = _('Products') + def clean_fields(self, exclude=None): + super().clean_fields(exclude=exclude) + if self.product_type.index_name == ProductType.WINE and not self.wine_region: + raise ValidationError(_('wine_region field must be specified.')) + if not self.product_type.index_name == ProductType.WINE and self.wine_region: + raise ValidationError(_('wine_region field must not be specified.')) + class OnlineProductManager(ProductManager): """Extended manger for OnlineProduct model.""" diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index f4f02b4d..c0a8c6ed 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -2,6 +2,7 @@ from rest_framework import serializers from utils.serializers import TranslatedField from product.models import Product, ProductSubType, ProductType +from location.serializers import WineRegionBaseSerializer class ProductSubTypeBaseSerializer(serializers.ModelSerializer): @@ -39,6 +40,7 @@ class ProductBaseSerializer(serializers.ModelSerializer): category_display = serializers.CharField(source='get_category_display') product_type = ProductTypeBaseSerializer() subtypes = ProductSubTypeBaseSerializer(many=True) + wine_region = WineRegionBaseSerializer(allow_null=True) class Meta: """Meta class.""" @@ -52,4 +54,5 @@ class ProductBaseSerializer(serializers.ModelSerializer): 'product_type', 'subtypes', 'public_mark', + 'wine_region', ] From d12df5c8e15e1cd720b31554b72e6e292ba32f32 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 28 Oct 2019 12:33:48 +0300 Subject: [PATCH 16/29] added WineAppellation model --- apps/location/admin.py | 6 ++++++ apps/location/serializers/common.py | 2 -- apps/product/models.py | 14 ++++++++++---- apps/product/serializers/common.py | 7 ++++++- apps/utils/models.py | 25 +++++++++++++------------ 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/apps/location/admin.py b/apps/location/admin.py index adb355ac..a52fa14e 100644 --- a/apps/location/admin.py +++ b/apps/location/admin.py @@ -19,9 +19,15 @@ class CityAdmin(admin.ModelAdmin): """City admin.""" +class WineAppellationInline(admin.TabularInline): + model = models.WineAppellation + extra = 0 + + @admin.register(models.WineRegion) class WineRegionAdmin(admin.ModelAdmin): """WineRegion admin.""" + inlines = [WineAppellationInline, ] @admin.register(models.WineAppellation) diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index cfc14355..2a70c3b8 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -167,7 +167,6 @@ class WineRegionBaseSerializer(serializers.ModelSerializer): """Wine region serializer.""" name_translated = TranslatedField() country = CountrySerializer() - appellations = WineAppellationBaseSerializer(many=True) class Meta: """Meta class.""" @@ -176,5 +175,4 @@ class WineRegionBaseSerializer(serializers.ModelSerializer): 'id', 'name_translated', 'country', - 'appellations', ] diff --git a/apps/product/models.py b/apps/product/models.py index 2f65c16a..b0b1795e 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -79,8 +79,8 @@ class ProductQuerySet(models.QuerySet): """Product queryset.""" def with_base_related(self): - return self.select_related('country', 'product_type', 'establishment') \ - .prefetch_related('product_type__subtypes') + return self.select_related('product_type', 'establishment') \ + .prefetch_related('product_type__subtypes', 'country') def common(self): return self.filter(category=self.model.COMMON) @@ -120,8 +120,8 @@ class Product(TranslatedFieldsMixin, BaseAttributes): description = TJSONField(_('Description'), null=True, blank=True, default=None, help_text='{"en-GB":"some text"}') characteristics = JSONField(_('Characteristics')) - country = models.ForeignKey('location.Country', on_delete=models.PROTECT, - verbose_name=_('Country')) + country = models.ManyToManyField('location.Country', + verbose_name=_('Country')) available = models.BooleanField(_('Available'), default=True) product_type = models.ForeignKey(ProductType, on_delete=models.PROTECT, related_name='products', verbose_name=_('Type')) @@ -138,6 +138,9 @@ class Product(TranslatedFieldsMixin, BaseAttributes): related_name='wines', blank=True, null=True, verbose_name=_('wine region')) + wine_appellation = models.ForeignKey('location.WineAppellation', on_delete=models.PROTECT, + blank=True, null=True, + verbose_name=_('wine appellation')) objects = ProductManager.from_queryset(ProductQuerySet)() @@ -153,6 +156,9 @@ class Product(TranslatedFieldsMixin, BaseAttributes): raise ValidationError(_('wine_region field must be specified.')) if not self.product_type.index_name == ProductType.WINE and self.wine_region: raise ValidationError(_('wine_region field must not be specified.')) + if (self.wine_region and self.wine_appellation) and \ + self.wine_appellation not in self.wine_region.appellations.all(): + raise ValidationError(_('Wine appellation not exists in wine region.')) class OnlineProductManager(ProductManager): diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index c0a8c6ed..fb2f7aeb 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -2,7 +2,8 @@ from rest_framework import serializers from utils.serializers import TranslatedField from product.models import Product, ProductSubType, ProductType -from location.serializers import WineRegionBaseSerializer +from location.serializers import (WineRegionBaseSerializer, WineAppellationBaseSerializer, + CountrySimpleSerializer) class ProductSubTypeBaseSerializer(serializers.ModelSerializer): @@ -41,6 +42,8 @@ class ProductBaseSerializer(serializers.ModelSerializer): product_type = ProductTypeBaseSerializer() subtypes = ProductSubTypeBaseSerializer(many=True) wine_region = WineRegionBaseSerializer(allow_null=True) + wine_appellation = WineAppellationBaseSerializer(allow_null=True) + available_countries = CountrySimpleSerializer(source='country', many=True) class Meta: """Meta class.""" @@ -55,4 +58,6 @@ class ProductBaseSerializer(serializers.ModelSerializer): 'subtypes', 'public_mark', 'wine_region', + 'wine_appellation', + 'available_countries', ] diff --git a/apps/utils/models.py b/apps/utils/models.py index fb1de17c..03330eb4 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -44,18 +44,19 @@ class TJSONField(JSONField): def to_locale(language): """Turn a language name (en-us) into a locale name (en_US).""" - language, _, country = language.lower().partition('-') - if not country: - return language - # A language with > 2 characters after the dash only has its first - # character after the dash capitalized; e.g. sr-latn becomes sr-Latn. - # A language with 2 characters after the dash has both characters - # capitalized; e.g. en-us becomes en-US. - country, _, tail = country.partition('-') - country = country.title() if len(country) > 2 else country.upper() - if tail: - country += '-' + tail - return language + '-' + country + if language: + language, _, country = language.lower().partition('-') + if not country: + return language + # A language with > 2 characters after the dash only has its first + # character after the dash capitalized; e.g. sr-latn becomes sr-Latn. + # A language with 2 characters after the dash has both characters + # capitalized; e.g. en-us becomes en-US. + country, _, tail = country.partition('-') + country = country.title() if len(country) > 2 else country.upper() + if tail: + country += '-' + tail + return language + '-' + country def translate_field(self, field_name): From 2ebeffcf3e255407fa94ac99a245117b21b604b1 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 28 Oct 2019 12:34:37 +0300 Subject: [PATCH 17/29] Merge branch 'develop' of /home/a.feteleu/projects/gm-backend with conflicts. --- celerybeat-schedule | Bin 12845 -> 13471 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/celerybeat-schedule b/celerybeat-schedule index 3efe528a0f0c9bb6e563876fdc4c1a02541e62f7..b8d3e3de18a9fcae15f3b9986a0be9fe2dcfa5aa 100644 GIT binary patch literal 13471 zcmeI3O=}ZD7{@2sB%4ec+a@iFw#8dhp-XLxBK05?PoW}f#fadt*$nJrn)cb5h%E$i zD*6>X=uJF|UqG*3dhzHdm z81GqKAdb2Kj(N1%_Pm}|SC0mkoh4^Mwuqgcb;nTx%-VjV#hN|q`5)c${qAwFD3542 zy`aVXc4rGxLBLup-gX+y7xZ2bvSz2nZp)dsDWt1mDoh1UUoa7EFnW*yYn9sH?DNIx z8|3aI$OK#4VuyP7*HNNSI;RHfF^bsiiy1f zBcTieN-mu1!f0cz3u9dvuPd)-&$gnNaVW2Se9Hw?T$td=`s?cd4|00BxIt=lb%q(2 z^R*%#ZpbD_`1}cXV%F$Ht)_RR&5PR2D3HVd4HP<+o1RSXzZw=wZN*@qVG;xsT^Q*? zNr6y61dO>b&K(XFO<}@?Nq!2;9ulS%TE}@%MtKmOvR4S%zYf_!56VdovUefxLZOyE z!Gmb^09G+ff`AbhN|KX;H7XrPz_<(LvrK15s+r@MW;N6N3Q4g%2n{)r79n;8ZE^E0wcG!(ab%>V&51`zr?ly7DTVO-{g(7HzG{E3B9 zn{yPFu<-IiwQ>Vh@Nbq?{mj9~02N{f3b8S4meZ4Cl1MiUbvk7V7~3Ts%*q From 65e7c965e4f67511bec70f3918145b2743d2f8c3 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 28 Oct 2019 12:46:10 +0300 Subject: [PATCH 18/29] added migrations --- .../0013_wineappellation_wineregion.py | 41 ++++++++ apps/product/migrations/0001_initial.py | 94 +++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 apps/location/migrations/0013_wineappellation_wineregion.py create mode 100644 apps/product/migrations/0001_initial.py diff --git a/apps/location/migrations/0013_wineappellation_wineregion.py b/apps/location/migrations/0013_wineappellation_wineregion.py new file mode 100644 index 00000000..dee84e55 --- /dev/null +++ b/apps/location/migrations/0013_wineappellation_wineregion.py @@ -0,0 +1,41 @@ +# Generated by Django 2.2.4 on 2019-10-28 07:27 + +from django.db import migrations, models +import django.db.models.deletion +import utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('location', '0012_data_migrate'), + ] + + operations = [ + migrations.CreateModel( + name='WineRegion', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', utils.models.TJSONField(help_text='{"en-GB":"some text"}', verbose_name='Name')), + ('country', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='location.Country', verbose_name='country')), + ], + options={ + 'verbose_name': 'wine region', + 'verbose_name_plural': 'wine regions', + }, + bases=(utils.models.TranslatedFieldsMixin, models.Model), + ), + migrations.CreateModel( + name='WineAppellation', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', utils.models.TJSONField(help_text='{"en-GB":"some text"}', verbose_name='Name')), + ('wine_region', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='appellations', to='location.WineRegion', verbose_name='wine region')), + ], + options={ + 'verbose_name': 'wine appellation', + 'verbose_name_plural': 'wine appellations', + }, + bases=(utils.models.TranslatedFieldsMixin, models.Model), + ), + ] diff --git a/apps/product/migrations/0001_initial.py b/apps/product/migrations/0001_initial.py new file mode 100644 index 00000000..a6abbbb8 --- /dev/null +++ b/apps/product/migrations/0001_initial.py @@ -0,0 +1,94 @@ +# Generated by Django 2.2.4 on 2019-10-28 07:27 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import utils.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('location', '0013_wineappellation_wineregion'), + ('establishment', '0044_position_index_name'), + ] + + operations = [ + migrations.CreateModel( + name='ProductType', + 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')), + ('name', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Name')), + ('index_name', models.CharField(choices=[('food', 'Food'), ('wine', 'Wine'), ('liquor', 'Liquor')], db_index=True, max_length=50, unique=True, verbose_name='Index name')), + ('use_subtypes', models.BooleanField(default=True, verbose_name='Use subtypes')), + ], + options={ + 'verbose_name': 'Product type', + 'verbose_name_plural': 'Product types', + }, + bases=(utils.models.TranslatedFieldsMixin, models.Model), + ), + migrations.CreateModel( + name='ProductSubType', + 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')), + ('name', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Name')), + ('index_name', models.CharField(choices=[('rum', 'Rum'), ('other', 'Other')], db_index=True, max_length=50, unique=True, verbose_name='Index name')), + ('product_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subtypes', to='product.ProductType', verbose_name='Product type')), + ], + options={ + 'verbose_name': 'Product subtype', + 'verbose_name_plural': 'Product subtypes', + }, + bases=(utils.models.TranslatedFieldsMixin, models.Model), + ), + migrations.CreateModel( + name='Product', + 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')), + ('category', models.PositiveIntegerField(choices=[(0, 'Common'), (1, 'Online')], default=0)), + ('name', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Name')), + ('description', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Description')), + ('characteristics', django.contrib.postgres.fields.jsonb.JSONField(verbose_name='Characteristics')), + ('available', models.BooleanField(default=True, verbose_name='Available')), + ('public_mark', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='public mark')), + ('country', models.ManyToManyField(to='location.Country', verbose_name='Country')), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_records_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')), + ('establishment', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='establishment.Establishment', verbose_name='establishment')), + ('modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_records_modified', to=settings.AUTH_USER_MODEL, verbose_name='modified by')), + ('product_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='product.ProductType', verbose_name='Type')), + ('subtypes', models.ManyToManyField(blank=True, related_name='products', to='product.ProductSubType', verbose_name='Subtypes')), + ('wine_appellation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='location.WineAppellation', verbose_name='wine appellation')), + ('wine_region', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='wines', to='location.WineRegion', verbose_name='wine region')), + ], + options={ + 'verbose_name': 'Product', + 'verbose_name_plural': 'Products', + }, + bases=(utils.models.TranslatedFieldsMixin, models.Model), + ), + migrations.CreateModel( + name='OnlineProduct', + fields=[ + ], + options={ + 'verbose_name': 'Online product', + 'verbose_name_plural': 'Online products', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('product.product',), + ), + ] From ed09ee78ccbee605163fb3837201823d1ea0e78d Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 28 Oct 2019 13:11:07 +0300 Subject: [PATCH 19/29] small refactoring --- apps/product/filters.py | 20 ++++++++++---------- apps/product/models.py | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/product/filters.py b/apps/product/filters.py index 9318b943..7894a6e6 100644 --- a/apps/product/filters.py +++ b/apps/product/filters.py @@ -9,26 +9,26 @@ class ProductListFilterSet(filters.FilterSet): """Product filter set.""" establishment_id = filters.NumberFilter() - type = filters.ChoiceFilter(method='by_type', - choices=models.ProductType.INDEX_NAME_TYPES) - subtype = filters.ChoiceFilter(method='by_subtype', - choices=models.ProductSubType.INDEX_NAME_TYPES) + product_type = filters.ChoiceFilter(method='by_product_type', + choices=models.ProductType.INDEX_NAME_TYPES) + product_subtype = filters.ChoiceFilter(method='by_product_subtype', + choices=models.ProductSubType.INDEX_NAME_TYPES) class Meta: """Meta class.""" model = models.Product fields = [ 'establishment_id', - 'type', - 'subtype', + 'product_type', + 'product_subtype', ] - def by_type(self, queryset, name, value): + def by_product_type(self, queryset, name, value): if value not in EMPTY_VALUES: - return queryset.by_type(value) + return queryset.by_product_type(value) return queryset - def by_subtype(self, queryset, name, value): + def by_product_subtype(self, queryset, name, value): if value not in EMPTY_VALUES: - return queryset.by_subtype(value) + return queryset.by_product_subtype(value) return queryset diff --git a/apps/product/models.py b/apps/product/models.py index b0b1795e..40007f18 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -91,13 +91,13 @@ class ProductQuerySet(models.QuerySet): def wines(self): return self.filter(type__index_name=ProductType.WINE) - def by_type(self, type: str): + def by_product_type(self, product_type: str): """Filter by type.""" - return self.filter(product_type__index_name=type) + return self.filter(product_type__index_name=product_type) - def by_subtype(self, subtype: str): + def by_product_subtype(self, product_subtype: str): """Filter by subtype.""" - return self.filter(subtypes__index_name=subtype) + return self.filter(subtypes__index_name=product_subtype) class Product(TranslatedFieldsMixin, BaseAttributes): From 251edda2ea876fb9fe73f6c9e9dc0a19c49137bb Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Tue, 29 Oct 2019 15:19:07 +0300 Subject: [PATCH 20/29] update Document ViewSet's pagination class --- apps/search_indexes/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index fd1c5b7a..42f4a87e 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -8,7 +8,7 @@ from django_elasticsearch_dsl_drf.filter_backends import ( from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet from search_indexes import serializers, filters from search_indexes.documents import EstablishmentDocument, NewsDocument -from utils.pagination import ProjectPageNumberPagination +from utils.pagination import ProjectMobilePagination class NewsDocumentViewSet(BaseDocumentViewSet): @@ -16,7 +16,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet): document = NewsDocument lookup_field = 'slug' - pagination_class = ProjectPageNumberPagination + pagination_class = ProjectMobilePagination permission_classes = (permissions.AllowAny,) serializer_class = serializers.NewsDocumentSerializer ordering = ('id',) @@ -53,7 +53,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): document = EstablishmentDocument lookup_field = 'slug' - pagination_class = ProjectPageNumberPagination + pagination_class = ProjectMobilePagination permission_classes = (permissions.AllowAny,) serializer_class = serializers.EstablishmentDocumentSerializer ordering = ('id',) From 13eaa942617f697f012977da3b28d0f804ee3ed5 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 29 Oct 2019 15:23:26 +0300 Subject: [PATCH 21/29] added filter to FavoritesEstablishmentListView --- apps/favorites/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/favorites/views.py b/apps/favorites/views.py index 5d99ed4b..6b7bdab0 100644 --- a/apps/favorites/views.py +++ b/apps/favorites/views.py @@ -1,6 +1,7 @@ """Views for app favorites.""" from rest_framework import generics from establishment.models import Establishment +from establishment.filters import EstablishmentFilter from establishment.serializers import EstablishmentBaseSerializer from .models import Favorites @@ -17,6 +18,7 @@ class FavoritesEstablishmentListView(generics.ListAPIView): """List views for favorites""" serializer_class = EstablishmentBaseSerializer + filter_class = EstablishmentFilter def get_queryset(self): """Override get_queryset method""" From 03c75efeced62f941d30362481a921ae019fdca7 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 29 Oct 2019 17:16:39 +0300 Subject: [PATCH 22/29] gm-259, gm-260, gm-261 --- apps/account/models.py | 14 ++++++++ apps/establishment/models.py | 8 ++--- apps/establishment/serializers/common.py | 34 +++++------------- apps/establishment/urls/common.py | 3 +- apps/establishment/views/web.py | 24 +++---------- apps/favorites/urls.py | 2 ++ apps/favorites/views.py | 17 ++++++++- apps/product/filters.py | 2 +- apps/product/migrations/0002_product_slug.py | 18 ++++++++++ apps/product/models.py | 4 +++ apps/product/serializers/common.py | 37 ++++++++++++++++++-- apps/product/urls/common.py | 4 ++- apps/product/views/common.py | 21 ++++++++++- apps/recipe/models.py | 8 ++--- apps/utils/serializers.py | 26 ++++++++++++++ 15 files changed, 160 insertions(+), 62 deletions(-) create mode 100644 apps/product/migrations/0002_product_slug.py diff --git a/apps/account/models.py b/apps/account/models.py index 2f8c97cc..cc80721a 100644 --- a/apps/account/models.py +++ b/apps/account/models.py @@ -241,6 +241,20 @@ class User(AbstractUser): template_name=settings.CHANGE_EMAIL_TEMPLATE, context=context) + @property + def favorite_establishment_ids(self): + """Return establishment IDs that in favorites for current user.""" + return self.favorites.by_content_type(app_label='establishment', + model='establishment')\ + .values_list('object_id', flat=True) + + @property + def favorite_recipe_ids(self): + """Return recipe IDs that in favorites for current user.""" + return self.favorites.by_content_type(app_label='recipe', + model='recipe')\ + .values_list('object_id', flat=True) + class UserRole(ProjectBaseMixin): """UserRole model.""" diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 0ca2463f..c31f3a60 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -248,14 +248,12 @@ class EstablishmentQuerySet(models.QuerySet): def annotate_in_favorites(self, user): """Annotate flag in_favorites""" - favorite_establishments = [] + favorite_establishment_ids = [] if user.is_authenticated: - favorite_establishments = user.favorites.by_content_type(app_label='establishment', - model='establishment') \ - .values_list('object_id', flat=True) + favorite_establishment_ids = user.favorite_establishment_ids return self.annotate(in_favorites=Case( When( - id__in=favorite_establishments, + id__in=favorite_establishment_ids, then=True), default=False, output_field=models.BooleanField(default=False))) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 14be142a..5e7c76d4 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -5,15 +5,14 @@ from rest_framework import serializers from comment import models as comment_models from comment.serializers import common as comment_serializers from establishment import models -from favorites.models import Favorites from location.serializers import AddressBaseSerializer from main.serializers import AwardSerializer, CurrencySerializer from review import models as review_models from tag.serializers import TagBaseSerializer from timetable.serialziers import ScheduleRUDSerializer from utils import exceptions as utils_exceptions -from utils.serializers import ProjectModelSerializer -from utils.serializers import TranslatedField +from utils.serializers import (ProjectModelSerializer, TranslatedField, + FavoritesCreateSerializer) class ContactPhonesSerializer(serializers.ModelSerializer): @@ -281,26 +280,13 @@ class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer): ] -class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer): - """Create comment serializer""" - - class Meta: - """Serializer for model Comment""" - model = Favorites - fields = [ - 'id', - 'created', - ] - - def get_user(self): - """Get user from request""" - return self.context.get('request').user +class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer): + """Serializer to favorite object w/ model Establishment.""" def validate(self, attrs): - """Override validate method""" + """Overridden validate method""" # Check establishment object - establishment_slug = self.context.get('request').parser_context.get('kwargs').get('slug') - establishment_qs = models.Establishment.objects.filter(slug=establishment_slug) + establishment_qs = models.Establishment.objects.filter(slug=self.slug) # Check establishment obj by slug from lookup_kwarg if not establishment_qs.exists(): @@ -309,18 +295,16 @@ class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer): establishment = establishment_qs.first() # Check existence in favorites - if self.get_user().favorites.by_content_type(app_label='establishment', - model='establishment')\ - .by_object_id(object_id=establishment.id).exists(): + if establishment.favorites.filter(user=self.user).exists(): raise utils_exceptions.FavoritesError() attrs['establishment'] = establishment return attrs def create(self, validated_data, *args, **kwargs): - """Override create method""" + """Overridden create method""" validated_data.update({ - 'user': self.get_user(), + 'user': self.user, 'content_object': validated_data.pop('establishment') }) return super().create(validated_data) diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index 8d9453c1..49cd3631 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('wineries/', views.WineriesListView.as_view(), name='wineries-list'), path('slug//', views.EstablishmentRetrieveView.as_view(), name='detail'), path('slug//similar/', views.EstablishmentSimilarListView.as_view(), name='similar'), path('slug//comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'), @@ -18,5 +17,5 @@ urlpatterns = [ path('slug//comments//', views.EstablishmentCommentRUDView.as_view(), name='rud-comment'), path('slug//favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), - name='add-to-favorites') + name='create-destroy-favorites') ] diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 0699d9d0..6a1de00e 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -138,15 +138,12 @@ class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.D """ Returns the object the view is displaying. """ - establishment_obj = get_object_or_404(models.Establishment, - slug=self.kwargs['slug']) - obj = get_object_or_404( - self.request.user.favorites.by_content_type(app_label='establishment', - model='establishment') - .by_object_id(object_id=establishment_obj.pk)) + establishment = get_object_or_404(models.Establishment, + slug=self.kwargs['slug']) + favorites = get_object_or_404(establishment.favorites.filter(user=self.request.user)) # May raise a permission denied - self.check_object_permissions(self.request, obj) - return obj + self.check_object_permissions(self.request, favorites) + return favorites class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView): @@ -170,14 +167,3 @@ class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIVi return qs.by_distance_from_point(**{k: v for k, v in filter_kwargs.items() if v is not None}) return qs - - -# Wineries -# todo: find out about difference between subtypes data -# class WineriesListView(EstablishmentListView): -# """Return list establishments with type Wineries""" -# -# def get_queryset(self): -# """Overridden get_queryset method.""" -# qs = super(WineriesListView, self).get_queryset() -# return qs.with_type_related().wineries() diff --git a/apps/favorites/urls.py b/apps/favorites/urls.py index bd0c1d16..ad4c6e9d 100644 --- a/apps/favorites/urls.py +++ b/apps/favorites/urls.py @@ -8,4 +8,6 @@ app_name = 'favorites' urlpatterns = [ path('establishments/', views.FavoritesEstablishmentListView.as_view(), name='establishment-list'), + path('products/', views.FavoritesProductListView.as_view(), + name='product-list'), ] diff --git a/apps/favorites/views.py b/apps/favorites/views.py index 6b7bdab0..d2973142 100644 --- a/apps/favorites/views.py +++ b/apps/favorites/views.py @@ -3,6 +3,9 @@ from rest_framework import generics from establishment.models import Establishment from establishment.filters import EstablishmentFilter from establishment.serializers import EstablishmentBaseSerializer +from product.models import Product +from product.serializers import ProductBaseSerializer +from product.filters import ProductFilterSet from .models import Favorites @@ -15,7 +18,7 @@ class FavoritesBaseView(generics.GenericAPIView): class FavoritesEstablishmentListView(generics.ListAPIView): - """List views for favorites""" + """List views for establishments in favorites.""" serializer_class = EstablishmentBaseSerializer filter_class = EstablishmentFilter @@ -24,3 +27,15 @@ class FavoritesEstablishmentListView(generics.ListAPIView): """Override get_queryset method""" return Establishment.objects.filter(favorites__user=self.request.user)\ .order_by('-favorites') + + +class FavoritesProductListView(generics.ListAPIView): + """List views for products in favorites.""" + + serializer_class = ProductBaseSerializer + filter_class = ProductFilterSet + + def get_queryset(self): + """Override get_queryset method""" + return Product.objects.filter(favorites__user=self.request.user)\ + .order_by('-favorites') diff --git a/apps/product/filters.py b/apps/product/filters.py index 7894a6e6..a30147eb 100644 --- a/apps/product/filters.py +++ b/apps/product/filters.py @@ -5,7 +5,7 @@ from django_filters import rest_framework as filters from product import models -class ProductListFilterSet(filters.FilterSet): +class ProductFilterSet(filters.FilterSet): """Product filter set.""" establishment_id = filters.NumberFilter() diff --git a/apps/product/migrations/0002_product_slug.py b/apps/product/migrations/0002_product_slug.py new file mode 100644 index 00000000..a4605464 --- /dev/null +++ b/apps/product/migrations/0002_product_slug.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-10-29 14:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='slug', + field=models.SlugField(max_length=255, null=True, unique=True, verbose_name='Establishment slug'), + ), + ] diff --git a/apps/product/models.py b/apps/product/models.py index 40007f18..d4011fa0 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -1,5 +1,6 @@ """Product app models.""" from django.db import models +from django.contrib.contenttypes import fields as generic from django.core.exceptions import ValidationError from django.contrib.postgres.fields import JSONField from django.utils.translation import gettext_lazy as _ @@ -141,6 +142,9 @@ class Product(TranslatedFieldsMixin, BaseAttributes): wine_appellation = models.ForeignKey('location.WineAppellation', on_delete=models.PROTECT, blank=True, null=True, verbose_name=_('wine appellation')) + slug = models.SlugField(unique=True, max_length=255, null=True, + verbose_name=_('Establishment slug')) + favorites = generic.GenericRelation(to='favorites.Favorites') objects = ProductManager.from_queryset(ProductQuerySet)() diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index fb2f7aeb..7ebfaa28 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -1,9 +1,11 @@ """Product app serializers.""" from rest_framework import serializers -from utils.serializers import TranslatedField +from utils.serializers import TranslatedField, FavoritesCreateSerializer from product.models import Product, ProductSubType, ProductType +from utils import exceptions as utils_exceptions +from django.utils.translation import gettext_lazy as _ from location.serializers import (WineRegionBaseSerializer, WineAppellationBaseSerializer, - CountrySimpleSerializer) + CountrySimpleSerializer) class ProductSubTypeBaseSerializer(serializers.ModelSerializer): @@ -50,6 +52,7 @@ class ProductBaseSerializer(serializers.ModelSerializer): model = Product fields = [ 'id', + 'slug', 'name_translated', 'category_display', 'description_translated', @@ -61,3 +64,33 @@ class ProductBaseSerializer(serializers.ModelSerializer): 'wine_appellation', 'available_countries', ] + + +class ProductFavoritesCreateSerializer(FavoritesCreateSerializer): + """Serializer to create favorite object w/ model Product.""" + + def validate(self, attrs): + """Overridden validate method""" + # Check establishment object + product_qs = Product.objects.filter(slug=self.slug) + + # Check establishment obj by slug from lookup_kwarg + if not product_qs.exists(): + raise serializers.ValidationError({'detail': _('Object not found.')}) + else: + product = product_qs.first() + + # Check existence in favorites + if product.favorites.filter(user=self.user).exists(): + raise utils_exceptions.FavoritesError() + + attrs['product'] = product + return attrs + + def create(self, validated_data, *args, **kwargs): + """Overridden create method""" + validated_data.update({ + 'user': self.user, + 'content_object': validated_data.pop('product') + }) + return super().create(validated_data) \ No newline at end of file diff --git a/apps/product/urls/common.py b/apps/product/urls/common.py index 57abf4f0..d0dbb8a9 100644 --- a/apps/product/urls/common.py +++ b/apps/product/urls/common.py @@ -6,5 +6,7 @@ from product import views app_name = 'product' urlpatterns = [ - path('', views.ProductListView.as_view(), name='list') + path('', views.ProductListView.as_view(), name='list'), + path('slug//favorites/', views.CreateFavoriteProductView.as_view(), + name='create-destroy-favorites') ] diff --git a/apps/product/views/common.py b/apps/product/views/common.py index 403781e4..63b9677f 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -1,5 +1,6 @@ """Product app views.""" from rest_framework import generics, permissions +from django.shortcuts import get_object_or_404 from product.models import Product from product import serializers from product import filters @@ -17,4 +18,22 @@ class ProductListView(ProductBaseView, generics.ListAPIView): """List view for model Product.""" permission_classes = (permissions.AllowAny, ) serializer_class = serializers.ProductBaseSerializer - filter_class = filters.ProductListFilterSet + filter_class = filters.ProductFilterSet + + +class CreateFavoriteProductView(generics.CreateAPIView, + generics.DestroyAPIView): + """View for create/destroy product in favorites.""" + serializer_class = serializers.ProductFavoritesCreateSerializer + lookup_field = 'slug' + + def get_object(self): + """ + Returns the object the view is displaying. + """ + product = get_object_or_404(Product, slug=self.kwargs['slug']) + favorites = get_object_or_404(product.favorites.filter(user=self.request.user)) + + # May raise a permission denied + self.check_object_permissions(self.request, favorites) + return favorites diff --git a/apps/recipe/models.py b/apps/recipe/models.py index 6df7adc2..349fed7b 100644 --- a/apps/recipe/models.py +++ b/apps/recipe/models.py @@ -15,14 +15,12 @@ class RecipeQuerySet(models.QuerySet): def annotate_in_favorites(self, user): """Annotate flag in_favorites""" - favorite_establishments = [] + favorite_recipe_ids = [] if user.is_authenticated: - favorite_establishments = user.favorites.by_content_type(app_label='recipe', - model='recipe') \ - .values_list('object_id', flat=True) + favorite_recipe_ids = user.favorite_recipe_ids return self.annotate(in_favorites=models.Case( models.When( - id__in=favorite_establishments, + id__in=favorite_recipe_ids, then=True), default=False, output_field=models.BooleanField(default=False))) diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index eeff1043..bd7e8b8e 100644 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -4,6 +4,7 @@ from django.core import exceptions from rest_framework import serializers from utils import models from translation.models import Language +from favorites.models import Favorites class EmptySerializer(serializers.Serializer): @@ -72,3 +73,28 @@ class ProjectModelSerializer(serializers.ModelSerializer): """Overrided ModelSerializer.""" serializers.ModelSerializer.serializer_field_mapping[models.TJSONField] = TJSONField + + +class FavoritesCreateSerializer(serializers.ModelSerializer): + """Serializer to favorite object.""" + + class Meta: + """Serializer for model Comment.""" + model = Favorites + fields = [ + 'id', + 'created', + ] + + @property + def request(self): + return self.context.get('request') + + @property + def user(self): + """Get user from request""" + return self.request.user + + @property + def slug(self): + return self.request.parser_context.get('kwargs').get('slug') From 3f233fd2460e724677f340bed0fc5af693f33936 Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Tue, 29 Oct 2019 18:13:21 +0300 Subject: [PATCH 23/29] update establishment document index & establishment documentview --- .../search_indexes/documents/establishment.py | 19 +------------------ apps/search_indexes/serializers.py | 7 +++++++ 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 88f7e44a..98138146 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -10,16 +10,6 @@ EstablishmentIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, EstablishmentIndex.settings(number_of_shards=1, number_of_replicas=1) -# todo: check & refactor -class ObjectField(fields.ObjectField): - - def get_value_from_instance(self, *args, **kwargs): - value = super(ObjectField, self).get_value_from_instance(*args, **kwargs) - if value == {}: - return None - return value - - @EstablishmentIndex.doc_type class EstablishmentDocument(Document): """Establishment document.""" @@ -63,7 +53,7 @@ class EstablishmentDocument(Document): 'closed_at': fields.KeywordField(attr='closed_at_str'), } )) - address = ObjectField( + address = fields.ObjectField( properties={ 'id': fields.IntegerField(), 'street_name_1': fields.TextField( @@ -93,13 +83,6 @@ class EstablishmentDocument(Document): ), }, ) - # todo: need to fix - # collections = fields.ObjectField( - # properties={ - # 'id': fields.IntegerField(attr='collection.id'), - # 'collection_type': fields.IntegerField(attr='collection.collection_type'), - # }, - # multi=True) class Django: diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index cb1fe735..ee2e2ee8 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -13,6 +13,8 @@ class TagsDocumentSerializer(serializers.Serializer): label_translated = serializers.SerializerMethodField() def get_label_translated(self, obj): + if isinstance(obj, dict): + return get_translated_value(obj.get('label')) return get_translated_value(obj.label) @@ -29,6 +31,11 @@ class AddressDocumentSerializer(serializers.Serializer): geo_lon = serializers.FloatField(allow_null=True, source='coordinates.lon') geo_lat = serializers.FloatField(allow_null=True, source='coordinates.lat') + def to_representation(self, instance): + if len(instance) != 0: + return super().to_representation(instance) + return None + class ScheduleDocumentSerializer(serializers.Serializer): """Schedule serializer for ES Document""" From 9fecbfe98f7d34e271440c3d99e6da271035d5cc Mon Sep 17 00:00:00 2001 From: evgeniy-st Date: Tue, 29 Oct 2019 19:17:44 +0300 Subject: [PATCH 24/29] EstablishmentViewSet default ordering and filtration --- apps/search_indexes/documents/establishment.py | 3 ++- apps/search_indexes/serializers.py | 5 ++++- apps/search_indexes/views.py | 14 ++++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 98138146..7eac2d6c 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -91,6 +91,7 @@ class EstablishmentDocument(Document): 'id', 'name', 'name_translated', + 'is_publish', 'price_level', 'toque_number', 'public_mark', @@ -98,4 +99,4 @@ class EstablishmentDocument(Document): ) def get_queryset(self): - return super().get_queryset().published().with_es_related() + return super().get_queryset().with_es_related() diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index ee2e2ee8..b9df01b7 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -1,5 +1,6 @@ """Search indexes serializers.""" from rest_framework import serializers +from elasticsearch_dsl import AttrDict from django_elasticsearch_dsl_drf.serializers import DocumentSerializer from news.serializers import NewsTypeSerializer from search_indexes.documents import EstablishmentDocument, NewsDocument @@ -31,8 +32,10 @@ class AddressDocumentSerializer(serializers.Serializer): geo_lon = serializers.FloatField(allow_null=True, source='coordinates.lon') geo_lat = serializers.FloatField(allow_null=True, source='coordinates.lat') + # todo: refator def to_representation(self, instance): - if len(instance) != 0: + if instance != AttrDict(d={}) or \ + (isinstance(instance, dict) and len(instance) != 0): return super().to_representation(instance) return None diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 42f4a87e..72ce2efa 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -3,7 +3,8 @@ from rest_framework import permissions from django_elasticsearch_dsl_drf import constants from django_elasticsearch_dsl_drf.filter_backends import ( FilteringFilterBackend, - GeoSpatialFilteringFilterBackend + GeoSpatialFilteringFilterBackend, + DefaultOrderingFilterBackend, ) from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet from search_indexes import serializers, filters @@ -56,12 +57,20 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): pagination_class = ProjectMobilePagination permission_classes = (permissions.AllowAny,) serializer_class = serializers.EstablishmentDocumentSerializer - ordering = ('id',) + + def get_queryset(self): + qs = super(EstablishmentDocumentViewSet, self).get_queryset() + qs = qs.filter('match', is_publish=True) + if self.request.country_code: + qs = qs.filter('term', + **{'address.city.country.code': self.request.country_code}) + return qs filter_backends = [ FilteringFilterBackend, filters.CustomSearchFilterBackend, GeoSpatialFilteringFilterBackend, + DefaultOrderingFilterBackend, ] search_fields = { @@ -72,6 +81,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): translated_search_fields = ( 'description', ) + ordering = 'id' filter_fields = { 'slug': 'slug', 'tag': { From a0aa503cfa59211a1286d745f2a2c5bf9e99c024 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 25/29] Add index_name type field to geo --- apps/establishment/serializers/common.py | 25 ++++++++++++++++++++++++ apps/establishment/views/web.py | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 14be142a..12e217a9 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -118,6 +118,18 @@ class EstablishmentTypeBaseSerializer(serializers.ModelSerializer): 'use_subtypes': {'write_only': True}, } +class EstablishmentTypeGeoSerializer(EstablishmentTypeBaseSerializer): + """Serializer for EstablishmentType model w/ index_name.""" + + class Meta(EstablishmentTypeBaseSerializer.Meta): + fields = EstablishmentTypeBaseSerializer.Meta.fields + [ + 'index_name' + ] + extra_kwargs = { + **EstablishmentTypeBaseSerializer.Meta.extra_kwargs, + 'index_name': {'read_only': True}, + } + class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer): """Serializer for EstablishmentSubType models.""" @@ -185,6 +197,19 @@ class EstablishmentBaseSerializer(ProjectModelSerializer): ] +class EstablishmentGeoSerializer(EstablishmentBaseSerializer): + """Serializer for Geo view.""" + + type = EstablishmentTypeGeoSerializer(source='establishment_type', read_only=True) + + class Meta(EstablishmentBaseSerializer.Meta): + """Meta class.""" + + fields = EstablishmentBaseSerializer.Meta.fields + [ + 'type' + ] + + class EstablishmentDetailSerializer(EstablishmentBaseSerializer): """Serializer for Establishment model.""" diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 0699d9d0..63033d4b 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -152,7 +152,7 @@ class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.D class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView): """Resource for getting list of nearest establishments.""" - serializer_class = serializers.EstablishmentBaseSerializer + serializer_class = serializers.EstablishmentGeoSerializer filter_class = filters.EstablishmentFilter def get_queryset(self): From 9e6634f1a6e1534966def39c29d8e146693f8236 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: Wed, 30 Oct 2019 10:48:59 +0300 Subject: [PATCH 26/29] Fix fab --- fabfile.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/fabfile.py b/fabfile.py index 9ad7f871..8bcff1c2 100644 --- a/fabfile.py +++ b/fabfile.py @@ -53,12 +53,13 @@ def collectstatic(): def deploy(branch=None): - fetch() - install_requirements() - migrate() - collectstatic() - touch() - kill_celery() + if env.roledefs[role]['branch'] !='develop': + fetch() + install_requirements() + migrate() + collectstatic() + touch() + kill_celery() def rev(): From 24ee90e8a09d8a28bd6b9d534c0e7111e770a6e7 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: Wed, 30 Oct 2019 10:56:48 +0300 Subject: [PATCH 27/29] Fix fab --- fabfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fabfile.py b/fabfile.py index 8bcff1c2..595a0903 100644 --- a/fabfile.py +++ b/fabfile.py @@ -53,6 +53,7 @@ def collectstatic(): def deploy(branch=None): + role = env.roles[0] if env.roledefs[role]['branch'] !='develop': fetch() install_requirements() From 156a5479aa08dfe0b4287ee5bd830e8e7fc4a508 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: Wed, 30 Oct 2019 11:42:23 +0300 Subject: [PATCH 28/29] Fix --- fabfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fabfile.py b/fabfile.py index 595a0903..e8a52f85 100644 --- a/fabfile.py +++ b/fabfile.py @@ -54,7 +54,7 @@ def collectstatic(): def deploy(branch=None): role = env.roles[0] - if env.roledefs[role]['branch'] !='develop': + if env.roledefs[role]['branch'] != 'develop': fetch() install_requirements() migrate() From 42c596b5920d384b8b61d13a1f0fedae44ebc84f Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 16 Oct 2019 14:11:07 +0300 Subject: [PATCH 29/29] Weekday for mobile devices --- apps/timetable/serialziers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/timetable/serialziers.py b/apps/timetable/serialziers.py index babe33c1..37725e1d 100644 --- a/apps/timetable/serialziers.py +++ b/apps/timetable/serialziers.py @@ -27,6 +27,7 @@ class ScheduleRUDSerializer(serializers.ModelSerializer): fields = [ 'id', 'weekday_display', + 'weekday', 'lunch_start', 'lunch_end', 'dinner_start',