From 2ad2b3b56d229b92431ff02e6f88c9e04e3045c3 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: Tue, 24 Sep 2019 12:13:39 +0300 Subject: [PATCH 01/36] Refactor --- apps/account/tests/tests_common.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/account/tests/tests_common.py b/apps/account/tests/tests_common.py index 0992b930..05185952 100644 --- a/apps/account/tests/tests_common.py +++ b/apps/account/tests/tests_common.py @@ -7,21 +7,19 @@ from django.urls import reverse class AccountUserTests(APITestCase): - - url = reverse('web:account:user-retrieve-update') - def setUp(self): self.data = get_tokens_for_user() def test_user_url(self): - response = self.client.get(self.url) + url = reverse('web:account:user-retrieve-update') + response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.client.cookies = SimpleCookie( {'access_token': self.data['tokens'].get('access_token'), 'refresh_token': self.data['tokens'].get('access_token')}) - response = self.client.get(self.url) + response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) data = { @@ -33,19 +31,16 @@ class AccountUserTests(APITestCase): "email": "sedragurdatest@desoz.com", "newsletter": self.data["newsletter"] } - response = self.client.patch(self.url, data=data, format='json') + response = self.client.patch(url, data=data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) data["email"] = "sedragurdatest2@desoz.com" - response = self.client.put(self.url, data=data, format='json') + response = self.client.put(url, data=data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) class AccountChangePasswordTests(APITestCase): - - url = reverse('web:account:change-password') - def setUp(self): self.data = get_tokens_for_user() @@ -54,15 +49,16 @@ class AccountChangePasswordTests(APITestCase): "old_password": self.data["password"], "password": "new password" } + url = reverse('web:account:change-password') - response = self.client.patch(self.url, data=data, format='json') + response = self.client.patch(url, data=data, format='json') self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.client.cookies = SimpleCookie( {'access_token': self.data['tokens'].get('access_token'), 'refresh_token': self.data['tokens'].get('access_token')}) - response = self.client.patch(self.url, data=data, format='json') + response = self.client.patch(url, data=data, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) From eebd66fa275c4ab45816ca97939fa9ade24048f6 Mon Sep 17 00:00:00 2001 From: littlewolf Date: Tue, 24 Sep 2019 13:35:05 +0300 Subject: [PATCH 02/36] Add tests to created_by and modified_by --- apps/utils/tests.py | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/apps/utils/tests.py b/apps/utils/tests.py index 1c9fa71d..786ed940 100644 --- a/apps/utils/tests.py +++ b/apps/utils/tests.py @@ -8,6 +8,8 @@ from http.cookies import SimpleCookie from account.models import User from news.models import News, NewsType +from establishment.models import Establishment, EstablishmentType, Employee + class BaseTestCase(APITestCase): @@ -28,6 +30,12 @@ class BaseTestCase(APITestCase): 'locale': "en" }) + +class TranslateFieldTests(BaseTestCase): + + def setUp(self): + super().setUp() + self.news_type = NewsType.objects.create(name="Test news type") self.news_item = News.objects.create( @@ -45,15 +53,9 @@ class BaseTestCase(APITestCase): news_type=self.news_type ) - -class TranslateFieldModel(BaseTestCase): - def test_model_field(self): self.assertIsNotNone(getattr(self.news_item, "title_translated", None)) - -class TranslateFieldReview(BaseTestCase): - def test_read_locale(self): response = self.client.get(f"/api/web/news/{self.news_item.id}/", format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -62,3 +64,32 @@ class TranslateFieldReview(BaseTestCase): self.assertIn("title_translated", news_data) self.assertEqual(news_data['title_translated'], "Test news item") + + +class BaseAttributeTests(BaseTestCase): + + def setUp(self): + super().setUp() + + self.establishment_type = EstablishmentType.objects.create(name="Test establishment type") + self.establishment = Establishment.objects.create( + name="Test establishment", + establishment_type_id=self.establishment_type.id, + is_publish=True + ) + + def test_base_attr_api(self): + data = { + 'user': self.user.id, + 'name': 'Test name' + } + + response = self.client.post('/api/back/establishments/employees/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response_data = response.json() + self.assertIn("id", response_data) + + employee = Employee.objects.get(id=response_data['id']) + self.assertEqual(self.user, employee.created_by) + self.assertEqual(self.user, employee.modified_by) From 28ae1ab8d14f02aa89031a61ac021b40a1418f44 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: Tue, 24 Sep 2019 14:03:55 +0300 Subject: [PATCH 03/36] Test reset password --- apps/account/tests/tests_web.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 apps/account/tests/tests_web.py diff --git a/apps/account/tests/tests_web.py b/apps/account/tests/tests_web.py new file mode 100644 index 00000000..33ba109f --- /dev/null +++ b/apps/account/tests/tests_web.py @@ -0,0 +1,24 @@ +from rest_framework.test import APITestCase +from rest_framework import status +from authorization.tests.tests import get_tokens_for_user +from django.urls import reverse + + +class AccountResetPassWordTests(APITestCase): + + def setUp(self): + self.data = get_tokens_for_user() + + def test_reset_password(self): + url = reverse('web:account:password-reset') + data = { + "username_or_email": self.data["email"] + } + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = { + "username_or_email": self.data["username"] + } + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, status.HTTP_200_OK) \ No newline at end of file From 67a5f8b2f0bedc4eb641135eb520aa974af577c5 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: Tue, 24 Sep 2019 15:07:00 +0300 Subject: [PATCH 04/36] Test web account --- apps/account/tests/tests_web.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/apps/account/tests/tests_web.py b/apps/account/tests/tests_web.py index 33ba109f..e771b31a 100644 --- a/apps/account/tests/tests_web.py +++ b/apps/account/tests/tests_web.py @@ -2,6 +2,7 @@ from rest_framework.test import APITestCase from rest_framework import status from authorization.tests.tests import get_tokens_for_user from django.urls import reverse +from account.models import User class AccountResetPassWordTests(APITestCase): @@ -21,4 +22,28 @@ class AccountResetPassWordTests(APITestCase): "username_or_email": self.data["username"] } response = self.client.post(url, data=data) - self.assertEqual(response.status_code, status.HTTP_200_OK) \ No newline at end of file + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class AccountResetPassWordTests(APITestCase): + + def setUp(self): + self.data = get_tokens_for_user() + + def test_reset_password_confirm(self): + data ={ + "password": "newpasswordnewpassword" + } + user = User.objects.get(email=self.data["email"]) + token = user.reset_password_token + uidb64 = user.get_user_uidb64 + + url = reverse('web:account:password-reset-confirm', kwargs={ + 'uidb64': uidb64, + 'token': token + }) + response = self.client.patch(url, data=data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + + From 82d29d2795f9564f79dced9f713ca44f3efdc440 Mon Sep 17 00:00:00 2001 From: littlewolf Date: Tue, 24 Sep 2019 15:17:08 +0300 Subject: [PATCH 05/36] Add signal --- apps/utils/models.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apps/utils/models.py b/apps/utils/models.py index 632cf4a2..929b5e5b 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -11,6 +11,9 @@ from easy_thumbnails.fields import ThumbnailerImageField from utils.methods import image_path, svg_image_path from utils.validators import svg_image_validator +from django.db.models.signals import pre_save +from django.dispatch import receiver + class ProjectBaseMixin(models.Model): """Base mixin model.""" @@ -123,6 +126,23 @@ class BaseAttributes(ProjectBaseMixin): null=True, related_name='%(class)s_records_modified' ) + @receiver(pre_save) + def _pre_save(sender, instance, **kwargs): + if not issubclass(sender, BaseAttributes): + return + + # debug + from establishment.models import Employee + if not isinstance(instance, Employee): + return + + user = False + + instance.modified_by = user + + if instance._state.adding: + instance.created_by = user + class Meta: """Meta class.""" From a83a62b26b1f22b1cfa6818d682a1c71fff439d2 Mon Sep 17 00:00:00 2001 From: michail Date: Tue, 24 Sep 2019 17:32:18 +0500 Subject: [PATCH 06/36] Add notification tests --- apps/notification/tests.py | 116 +++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 apps/notification/tests.py diff --git a/apps/notification/tests.py b/apps/notification/tests.py new file mode 100644 index 00000000..d78c7fca --- /dev/null +++ b/apps/notification/tests.py @@ -0,0 +1,116 @@ +from http.cookies import SimpleCookie + +from django.test import TestCase +from rest_framework.test import APITestCase +from rest_framework import status + +from account.models import User +from notification.models import Subscriber + + +class BaseTestCase(APITestCase): + + def setUp(self): + self.username = 'sedragurda' + self.password = 'sedragurdaredips19' + self.email = 'sedragurda@desoz.com' + self.user = User.objects.create_user(username=self.username, email=self.email, password=self.password) + # get tokkens + tokkens = User.create_jwt_tokens(self.user) + self.client.cookies = SimpleCookie({'access_token': tokkens.get('access_token'), + 'refresh_token': tokkens.get('refresh_token')}) + + +class NotificationAnonSubscribeTestCase(APITestCase): + + def test_subscribe(self): + + test_data = { + "email": "test@email.com", + "state": 1 + } + + response = self.client.post("/api/web/notifications/subscribe/", data=test_data, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["email"], test_data["email"]) + self.assertEqual(response.json()["state"], test_data["state"]) + + +class NotificationSubscribeTestCase(BaseTestCase): + + def setUp(self): + super().setUp() + + self.test_data = { + "email": self.email, + "state": 1 + } + + def test_subscribe(self): + + response = self.client.post("/api/web/notifications/subscribe/", data=self.test_data, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.json()["email"], self.email) + self.assertEqual(response.json()["state"], self.test_data["state"]) + + def test_subscribe_info_auth_user(self): + + Subscriber.objects.create(user=self.user, email=self.email, state=1) + + response = self.client.get("/api/web/notifications/subscribe-info/", data=self.test_data, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class NotificationSubscribeInfoTestCase(APITestCase): + + def test_subscribe_info(self): + + self.username = 'sedragurda' + self.password = 'sedragurdaredips19' + self.email = 'sedragurda@desoz.com' + self.user = User.objects.create_user(username=self.username, email=self.email, password=self.password) + + test_subscriber = Subscriber.objects.create(user=self.user, email=self.email, state=1) + + response = self.client.get(f"/api/web/notifications/subscribe-info/{test_subscriber.update_code}/") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class NotificationUnsubscribeAuthUserTestCase(BaseTestCase): + + def test_unsubscribe_auth_user(self): + + Subscriber.objects.create(user=self.user, email=self.email, state=1) + + self.test_data = { + "email": self.email, + "state": 1 + } + + response = self.client.patch("/api/web/notifications/unsubscribe/", data=self.test_data, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class NotificationUnsubscribeTestCase(APITestCase): + + def test_unsubscribe(self): + self.username = 'sedragurda' + self.password = 'sedragurdaredips19' + self.email = 'sedragurda@desoz.com' + self.user = User.objects.create_user(username=self.username, email=self.email, password=self.password) + + self.test_data = { + "email": self.email, + "state": 1 + } + + test_subscriber = Subscriber.objects.create(user=self.user, email=self.email, state=1) + + response = self.client.patch(f"/api/web/notifications/unsubscribe/{test_subscriber.update_code}/", + data=self.test_data, format="json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) From 7e5844a24f293849498d56323a6d215bebc04084 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: Tue, 24 Sep 2019 16:06:31 +0300 Subject: [PATCH 07/36] Fix test auth --- apps/account/tests/tests_common.py | 2 +- apps/account/tests/tests_web.py | 2 +- apps/authorization/tests/__init__.py | 0 apps/authorization/tests/{tests.py => tests_authorization.py} | 1 + 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 apps/authorization/tests/__init__.py rename apps/authorization/tests/{tests.py => tests_authorization.py} (98%) diff --git a/apps/account/tests/tests_common.py b/apps/account/tests/tests_common.py index 05185952..dea807c4 100644 --- a/apps/account/tests/tests_common.py +++ b/apps/account/tests/tests_common.py @@ -1,6 +1,6 @@ from rest_framework.test import APITestCase from rest_framework import status -from authorization.tests.tests import get_tokens_for_user +from authorization.tests.tests_authorization import get_tokens_for_user from http.cookies import SimpleCookie from account.models import User from django.urls import reverse diff --git a/apps/account/tests/tests_web.py b/apps/account/tests/tests_web.py index e771b31a..a3d93f4e 100644 --- a/apps/account/tests/tests_web.py +++ b/apps/account/tests/tests_web.py @@ -1,6 +1,6 @@ from rest_framework.test import APITestCase from rest_framework import status -from authorization.tests.tests import get_tokens_for_user +from authorization.tests.tests_authorization import get_tokens_for_user from django.urls import reverse from account.models import User diff --git a/apps/authorization/tests/__init__.py b/apps/authorization/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/authorization/tests/tests.py b/apps/authorization/tests/tests_authorization.py similarity index 98% rename from apps/authorization/tests/tests.py rename to apps/authorization/tests/tests_authorization.py index d9fd7b71..4a5d2a2b 100644 --- a/apps/authorization/tests/tests.py +++ b/apps/authorization/tests/tests_authorization.py @@ -22,6 +22,7 @@ def get_tokens_for_user( class AuthorizationTests(APITestCase): def setUp(self): + print("Auth!") data = get_tokens_for_user() self.username = data["username"] self.password = data["password"] From 2f61df2c650f4cbef67f1a81849a728154199f7a Mon Sep 17 00:00:00 2001 From: littlewolf Date: Tue, 24 Sep 2019 16:20:04 +0300 Subject: [PATCH 08/36] Add address CRUD tests --- apps/location/tests.py | 74 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/apps/location/tests.py b/apps/location/tests.py index 7ce503c2..cab871a0 100644 --- a/apps/location/tests.py +++ b/apps/location/tests.py @@ -1,3 +1,73 @@ -from django.test import TestCase +from rest_framework.test import APITestCase +from account.models import User +from rest_framework import status +from http.cookies import SimpleCookie -# Create your tests here. +from location.models import City, Region, Country + + +class BaseTestCase(APITestCase): + + def setUp(self): + self.username = 'sedragurda' + self.password = 'sedragurdaredips19' + self.email = 'sedragurda@desoz.com' + self.newsletter = True + self.user = User.objects.create_user( + username=self.username, email=self.email, password=self.password) + + # get tokens + + tokkens = User.create_jwt_tokens(self.user) + self.client.cookies = SimpleCookie( + {'access_token': tokkens.get('access_token'), + 'refresh_token': tokkens.get('refresh_token')}) + + +class AddressTests(BaseTestCase): + + def setUp(self): + super().setUp() + + self.country = Country( + name="Test country", + code="+7" + ) + + self.region = Region( + name="Test region", + code="812", + country=self.country + ) + + self.city = City( + name="Test region", + code="812", + region=self.region, + country=self.country + ) + + def test_address_CRUD(self): + response = self.client.get('/api/back/location/addresses/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = { + 'user': self.user.id, + 'name': 'Test name' + } + + response = self.client.post('/api/back/location/addresses/', data=data) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.get('/api/back/location/addresses/1/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + update_data = { + 'name': 'Test new name' + } + + response = self.client.patch('/api/back/location/addresses/1/', data=update_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.delete('/api/back/location/addresses/1/', format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) From ecdf2fcaa095446eadf0ff0c56324a52ac2ea117 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 24 Sep 2019 16:45:36 +0300 Subject: [PATCH 09/36] Change slugs and str url order --- apps/news/urls/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/news/urls/web.py b/apps/news/urls/web.py index c483b05a..10513910 100644 --- a/apps/news/urls/web.py +++ b/apps/news/urls/web.py @@ -6,6 +6,6 @@ app_name = 'news' urlpatterns = [ path('', views.NewsListView.as_view(), name='list'), - path('/', views.NewsDetailView.as_view(), name='rud'), path('types/', views.NewsTypeListView.as_view(), name='type'), + path('/', views.NewsDetailView.as_view(), name='rud'), ] From 77d39954c39b4c1584409e6f322cd59ca9737036 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 24 Sep 2019 17:54:22 +0300 Subject: [PATCH 10/36] added ordering to list of favorites --- apps/favorites/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/favorites/views.py b/apps/favorites/views.py index aed11709..a80960a8 100644 --- a/apps/favorites/views.py +++ b/apps/favorites/views.py @@ -19,4 +19,5 @@ class FavoritesEstablishmentListView(generics.ListAPIView): def get_queryset(self): """Override get_queryset method""" - return Establishment.objects.filter(favorites__user=self.request.user) + return Establishment.objects.filter(favorites__user=self.request.user)\ + .order_by('-favorites') From a55695b12060210fc3da6b40098cea16578b7c86 Mon Sep 17 00:00:00 2001 From: littlewolf Date: Tue, 24 Sep 2019 20:12:09 +0300 Subject: [PATCH 11/36] Switch to decorator Add update test --- apps/establishment/serializers/back.py | 9 ++++++- apps/utils/decorators.py | 35 ++++++++++++++++++++++++++ apps/utils/models.py | 20 --------------- apps/utils/tests.py | 23 +++++++++++++++++ 4 files changed, 66 insertions(+), 21 deletions(-) create mode 100644 apps/utils/decorators.py diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 7199eb54..b4269c5f 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -7,6 +7,9 @@ from establishment.serializers import ( EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer, ContactPhonesSerializer, SocialNetworkRelatedSerializers, EstablishmentDetailSerializer ) + +from utils.decorators import with_base_attributes + from main.models import Currency @@ -121,7 +124,10 @@ class ContactEmailBackSerializers(PlateSerializer): ] +# TODO: test decorator +@with_base_attributes class EmployeeBackSerializers(serializers.ModelSerializer): + """Social network serializers.""" class Meta: model = models.Employee @@ -129,4 +135,5 @@ class EmployeeBackSerializers(serializers.ModelSerializer): 'id', 'user', 'name' - ] \ No newline at end of file + ] + diff --git a/apps/utils/decorators.py b/apps/utils/decorators.py new file mode 100644 index 00000000..12233021 --- /dev/null +++ b/apps/utils/decorators.py @@ -0,0 +1,35 @@ +from functools import wraps + +def with_base_attributes(cls): + + def create(self, validated_data): + user = None + request = self.context.get("request") + + if request and hasattr(request, "user"): + user = request.user + + if user is not None: + validated_data['created_by'] = user + validated_data['modified_by'] = user + + obj = self.Meta.model.objects.create(**validated_data) + return obj + + def update(self, validated_data): + user = None + request = self.context.get("request") + + if request and hasattr(request, "user"): + user = request.user + + if user is not None: + validated_data['modified_by'] = user + + obj = self.Meta.model.objects.create(**validated_data) + return obj + + setattr(cls, "create", create) + setattr(cls, "update", update) + + return cls diff --git a/apps/utils/models.py b/apps/utils/models.py index 929b5e5b..632cf4a2 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -11,9 +11,6 @@ from easy_thumbnails.fields import ThumbnailerImageField from utils.methods import image_path, svg_image_path from utils.validators import svg_image_validator -from django.db.models.signals import pre_save -from django.dispatch import receiver - class ProjectBaseMixin(models.Model): """Base mixin model.""" @@ -126,23 +123,6 @@ class BaseAttributes(ProjectBaseMixin): null=True, related_name='%(class)s_records_modified' ) - @receiver(pre_save) - def _pre_save(sender, instance, **kwargs): - if not issubclass(sender, BaseAttributes): - return - - # debug - from establishment.models import Employee - if not isinstance(instance, Employee): - return - - user = False - - instance.modified_by = user - - if instance._state.adding: - instance.created_by = user - class Meta: """Meta class.""" diff --git a/apps/utils/tests.py b/apps/utils/tests.py index 786ed940..dafe0fee 100644 --- a/apps/utils/tests.py +++ b/apps/utils/tests.py @@ -91,5 +91,28 @@ class BaseAttributeTests(BaseTestCase): self.assertIn("id", response_data) employee = Employee.objects.get(id=response_data['id']) + self.assertEqual(self.user, employee.created_by) self.assertEqual(self.user, employee.modified_by) + + modify_user = User.objects.create_user( + username='sedragurda', + password='sedragurdaredips19', + email='sedragurda@desoz.com', + ) + + modify_tokkens = User.create_jwt_tokens(modify_user) + self.client.cookies = SimpleCookie( + {'access_token': modify_tokkens.get('access_token'), + 'refresh_token': modify_tokkens.get('refresh_token'), + 'locale': "en" + }) + + update_data = { + 'name': 'Test new name' + } + + response = self.client.patch('/api/back/establishments/employees/1/', data=update_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + self.assertEqual(modify_user, employee.modified_by) From d006184b14f905c2d9ec8f772b9927db6ddaa298 Mon Sep 17 00:00:00 2001 From: littlewolf Date: Tue, 24 Sep 2019 20:22:27 +0300 Subject: [PATCH 12/36] Fix update --- apps/utils/decorators.py | 9 +++++---- apps/utils/tests.py | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/apps/utils/decorators.py b/apps/utils/decorators.py index 12233021..e22fc3dc 100644 --- a/apps/utils/decorators.py +++ b/apps/utils/decorators.py @@ -1,4 +1,3 @@ -from functools import wraps def with_base_attributes(cls): @@ -16,7 +15,7 @@ def with_base_attributes(cls): obj = self.Meta.model.objects.create(**validated_data) return obj - def update(self, validated_data): + def update(self, instance, validated_data): user = None request = self.context.get("request") @@ -26,8 +25,10 @@ def with_base_attributes(cls): if user is not None: validated_data['modified_by'] = user - obj = self.Meta.model.objects.create(**validated_data) - return obj + obj = self.Meta.model + obj.objects.filter(pk=instance.id).update(**validated_data) + + return instance setattr(cls, "create", create) setattr(cls, "update", update) diff --git a/apps/utils/tests.py b/apps/utils/tests.py index dafe0fee..8645a020 100644 --- a/apps/utils/tests.py +++ b/apps/utils/tests.py @@ -96,9 +96,9 @@ class BaseAttributeTests(BaseTestCase): self.assertEqual(self.user, employee.modified_by) modify_user = User.objects.create_user( - username='sedragurda', - password='sedragurdaredips19', - email='sedragurda@desoz.com', + username='sedragurda2', + password='sedragurdaredips192', + email='sedragurda2@desoz.com', ) modify_tokkens = User.create_jwt_tokens(modify_user) @@ -115,4 +115,5 @@ class BaseAttributeTests(BaseTestCase): response = self.client.patch('/api/back/establishments/employees/1/', data=update_data) self.assertEqual(response.status_code, status.HTTP_200_OK) + employee.refresh_from_db() self.assertEqual(modify_user, employee.modified_by) From d62cee341cddc5195f8aa97c43cbd254c942dde7 Mon Sep 17 00:00:00 2001 From: littlewolf Date: Tue, 24 Sep 2019 21:03:50 +0300 Subject: [PATCH 13/36] Add address tests --- apps/location/tests.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/apps/location/tests.py b/apps/location/tests.py index cab871a0..a3213e69 100644 --- a/apps/location/tests.py +++ b/apps/location/tests.py @@ -1,8 +1,11 @@ +import json + from rest_framework.test import APITestCase from account.models import User from rest_framework import status from http.cookies import SimpleCookie +from django.contrib.gis.db.models import PointField from location.models import City, Region, Country @@ -28,19 +31,18 @@ class AddressTests(BaseTestCase): def setUp(self): super().setUp() - - self.country = Country( - name="Test country", - code="+7" + self.country = Country.objects.create( + name=json.dumps({"en-GB": "Test country"}), + code="test" ) - self.region = Region( + self.region = Region.objects.create( name="Test region", code="812", country=self.country ) - self.city = City( + self.city = City.objects.create( name="Test region", code="812", region=self.region, @@ -52,18 +54,25 @@ class AddressTests(BaseTestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) data = { - 'user': self.user.id, - 'name': 'Test name' + 'city_id': self.city.id, + 'number': '+79999999', + "coordinates": { + "latitude": 37.0625, + "longitude": -95.677068 + }, + "geo_lon": -95.677068, + "geo_lat": 37.0625 } - response = self.client.post('/api/back/location/addresses/', data=data) + response = self.client.post('/api/back/location/addresses/', data=data, format='json') + print(f"=========RESPONSE: {response.json()}") self.assertEqual(response.status_code, status.HTTP_201_CREATED) response = self.client.get('/api/back/location/addresses/1/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) update_data = { - 'name': 'Test new name' + 'number': '+79999991' } response = self.client.patch('/api/back/location/addresses/1/', data=update_data) From 6b3558e3acf56ade095cd7989b072983c640162d Mon Sep 17 00:00:00 2001 From: littlewolf Date: Tue, 24 Sep 2019 21:28:11 +0300 Subject: [PATCH 14/36] Add country tests --- apps/location/tests.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/apps/location/tests.py b/apps/location/tests.py index a3213e69..0d7803bc 100644 --- a/apps/location/tests.py +++ b/apps/location/tests.py @@ -27,6 +27,35 @@ class BaseTestCase(APITestCase): 'refresh_token': tokkens.get('refresh_token')}) +class CountryTests(BaseTestCase): + + def test_country_CRUD(self): + response = self.client.get('/api/back/location/countries/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = { + 'name': 'Test country', + 'code': 'test' + } + + response = self.client.post('/api/back/location/countries/', data=data, format='json') + response_data = response.json() + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.get(f'/api/back/location/countries/{response_data["id"]}/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + update_data = { + 'name': json.dumps({"en-GB": "Test new country"}) + } + + response = self.client.patch(f'/api/back/location/countries/{response_data["id"]}/', data=update_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.delete(f'/api/back/location/countries/{response_data["id"]}/', format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + class AddressTests(BaseTestCase): def setUp(self): @@ -65,18 +94,18 @@ class AddressTests(BaseTestCase): } response = self.client.post('/api/back/location/addresses/', data=data, format='json') - print(f"=========RESPONSE: {response.json()}") + response_data = response.json() self.assertEqual(response.status_code, status.HTTP_201_CREATED) - response = self.client.get('/api/back/location/addresses/1/', format='json') + response = self.client.get(f'/api/back/location/addresses/{response_data["id"]}/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) update_data = { 'number': '+79999991' } - response = self.client.patch('/api/back/location/addresses/1/', data=update_data) + response = self.client.patch(f'/api/back/location/addresses/{response_data["id"]}/', data=update_data) self.assertEqual(response.status_code, status.HTTP_200_OK) - response = self.client.delete('/api/back/location/addresses/1/', format='json') + response = self.client.delete(f'/api/back/location/addresses/{response_data["id"]}/', format='json') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) From 7455d2c8564144b4a746b930042f3399ec3404b6 Mon Sep 17 00:00:00 2001 From: littlewolf Date: Tue, 24 Sep 2019 21:30:59 +0300 Subject: [PATCH 15/36] Add region tests --- apps/location/tests.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/apps/location/tests.py b/apps/location/tests.py index 0d7803bc..ceeb2043 100644 --- a/apps/location/tests.py +++ b/apps/location/tests.py @@ -56,6 +56,43 @@ class CountryTests(BaseTestCase): self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) +class RegionTests(BaseTestCase): + + def setUp(self): + super().setUp() + self.country = Country.objects.create( + name=json.dumps({"en-GB": "Test country"}), + code="test" + ) + + def test_region_CRUD(self): + response = self.client.get('/api/back/location/countries/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = { + 'name': 'Test country', + 'code': 'test', + 'country_id': self.country.id + } + + response = self.client.post('/api/back/location/regions/', data=data, format='json') + response_data = response.json() + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.get(f'/api/back/location/regions/{response_data["id"]}/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + update_data = { + 'name': json.dumps({"en-GB": "Test new country"}) + } + + response = self.client.patch(f'/api/back/location/regions/{response_data["id"]}/', data=update_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.delete(f'/api/back/location/regions/{response_data["id"]}/', format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + class AddressTests(BaseTestCase): def setUp(self): From c50bdc863745caf442a8a51f77e964a43be7bbf9 Mon Sep 17 00:00:00 2001 From: littlewolf Date: Tue, 24 Sep 2019 21:34:21 +0300 Subject: [PATCH 16/36] Add city tests --- apps/location/tests.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/apps/location/tests.py b/apps/location/tests.py index ceeb2043..d85db414 100644 --- a/apps/location/tests.py +++ b/apps/location/tests.py @@ -66,7 +66,7 @@ class RegionTests(BaseTestCase): ) def test_region_CRUD(self): - response = self.client.get('/api/back/location/countries/', format='json') + response = self.client.get('/api/back/location/regions/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) data = { @@ -93,6 +93,46 @@ class RegionTests(BaseTestCase): self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) +class CityTests(RegionTests): + + def setUp(self): + super().setUp() + + self.region = Region.objects.create( + name="Test region", + code="812", + country=self.country + ) + + def test_city_CRUD(self): + response = self.client.get('/api/back/location/cities/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + data = { + 'name': 'Test country', + 'code': 'test', + 'country_id': self.country.id, + 'region_id': self.region.id + } + + response = self.client.post('/api/back/location/cities/', data=data, format='json') + response_data = response.json() + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.get(f'/api/back/location/cities/{response_data["id"]}/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + update_data = { + 'name': json.dumps({"en-GB": "Test new country"}) + } + + response = self.client.patch(f'/api/back/location/cities/{response_data["id"]}/', data=update_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.delete(f'/api/back/location/cities/{response_data["id"]}/', format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + class AddressTests(BaseTestCase): def setUp(self): From 2261875d3c64c00d449bbe988e0f6ccf8a4309ee Mon Sep 17 00:00:00 2001 From: littlewolf Date: Tue, 24 Sep 2019 21:36:59 +0300 Subject: [PATCH 17/36] Fix tests --- apps/location/tests.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/location/tests.py b/apps/location/tests.py index d85db414..f68ba56b 100644 --- a/apps/location/tests.py +++ b/apps/location/tests.py @@ -5,7 +5,6 @@ from account.models import User from rest_framework import status from http.cookies import SimpleCookie -from django.contrib.gis.db.models import PointField from location.models import City, Region, Country @@ -93,11 +92,16 @@ class RegionTests(BaseTestCase): self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) -class CityTests(RegionTests): +class CityTests(BaseTestCase): def setUp(self): super().setUp() + self.country = Country.objects.create( + name=json.dumps({"en-GB": "Test country"}), + code="test" + ) + self.region = Region.objects.create( name="Test region", code="812", @@ -137,6 +141,7 @@ class AddressTests(BaseTestCase): def setUp(self): super().setUp() + self.country = Country.objects.create( name=json.dumps({"en-GB": "Test country"}), code="test" From 6bbbd1d6fe2414b8c5cabf9b84519c9da4722566 Mon Sep 17 00:00:00 2001 From: littlewolf Date: Wed, 25 Sep 2019 11:11:12 +0300 Subject: [PATCH 18/36] Switch decorator to validate --- apps/utils/decorators.py | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/apps/utils/decorators.py b/apps/utils/decorators.py index e22fc3dc..c48a26c7 100644 --- a/apps/utils/decorators.py +++ b/apps/utils/decorators.py @@ -1,7 +1,7 @@ def with_base_attributes(cls): - def create(self, validated_data): + def validate(self, data): user = None request = self.context.get("request") @@ -9,28 +9,13 @@ def with_base_attributes(cls): user = request.user if user is not None: - validated_data['created_by'] = user - validated_data['modified_by'] = user + data.update({'modified_by': user}) - obj = self.Meta.model.objects.create(**validated_data) - return obj + if not self.instance: + data.update({'created_by': user}) - def update(self, instance, validated_data): - user = None - request = self.context.get("request") + return data - if request and hasattr(request, "user"): - user = request.user - - if user is not None: - validated_data['modified_by'] = user - - obj = self.Meta.model - obj.objects.filter(pk=instance.id).update(**validated_data) - - return instance - - setattr(cls, "create", create) - setattr(cls, "update", update) + setattr(cls, "validate", validate) return cls From 617beb652f38659f7eebc4dc828a161bbbab5d9d Mon Sep 17 00:00:00 2001 From: littlewolf Date: Wed, 25 Sep 2019 11:12:17 +0300 Subject: [PATCH 19/36] Add test to modified item --- apps/utils/tests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/utils/tests.py b/apps/utils/tests.py index 8645a020..e9ad3c23 100644 --- a/apps/utils/tests.py +++ b/apps/utils/tests.py @@ -117,3 +117,4 @@ class BaseAttributeTests(BaseTestCase): employee.refresh_from_db() self.assertEqual(modify_user, employee.modified_by) + self.assertEqual(self.user, employee.created_by) From cf19904349bd31fd8dcd3c817f82ab2b3ee5fc42 Mon Sep 17 00:00:00 2001 From: michail Date: Wed, 25 Sep 2019 13:17:24 +0500 Subject: [PATCH 20/36] Fixed urls and tests for news --- apps/news/tests.py | 13 ++++++++++--- apps/news/urls/web.py | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/news/tests.py b/apps/news/tests.py index 9ac8742c..7d6724d7 100644 --- a/apps/news/tests.py +++ b/apps/news/tests.py @@ -35,11 +35,18 @@ class NewsTestCase(BaseTestCase): response = self.client.get("/api/web/news/") self.assertEqual(response.status_code, status.HTTP_200_OK) - def test_news_detail(self): - response = self.client.get(f"/api/web/news/{self.test_news.slug}/") + def test_news_web_detail(self): + response = self.client.get(f"/api/web/news/slug/{self.test_news.slug}/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_news_back_detail(self): + response = self.client.get(f"/api/back/news/{self.test_news.id}/") + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_news_list_back(self): + response = self.client.get("/api/back/news/") self.assertEqual(response.status_code, status.HTTP_200_OK) def test_news_type_list(self): response = self.client.get("/api/web/news/types/") self.assertEqual(response.status_code, status.HTTP_200_OK) - diff --git a/apps/news/urls/web.py b/apps/news/urls/web.py index 10513910..80fcf072 100644 --- a/apps/news/urls/web.py +++ b/apps/news/urls/web.py @@ -7,5 +7,5 @@ app_name = 'news' urlpatterns = [ path('', views.NewsListView.as_view(), name='list'), path('types/', views.NewsTypeListView.as_view(), name='type'), - path('/', views.NewsDetailView.as_view(), name='rud'), + path('slug//', views.NewsDetailView.as_view(), name='rud'), ] From 0e643a1dfa69f2cc4c7fbc8fac70a8811fc7f478 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 25 Sep 2019 12:23:25 +0300 Subject: [PATCH 21/36] Merge branch 'develop' of /home/a.feteleu/projects/gm-backend with conflicts. --- apps/establishment/models.py | 77 ++++++------------------------------ apps/gallery/views.py | 2 - 2 files changed, 12 insertions(+), 67 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index e3384dfb..4731a212 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -101,70 +101,16 @@ class EstablishmentQuerySet(models.QuerySet): DistanceMeasure(Distance('address__coordinates', point, srid=4236)).m, output_field=models.FloatField())) - def annotate_distance_mark(self): + def annotate_mark_similarity(self, mark): """ - Return QuerySet with annotated field - distance_mark. - Required fields: distance. + Return a QuerySet with annotated field - mark_similarity Description: - If the radius of the establishments in QuerySet does not exceed 500 meters, - then distance_mark is set to 0.6, otherwise 0. + Similarity mark determined by comparison with compared establishment mark """ - return self.annotate(distance_mark=models.Case( - models.When(distance__lte=500, - then=0.6), - default=0, - output_field=models.FloatField())) - - def annotate_intermediate_public_mark(self): - """ - Return QuerySet with annotated field - intermediate_public_mark. - Description: - If establishments in collection POP and its mark is null, then - intermediate_mark is set to 10; - """ - return self.annotate(intermediate_public_mark=models.Case( - models.When( - collections__collection_type=Collection.POP, - public_mark__isnull=True, - then=10 - ), - default='public_mark', - output_field=models.FloatField())) - - def annotate_additional_mark(self, public_mark: float): - """ - Return QuerySet with annotated field - additional_mark. - Required fields: intermediate_public_mark - Description: - IF - establishments public_mark + 3 > compared establishment public_mark - OR - establishments public_mark - 3 > compared establishment public_mark, - THEN - additional_mark is set to 0.4, - ELSE - set to 0. - """ - return self.annotate(additional_mark=models.Case( - models.When( - models.Q(intermediate_public_mark__lte=public_mark + 3) | - models.Q(intermediate_public_mark__lte=public_mark - 3), - then=0.4), - default=0, - output_field=models.FloatField())) - - def annotate_total_mark(self): - """ - Return QuerySet with annotated field - total_mark. - Required fields: distance_mark, additional_mark. - Fields - Description: - Annotated field is obtained by formula: - (distance + additional marks) * intermediate_public_mark. - """ - return self.annotate( - total_mark=(models.F('distance_mark') + models.F('additional_mark')) * - models.F('intermediate_public_mark')) + return self.annotate(mark_similarity=models.ExpressionWrapper( + mark - models.F('mark'), + output_field=models.FloatField() + )) def similar(self, establishment_slug: str): """ @@ -181,10 +127,11 @@ class EstablishmentQuerySet(models.QuerySet): reviews__status=Review.READY, public_mark__gte=10) \ .annotate_distance(point=establishment.address.coordinates) \ - .annotate_distance_mark() \ - .annotate_intermediate_public_mark() \ - .annotate_additional_mark(public_mark=establishment.public_mark) \ - .annotate_total_mark() + .annotate_mark_similarity(establishment_qs.first().public_mark) + # .annotate_distance_mark() \ + # .annotate_intermediate_public_mark() \ + # .annotate_additional_mark(public_mark=establishment.public_mark) \ + # .annotate_total_mark() else: return self.none() diff --git a/apps/gallery/views.py b/apps/gallery/views.py index 109a01ef..8a9195c3 100644 --- a/apps/gallery/views.py +++ b/apps/gallery/views.py @@ -1,6 +1,5 @@ from rest_framework import generics -from utils.permissions import IsAuthenticatedAndTokenIsValid from . import models, serializers @@ -9,4 +8,3 @@ class ImageUploadView(generics.CreateAPIView): model = models.Image queryset = models.Image.objects.all() serializer_class = serializers.ImageSerializer - permission_classes = (IsAuthenticatedAndTokenIsValid, ) From 90c9e321f7a627512aadf1274ac537c2bd767fd8 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 25 Sep 2019 13:05:07 +0300 Subject: [PATCH 22/36] fixed similar establishments --- apps/establishment/models.py | 30 ++++++++++++++++++++++-------- apps/establishment/views/web.py | 7 +++++-- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 4731a212..2fbff239 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -101,6 +101,22 @@ class EstablishmentQuerySet(models.QuerySet): DistanceMeasure(Distance('address__coordinates', point, srid=4236)).m, output_field=models.FloatField())) + def annotate_intermediate_public_mark(self): + """ + Return QuerySet with annotated field - intermediate_public_mark. + Description: + If establishments in collection POP and its mark is null, then + intermediate_mark is set to 10; + """ + return self.annotate(intermediate_public_mark=models.Case( + models.When( + collections__collection_type=Collection.POP, + public_mark__isnull=True, + then=10 + ), + default='public_mark', + output_field=models.FloatField())) + def annotate_mark_similarity(self, mark): """ Return a QuerySet with annotated field - mark_similarity @@ -108,7 +124,7 @@ class EstablishmentQuerySet(models.QuerySet): Similarity mark determined by comparison with compared establishment mark """ return self.annotate(mark_similarity=models.ExpressionWrapper( - mark - models.F('mark'), + mark - models.F('intermediate_public_mark'), output_field=models.FloatField() )) @@ -117,7 +133,8 @@ class EstablishmentQuerySet(models.QuerySet): Return QuerySet with objects that similar to Establishment. :param establishment_slug: str Establishment slug """ - establishment_qs = Establishment.objects.filter(slug=establishment_slug) + establishment_qs = Establishment.objects.filter(slug=establishment_slug, + public_mark__isnull=False) if establishment_qs.exists(): establishment = establishment_qs.first() return self.exclude(slug=establishment_slug) \ @@ -127,11 +144,8 @@ class EstablishmentQuerySet(models.QuerySet): reviews__status=Review.READY, public_mark__gte=10) \ .annotate_distance(point=establishment.address.coordinates) \ - .annotate_mark_similarity(establishment_qs.first().public_mark) - # .annotate_distance_mark() \ - # .annotate_intermediate_public_mark() \ - # .annotate_additional_mark(public_mark=establishment.public_mark) \ - # .annotate_total_mark() + .annotate_intermediate_public_mark() \ + .annotate_mark_similarity(mark=establishment.public_mark) else: return self.none() @@ -225,7 +239,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): preview_image_url = models.URLField(verbose_name=_('Preview image URL path'), blank=True, null=True, default=None) slug = models.SlugField(unique=True, max_length=50, null=True, - verbose_name=_('Establishment slug'), editable=True) + verbose_name=_('Establishment slug'), editable=True) awards = generic.GenericRelation(to='main.Award') tags = generic.GenericRelation(to='main.MetaDataContent') diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index ce2bed05..fef7fd5b 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -30,8 +30,11 @@ class EstablishmentSimilarListView(EstablishmentListView): def get_queryset(self): """Override get_queryset method""" - return super().get_queryset().similar(establishment_slug=self.kwargs.get('slug'))\ - .order_by('-total_mark')[:13] + number_objects = 12 # Count of similar objects + return super().get_queryset().similar(establishment_slug=self.kwargs.get('slug')) \ + .order_by('distance')[:number_objects * 3] \ + .order_by('mark_similarity')[:number_objects] + class EstablishmentRetrieveView(EstablishmentMixin, generics.RetrieveAPIView): """Resource for getting a establishment.""" From 8766388256438b25cbec076ddbd8a296a9df481a Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 25 Sep 2019 13:16:02 +0300 Subject: [PATCH 23/36] fixed similar establishments --- apps/establishment/models.py | 7 +++++-- apps/establishment/views/web.py | 5 +---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 2fbff239..5391d066 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -128,10 +128,11 @@ class EstablishmentQuerySet(models.QuerySet): output_field=models.FloatField() )) - def similar(self, establishment_slug: str): + def similar(self, establishment_slug: str, output_objects: int = 12): """ Return QuerySet with objects that similar to Establishment. :param establishment_slug: str Establishment slug + :param output_objects: int of output objects """ establishment_qs = Establishment.objects.filter(slug=establishment_slug, public_mark__isnull=False) @@ -145,7 +146,9 @@ class EstablishmentQuerySet(models.QuerySet): public_mark__gte=10) \ .annotate_distance(point=establishment.address.coordinates) \ .annotate_intermediate_public_mark() \ - .annotate_mark_similarity(mark=establishment.public_mark) + .annotate_mark_similarity(mark=establishment.public_mark) \ + .order_by('distance') \ + .order_by('mark_similarity')[:output_objects] else: return self.none() diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index fef7fd5b..da10300f 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -30,10 +30,7 @@ class EstablishmentSimilarListView(EstablishmentListView): def get_queryset(self): """Override get_queryset method""" - number_objects = 12 # Count of similar objects - return super().get_queryset().similar(establishment_slug=self.kwargs.get('slug')) \ - .order_by('distance')[:number_objects * 3] \ - .order_by('mark_similarity')[:number_objects] + return super().get_queryset().similar(establishment_slug=self.kwargs.get('slug')) class EstablishmentRetrieveView(EstablishmentMixin, generics.RetrieveAPIView): From a2ade761b463454ec2c7bfc3ace7a2f93c6f35bf Mon Sep 17 00:00:00 2001 From: littlewolf Date: Wed, 25 Sep 2019 14:04:52 +0300 Subject: [PATCH 24/36] Update web urls --- apps/establishment/urls/common.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index bd53c7eb..99027d12 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -8,13 +8,13 @@ app_name = 'establishment' urlpatterns = [ path('', views.EstablishmentListView.as_view(), name='list'), path('tags/', views.EstablishmentTagListView.as_view(), name='tags'), - path('/', views.EstablishmentRetrieveView.as_view(), name='detail'), - path('/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'), - path('/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'), - path('/comments/create/', views.EstablishmentCommentCreateView.as_view(), + 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'), + path('slug//comments/create/', views.EstablishmentCommentCreateView.as_view(), name='create-comment'), - path('/comments//', views.EstablishmentCommentRUDView.as_view(), + path('slug//comments//', views.EstablishmentCommentRUDView.as_view(), name='rud-comment'), - path('/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), + path('slug//favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), name='add-to-favorites') ] From f38924db42b8cfe2ddda3a6d40332c7773f1c76b Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 25 Sep 2019 14:09:24 +0300 Subject: [PATCH 25/36] Fix favorites creation by slug --- apps/establishment/serializers/common.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 7b974887..691f3b43 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -315,20 +315,22 @@ class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer): def validate(self, attrs): """Override validate method""" # Check establishment object - establishment_id = self.context.get('request').parser_context.get('kwargs').get('pk') - establishment_qs = models.Establishment.objects.filter(id=establishment_id) + establishment_slug = self.context.get('request').parser_context.get('kwargs').get('slug') + establishment_qs = models.Establishment.objects.filter(slug=establishment_slug) - # Check establishment obj by pk from lookup_kwarg + # Check establishment obj by slug from lookup_kwarg if not establishment_qs.exists(): raise serializers.ValidationError({'detail': _('Object not found.')}) + else: + 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(): + .by_object_id(object_id=establishment.id).exists(): raise utils_exceptions.FavoritesError() - attrs['establishment'] = establishment_qs.first() + attrs['establishment'] = establishment return attrs def create(self, validated_data, *args, **kwargs): From 07a0f8e295e37d954f069f5a1ab1fce5565ee42f Mon Sep 17 00:00:00 2001 From: littlewolf Date: Wed, 25 Sep 2019 17:34:11 +0300 Subject: [PATCH 26/36] Temp commit --- apps/establishment/tests.py | 54 +++++++++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index e4e3b02c..3b5a44dc 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -27,7 +27,7 @@ class BaseTestCase(APITestCase): self.establishment_type = EstablishmentType.objects.create(name="Test establishment type") -class EstablishmentTests(BaseTestCase): +class EstablishmentBTests(BaseTestCase): def test_establishment_CRUD(self): params = {'page': 1, 'page_size': 1,} response = self.client.get('/api/back/establishments/', params, format='json') @@ -93,7 +93,8 @@ class ChildTestCase(BaseTestCase): self.establishment = Establishment.objects.create( name="Test establishment", establishment_type_id=self.establishment_type.id, - is_publish=True + is_publish=True, + slug="test" ) @@ -263,3 +264,52 @@ class EstablishmentShedulerTests(ChildTestCase): response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/schedule/1/') self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + +# Web tests +class EstablishmentWebTests(BaseTestCase): + + def test_establishment_Read(self): + params = {'page': 1, 'page_size': 1,} + response = self.client.get('/api/web/establishments/', params, format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class EstablishmentWebTagTests(BaseTestCase): + + def test_tag_Read(self): + response = self.client.get('/api/web/establishments/tags/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class EstablishmentWebSlugTests(ChildTestCase): + + def test_slug_Read(self): + response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class EstablishmentWebSimilarTests(ChildTestCase): + + def test_similar_Read(self): + response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/similar/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class EstablishmentWebCommentsTests(ChildTestCase): + + def test_comments_Read(self): + response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/comments/', format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + + + + + + +# class EstablishmentWebFavoriteTests(ChildTestCase): +# +# def test_comments_Read(self): +# response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/favorites/', format='json') +# self.assertEqual(response.status_code, status.HTTP_200_OK) From cabee12fab98e3ebb22d2c0e452908a7d554f1fa Mon Sep 17 00:00:00 2001 From: littlewolf Date: Wed, 25 Sep 2019 18:58:52 +0300 Subject: [PATCH 27/36] Add comment tests Add favorite tests --- apps/establishment/tests.py | 51 ++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index 3b5a44dc..a001b055 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -298,18 +298,55 @@ class EstablishmentWebSimilarTests(ChildTestCase): class EstablishmentWebCommentsTests(ChildTestCase): - def test_comments_Read(self): + def test_comments_CRUD(self): + response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/comments/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) + data = { + 'text': 'test', + 'user': self.user.id, + 'mark': 4 + } + + response = self.client.post(f'/api/web/establishments/slug/{self.establishment.slug}/comments/create/', + data=data) + + comment = response.json() + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/comments/{comment["id"]}/', + format='json') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + update_data = { + 'text': 'Test new establishment' + } + + response = self.client.patch(f'/api/web/establishments/slug/{self.establishment.slug}/comments/{comment["id"]}/', + data=update_data) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + response = self.client.delete( + f'/api/web/establishments/slug/{self.establishment.slug}/comments/{comment["id"]}/', + format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) +class EstablishmentWebFavoriteTests(ChildTestCase): + def test_favorite_cd(self): + data = { + "user": self.user.id, + "object_id": self.establishment.id + } + response = self.client.post(f'/api/web/establishments/slug/{self.establishment.slug}/favorites/', + data=data) + print(f"================================RESPONSE: {response.json()}") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) - -# class EstablishmentWebFavoriteTests(ChildTestCase): -# -# def test_comments_Read(self): -# response = self.client.get(f'/api/web/establishments/slug/{self.establishment.slug}/favorites/', format='json') -# self.assertEqual(response.status_code, status.HTTP_200_OK) + response = self.client.delete( + f'/api/web/establishments/slug/{self.establishment.slug}/favorites/', + format='json') + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) From d8dd1fd7d669a09b93b70ec4cbf2f372fee02677 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 25 Sep 2019 19:00:10 +0300 Subject: [PATCH 28/36] Fix carousel field types --- apps/main/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/main/serializers.py b/apps/main/serializers.py index 10d75f2b..c981f0ee 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -136,8 +136,8 @@ class CarouselListSerializer(serializers.ModelSerializer): """Serializer for retrieving list of carousel items.""" model_name = serializers.CharField() name = serializers.CharField() - toque_number = serializers.CharField() - public_mark = serializers.CharField() + toque_number = serializers.IntegerField() + public_mark = serializers.IntegerField() image = serializers.URLField(source='image_url') awards = AwardBaseSerializer(many=True) vintage_year = serializers.IntegerField() From 207e35feb143ea38afbaffb2a52abe16a11d38c1 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Wed, 25 Sep 2019 19:39:37 +0300 Subject: [PATCH 29/36] GM-127: in progress --- apps/establishment/models.py | 37 +++++++++++++----------- apps/establishment/serializers/common.py | 19 ++---------- apps/establishment/views/web.py | 10 ++++++- apps/review/models.py | 4 +++ apps/utils/pagination.py | 7 +++++ project/settings/base.py | 2 ++ 6 files changed, 44 insertions(+), 35 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 5391d066..666b7194 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -1,18 +1,20 @@ """Establishment models.""" from functools import reduce -from django.contrib.gis.db.models.functions import Distance -from django.contrib.gis.measure import Distance as DistanceMeasure -from django.contrib.gis.geos import Point +from django.conf import settings from django.contrib.contenttypes import fields as generic +from django.contrib.gis.db.models.functions import Distance +from django.contrib.gis.geos import Point +from django.contrib.gis.measure import Distance as DistanceMeasure from django.core.exceptions import ValidationError from django.db import models +from django.db.models import When, Case, F, ExpressionWrapper, Subquery from django.utils import timezone from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField -from location.models import Address from collection.models import Collection +from location.models import Address from review.models import Review from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField, URLImageMixin, TranslatedFieldsMixin, BaseAttributes) @@ -128,27 +130,28 @@ class EstablishmentQuerySet(models.QuerySet): output_field=models.FloatField() )) - def similar(self, establishment_slug: str, output_objects: int = 12): + def similar(self, establishment_slug: str): """ Return QuerySet with objects that similar to Establishment. :param establishment_slug: str Establishment slug - :param output_objects: int of output objects """ establishment_qs = Establishment.objects.filter(slug=establishment_slug, public_mark__isnull=False) if establishment_qs.exists(): establishment = establishment_qs.first() - return self.exclude(slug=establishment_slug) \ - .filter(is_publish=True, - image_url__isnull=False, - reviews__isnull=False, - reviews__status=Review.READY, - public_mark__gte=10) \ - .annotate_distance(point=establishment.address.coordinates) \ - .annotate_intermediate_public_mark() \ - .annotate_mark_similarity(mark=establishment.public_mark) \ - .order_by('distance') \ - .order_by('mark_similarity')[:output_objects] + subquery_filter_by_distance = Subquery( + self.exclude(slug=establishment_slug) + .filter(image_url__isnull=False, public_mark__gte=10) + .published() + .has_published_reviews() + .annotate_distance(point=establishment.address.coordinates) + .order_by('distance')[:settings.LIMITING_QUERY_NUMBER] + .values('id') + ) + return Establishment.objects.filter(id__in=subquery_filter_by_distance) \ + .annotate_intermediate_public_mark() \ + .annotate_mark_similarity(mark=establishment.public_mark) \ + .order_by('mark_similarity') else: return self.none() diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 7b974887..1f001886 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -191,7 +191,7 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): schedule = ScheduleRUDSerializer(many=True, allow_null=True) phones = ContactPhonesSerializer(read_only=True, many=True, ) emails = ContactEmailsSerializer(read_only=True, many=True, ) - review = serializers.SerializerMethodField() + review = ReviewSerializer(source='last_published_review', allow_null=True) employees = EstablishmentEmployeeSerializer(source='actual_establishment_employees', many=True) menu = MenuSerializers(source='menu_set', many=True, read_only=True) @@ -201,7 +201,7 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): slug = serializers.SlugField(required=True, allow_blank=False, max_length=50) - in_favorites = serializers.SerializerMethodField() + in_favorites = serializers.BooleanField() image = serializers.URLField(source='image_url') @@ -231,21 +231,6 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): 'slug', ] - def get_review(self, obj): - """Serializer method for getting last published review""" - return ReviewSerializer(obj.reviews.by_status(status=review_models.Review.READY) - .order_by('-published_at').first()).data - - def get_in_favorites(self, obj): - """Get in_favorites status flag""" - user = self.context.get('request').user - if user.is_authenticated: - return obj.id in user.favorites.by_content_type(app_label='establishment', - model='establishment')\ - .values_list('object_id', flat=True) - else: - return False - class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer): """Create comment serializer""" diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index da10300f..5a69bd87 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -9,6 +9,7 @@ from establishment import filters from establishment import models, serializers from establishment.views import EstablishmentMixin from main.models import MetaDataContent +from utils.pagination import EstablishmentPagination from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer @@ -27,10 +28,12 @@ class EstablishmentListView(EstablishmentMixin, generics.ListAPIView): class EstablishmentSimilarListView(EstablishmentListView): """Resource for getting a list of establishments.""" serializer_class = serializers.EstablishmentListSerializer + pagination_class = EstablishmentPagination def get_queryset(self): """Override get_queryset method""" - return super().get_queryset().similar(establishment_slug=self.kwargs.get('slug')) + return super().get_queryset() \ + .similar(establishment_slug=self.kwargs.get('slug')) class EstablishmentRetrieveView(EstablishmentMixin, generics.RetrieveAPIView): @@ -38,6 +41,11 @@ class EstablishmentRetrieveView(EstablishmentMixin, generics.RetrieveAPIView): lookup_field = 'slug' serializer_class = serializers.EstablishmentDetailSerializer + def get_queryset(self): + """Override 'get_queryset' method.""" + return super(EstablishmentRetrieveView, self).get_queryset() \ + .annotate_in_favorites(self.request.user) + class EstablishmentTypeListView(generics.ListAPIView): """Resource for getting a list of establishment types.""" diff --git a/apps/review/models.py b/apps/review/models.py index d0076f68..9d3a39c4 100644 --- a/apps/review/models.py +++ b/apps/review/models.py @@ -23,6 +23,10 @@ class ReviewQuerySet(models.QuerySet): """Filter by status""" return self.filter(status=status) + def published(self): + """Return published reviews""" + return self.filter(status=Review.READY) + class Review(BaseAttributes, TranslatedFieldsMixin): """Review model""" diff --git a/apps/utils/pagination.py b/apps/utils/pagination.py index 85bd293c..efaf329e 100644 --- a/apps/utils/pagination.py +++ b/apps/utils/pagination.py @@ -44,3 +44,10 @@ class ProjectMobilePagination(ProjectPageNumberPagination): if not self.page.has_previous(): return None return self.page.previous_page_number() + + +class EstablishmentPagination(ProjectMobilePagination): + """ + Pagination for app establishments with limit page size equal to 12 + """ + page_size = 12 diff --git a/project/settings/base.py b/project/settings/base.py index 4aec6f4c..2007d908 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -412,3 +412,5 @@ SOLO_CACHE_TIMEOUT = 300 SITE_REDIRECT_URL_UNSUBSCRIBE = '/unsubscribe/' SITE_NAME = 'Gault & Millau' + +LIMITING_QUERY_NUMBER = 36 # Need to restrict objects to sort (used in establishments) From 877d3d9e6a4233cdfa8e10b146ec0b41a74c9439 Mon Sep 17 00:00:00 2001 From: littlewolf Date: Wed, 25 Sep 2019 19:44:33 +0300 Subject: [PATCH 30/36] Fix test Add todo to fix error --- apps/establishment/models.py | 3 +++ apps/establishment/tests.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index e3384dfb..dc596600 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -174,6 +174,9 @@ class EstablishmentQuerySet(models.QuerySet): establishment_qs = Establishment.objects.filter(slug=establishment_slug) if establishment_qs.exists(): establishment = establishment_qs.first() + + # TODO fix error: + # AttributeError: 'NoneType' object has no attribute 'coordinates' return self.exclude(slug=establishment_slug) \ .filter(is_publish=True, image_url__isnull=False, diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index a001b055..f80cb448 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -343,7 +343,7 @@ class EstablishmentWebFavoriteTests(ChildTestCase): response = self.client.post(f'/api/web/establishments/slug/{self.establishment.slug}/favorites/', data=data) - print(f"================================RESPONSE: {response.json()}") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) response = self.client.delete( From c2bd4b7da83a17a9668c5bf8311c7e9b98d36a1b Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Wed, 25 Sep 2019 19:58:25 +0300 Subject: [PATCH 31/36] add slug && preview_image_url to search results --- apps/search_indexes/documents/establishment.py | 2 ++ apps/search_indexes/serializers.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/apps/search_indexes/documents/establishment.py b/apps/search_indexes/documents/establishment.py index 10f95d6c..16321723 100644 --- a/apps/search_indexes/documents/establishment.py +++ b/apps/search_indexes/documents/establishment.py @@ -84,6 +84,8 @@ class EstablishmentDocument(Document): 'name', 'toque_number', 'price_level', + 'preview_image_url', + 'slug', ) def get_queryset(self): diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index e6950bdd..480f509d 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -63,6 +63,8 @@ class EstablishmentDocumentSerializer(DocumentSerializer): 'collections', 'establishment_type', 'establishment_subtypes', + 'preview_image_url', + 'slug', ) @staticmethod From 3279fce33917d50b4f27f5ce365f187d66fef6a3 Mon Sep 17 00:00:00 2001 From: littlewolf Date: Thu, 26 Sep 2019 11:19:06 +0300 Subject: [PATCH 32/36] Update tests --- apps/establishment/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index f80cb448..90a8f80c 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -335,7 +335,7 @@ class EstablishmentWebCommentsTests(ChildTestCase): class EstablishmentWebFavoriteTests(ChildTestCase): - def test_favorite_cd(self): + def test_favorite_CR(self): data = { "user": self.user.id, "object_id": self.establishment.id From 86d754f4bb9ea85a75e17637f5be64775290b427 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, 26 Sep 2019 11:38:37 +0300 Subject: [PATCH 33/36] validate json translated field --- apps/establishment/serializers/back.py | 3 +- apps/establishment/serializers/common.py | 8 ++--- apps/establishment/tests.py | 1 - apps/utils/models.py | 16 +++++++--- apps/utils/serializers.py | 25 ++++++++++++++++ apps/utils/tests.py | 38 ++++++++++++++++++++++++ 6 files changed, 81 insertions(+), 10 deletions(-) diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 7013fe6a..95491bec 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -8,6 +8,7 @@ from establishment.serializers import ( ContactPhonesSerializer, SocialNetworkRelatedSerializers, EstablishmentDetailSerializer ) from main.models import Currency +from utils.serializers import TJSONSerializer class EstablishmentListCreateSerializer(EstablishmentBaseSerializer): @@ -86,7 +87,7 @@ class SocialNetworkSerializers(serializers.ModelSerializer): class PlatesSerializers(PlateSerializer): """Social network serializers.""" - name = serializers.JSONField() + name = TJSONSerializer currency_id = serializers.PrimaryKeyRelatedField( source='currency', queryset=Currency.objects.all(), write_only=True diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 7b974887..d736ee99 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -12,7 +12,7 @@ from review import models as review_models from timetable.serialziers import ScheduleRUDSerializer from utils import exceptions as utils_exceptions from django.utils.translation import gettext_lazy as _ - +from utils.serializers import TJSONSerializer class ContactPhonesSerializer(serializers.ModelSerializer): """Contact phone serializer""" @@ -60,7 +60,7 @@ class PlateSerializer(serializers.ModelSerializer): class MenuSerializers(serializers.ModelSerializer): plates = PlateSerializer(read_only=True, many=True, source='plate_set') - category = serializers.JSONField() + category = TJSONSerializer() category_translated = serializers.CharField(read_only=True) class Meta: @@ -74,9 +74,9 @@ class MenuSerializers(serializers.ModelSerializer): ] -class MenuRUDSerializers(serializers.ModelSerializer): +class MenuRUDSerializers(serializers.ModelSerializer, ): plates = PlateSerializer(read_only=True, many=True, source='plate_set') - category = serializers.JSONField() + category = TJSONSerializer() class Meta: model = models.Menu diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index e4e3b02c..1f3151fd 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -5,7 +5,6 @@ from rest_framework import status from http.cookies import SimpleCookie from main.models import Currency from establishment.models import Establishment, EstablishmentType, Menu - # Create your tests here. diff --git a/apps/utils/models.py b/apps/utils/models.py index 632cf4a2..4e6df35e 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -10,6 +10,8 @@ from django.utils.translation import ugettext_lazy as _, get_language from easy_thumbnails.fields import ThumbnailerImageField from utils.methods import image_path, svg_image_path from utils.validators import svg_image_validator +from django.db.models.fields import Field +from django.core import exceptions class ProjectBaseMixin(models.Model): @@ -26,6 +28,10 @@ class ProjectBaseMixin(models.Model): abstract = True +def valid(value): + print("Run") + + class TJSONField(JSONField): """Overrided JsonField.""" @@ -52,6 +58,7 @@ def translate_field(self, field_name): if isinstance(field, dict): return field.get(to_locale(get_language())) return None + return translate @@ -70,6 +77,7 @@ def index_field(self, field_name): for key, value in field.items(): setattr(obj, key, value) return obj + return index @@ -236,7 +244,8 @@ class LocaleManagerMixin(models.Manager): queryset = self.filter(**filters) # Prepare field for annotator - localized_fields = {f'{field}_{prefix}': KeyTextTransform(f'{locale}', field) for field in fields} + localized_fields = {f'{field}_{prefix}': KeyTextTransform(f'{locale}', field) for field in + fields} # Annotate them for _ in fields: @@ -245,7 +254,6 @@ class LocaleManagerMixin(models.Manager): class GMTokenGenerator(PasswordResetTokenGenerator): - CHANGE_EMAIL = 0 RESET_PASSWORD = 1 CHANGE_PASSWORD = 2 @@ -268,10 +276,10 @@ class GMTokenGenerator(PasswordResetTokenGenerator): """ fields = [str(timestamp), str(user.is_active), str(user.pk)] if self.purpose == self.CHANGE_EMAIL or \ - self.purpose == self.CONFIRM_EMAIL: + self.purpose == self.CONFIRM_EMAIL: fields.extend([str(user.email_confirmed), str(user.email)]) elif self.purpose == self.RESET_PASSWORD or \ - self.purpose == self.CHANGE_PASSWORD: + self.purpose == self.CHANGE_PASSWORD: fields.append(str(user.password)) return fields diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index 1f4c6f96..d30a046c 100644 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -1,6 +1,8 @@ """Utils app serializer.""" from rest_framework import serializers from utils.models import PlatformMixin +from django.core import exceptions +from translation.models import Language class EmptySerializer(serializers.Serializer): @@ -21,3 +23,26 @@ class TranslatedField(serializers.CharField): **kwargs): super().__init__(allow_null=allow_null, required=required, read_only=read_only, **kwargs) + + +def validate_tjson(value): + + if not isinstance(value, dict): + raise exceptions.ValidationError( + 'invalid_json', + code='invalid_json', + params={'value': value}, + ) + + lang_count = Language.objects.filter(locale__in=value.keys()).count() + + if lang_count == 0: + raise exceptions.ValidationError( + 'invalid_translated_keys', + code='invalid_translated_keys', + params={'value': value}, + ) + + +class TJSONSerializer(serializers.JSONField): + validators = [validate_tjson] diff --git a/apps/utils/tests.py b/apps/utils/tests.py index 1c9fa71d..81fa02a3 100644 --- a/apps/utils/tests.py +++ b/apps/utils/tests.py @@ -8,6 +8,11 @@ from http.cookies import SimpleCookie from account.models import User from news.models import News, NewsType +from django.test import TestCase +from translation.models import Language +from django.core import exceptions + +from .serializers import validate_tjson class BaseTestCase(APITestCase): @@ -62,3 +67,36 @@ class TranslateFieldReview(BaseTestCase): self.assertIn("title_translated", news_data) self.assertEqual(news_data['title_translated'], "Test news item") + + +class ValidJSONTest(TestCase): + + def test_valid_json(self): + lang = Language.objects.create(title='English', locale='en-GB') + lang.save() + + data = 'str' + + with self.assertRaises(exceptions.ValidationError) as err: + validate_tjson(data) + + self.assertEqual(err.exception.code, 'invalid_json') + + data = { + "string": "value" + } + + with self.assertRaises(exceptions.ValidationError) as err: + validate_tjson(data) + + self.assertEqual(err.exception.code, 'invalid_translated_keys') + + data = { + "en-GB": "English" + } + + try: + validate_tjson(data) + self.assertTrue(True) + except exceptions.ValidationError: + self.assert_(False, "Test json translated FAILED") From 7c0e5057983f5b118723ac8ec810b3b9decb7c9d 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, 26 Sep 2019 11:48:05 +0300 Subject: [PATCH 34/36] commit changes --- apps/utils/{!tests.py => tests.py} | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename apps/utils/{!tests.py => tests.py} (98%) diff --git a/apps/utils/!tests.py b/apps/utils/tests.py similarity index 98% rename from apps/utils/!tests.py rename to apps/utils/tests.py index 13058144..0ca77b6b 100644 --- a/apps/utils/!tests.py +++ b/apps/utils/tests.py @@ -8,11 +8,9 @@ from http.cookies import SimpleCookie from account.models import User from news.models import News, NewsType - from django.test import TestCase from translation.models import Language from django.core import exceptions - from .serializers import validate_tjson from establishment.models import Establishment, EstablishmentType, Employee @@ -126,6 +124,7 @@ class BaseAttributeTests(BaseTestCase): self.assertEqual(modify_user, employee.modified_by) self.assertEqual(self.user, employee.created_by) + class ValidJSONTest(TestCase): def test_valid_json(self): From 0e0d6e318e1593716203273a559d1529b8fa5749 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 26 Sep 2019 14:12:49 +0300 Subject: [PATCH 35/36] GM-127: finished --- apps/establishment/models.py | 19 +++++++++---------- apps/establishment/views/web.py | 10 ++++++---- apps/main/methods.py | 16 +++++++++++++++- apps/utils/middleware.py | 11 ----------- apps/utils/pagination.py | 6 ++++-- project/settings/base.py | 11 ++++++++++- 6 files changed, 44 insertions(+), 29 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 666b7194..aed78d9f 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -114,7 +114,7 @@ class EstablishmentQuerySet(models.QuerySet): models.When( collections__collection_type=Collection.POP, public_mark__isnull=True, - then=10 + then=settings.DEFAULT_ESTABLISHMENT_PUBLIC_MARK ), default='public_mark', output_field=models.FloatField())) @@ -135,23 +135,22 @@ class EstablishmentQuerySet(models.QuerySet): Return QuerySet with objects that similar to Establishment. :param establishment_slug: str Establishment slug """ - establishment_qs = Establishment.objects.filter(slug=establishment_slug, - public_mark__isnull=False) + establishment_qs = self.filter(slug=establishment_slug, + public_mark__isnull=False) if establishment_qs.exists(): establishment = establishment_qs.first() subquery_filter_by_distance = Subquery( self.exclude(slug=establishment_slug) .filter(image_url__isnull=False, public_mark__gte=10) - .published() .has_published_reviews() - .annotate_distance(point=establishment.address.coordinates) + .annotate_distance(point=establishment.location) .order_by('distance')[:settings.LIMITING_QUERY_NUMBER] .values('id') ) - return Establishment.objects.filter(id__in=subquery_filter_by_distance) \ - .annotate_intermediate_public_mark() \ - .annotate_mark_similarity(mark=establishment.public_mark) \ - .order_by('mark_similarity') + return self.filter(id__in=subquery_filter_by_distance) \ + .annotate_intermediate_public_mark() \ + .annotate_mark_similarity(mark=establishment.public_mark) \ + .order_by('mark_similarity') else: return self.none() @@ -168,7 +167,7 @@ class EstablishmentQuerySet(models.QuerySet): favorite_establishments = [] if user.is_authenticated: favorite_establishments = user.favorites.by_content_type(app_label='establishment', - model='establishment')\ + model='establishment') \ .values_list('object_id', flat=True) return self.annotate(in_favorites=models.Case( models.When( diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 5a69bd87..62b12016 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -1,5 +1,6 @@ """Establishment app views.""" +from django.conf import settings from django.contrib.gis.geos import Point from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions @@ -8,9 +9,10 @@ from comment import models as comment_models from establishment import filters from establishment import models, serializers from establishment.views import EstablishmentMixin +from main import methods from main.models import MetaDataContent -from utils.pagination import EstablishmentPagination from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer +from utils.pagination import EstablishmentPortionPagination class EstablishmentListView(EstablishmentMixin, generics.ListAPIView): @@ -28,12 +30,12 @@ class EstablishmentListView(EstablishmentMixin, generics.ListAPIView): class EstablishmentSimilarListView(EstablishmentListView): """Resource for getting a list of establishments.""" serializer_class = serializers.EstablishmentListSerializer - pagination_class = EstablishmentPagination + pagination_class = EstablishmentPortionPagination def get_queryset(self): """Override get_queryset method""" - return super().get_queryset() \ - .similar(establishment_slug=self.kwargs.get('slug')) + qs = super().get_queryset() + return qs.similar(establishment_slug=self.kwargs.get('slug')) class EstablishmentRetrieveView(EstablishmentMixin, generics.RetrieveAPIView): diff --git a/apps/main/methods.py b/apps/main/methods.py index e9ec780c..67da3480 100644 --- a/apps/main/methods.py +++ b/apps/main/methods.py @@ -1,9 +1,10 @@ """Main app methods.""" import logging + from django.conf import settings from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception -from main import models +from main import models logger = logging.getLogger(__name__) @@ -38,6 +39,19 @@ def determine_country_code(ip_addr): return country_code +def determine_coordinates(ip_addr): + longitude, latitude = None, None + if ip_addr: + try: + geoip = GeoIP2() + longitude, latitude = geoip.coords(ip_addr) + except GeoIP2Exception as ex: + logger.info(f'GEOIP Exception: {ex}. ip: {ip_addr}') + except Exception as ex: + logger.error(f'GEOIP Base exception: {ex}') + return longitude, latitude + + def determine_user_site_url(country_code): """Determine user's site url.""" try: diff --git a/apps/utils/middleware.py b/apps/utils/middleware.py index 7203127c..096f8474 100644 --- a/apps/utils/middleware.py +++ b/apps/utils/middleware.py @@ -31,14 +31,3 @@ def parse_cookies(get_response): response = get_response(request) return response return middleware - - -class CORSMiddleware: - """Added parameter {Access-Control-Allow-Origin: *} to response""" - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - response = self.get_response(request) - response["Access-Control-Allow-Origin"] = '*' - return response diff --git a/apps/utils/pagination.py b/apps/utils/pagination.py index efaf329e..2c9e92e5 100644 --- a/apps/utils/pagination.py +++ b/apps/utils/pagination.py @@ -1,6 +1,8 @@ """Pagination settings.""" from base64 import b64encode from urllib import parse as urlparse + +from django.conf import settings from rest_framework.pagination import PageNumberPagination, CursorPagination @@ -46,8 +48,8 @@ class ProjectMobilePagination(ProjectPageNumberPagination): return self.page.previous_page_number() -class EstablishmentPagination(ProjectMobilePagination): +class EstablishmentPortionPagination(ProjectMobilePagination): """ Pagination for app establishments with limit page size equal to 12 """ - page_size = 12 + page_size = settings.LIMITING_OUTPUT_OBJECTS diff --git a/project/settings/base.py b/project/settings/base.py index 2007d908..10eb3544 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -413,4 +413,13 @@ SITE_REDIRECT_URL_UNSUBSCRIBE = '/unsubscribe/' SITE_NAME = 'Gault & Millau' -LIMITING_QUERY_NUMBER = 36 # Need to restrict objects to sort (used in establishments) +# Used in annotations for establishments. +DEFAULT_ESTABLISHMENT_PUBLIC_MARK = 10 +# Limit output objects (see in pagination classes). +LIMITING_OUTPUT_OBJECTS = 12 +# Need to restrict objects to sort (3 times more then expected). +LIMITING_QUERY_NUMBER = LIMITING_OUTPUT_OBJECTS * 3 + +# GEO +# A Spatial Reference System Identifier +GEO_DEFAULT_SRID = 4326 From c4d8b89a8db7feb1cd9c92c278f10e0276a14886 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Thu, 26 Sep 2019 14:19:01 +0300 Subject: [PATCH 36/36] gm-127: finished --- apps/establishment/models.py | 55 ++++++++++++++++++++++++------ apps/establishment/urls/common.py | 2 ++ apps/establishment/views/common.py | 2 +- apps/establishment/views/web.py | 19 +++++++++++ project/settings/base.py | 33 +++++++++++++----- 5 files changed, 90 insertions(+), 21 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index aed78d9f..9334e4b7 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -93,15 +93,20 @@ class EstablishmentQuerySet(models.QuerySet): """ return self.filter(is_publish=True) - def annotate_distance(self, point: Point): + def has_published_reviews(self): + """ + Return QuerySet establishments with published reviews. + """ + return self.filter(reviews__status=Review.READY,) + + def annotate_distance(self, point: Point = None): """ Return QuerySet with annotated field - distance Description: """ - return self.annotate(distance=models.Value( - DistanceMeasure(Distance('address__coordinates', point, srid=4236)).m, - output_field=models.FloatField())) + return self.annotate(distance=Distance('address__coordinates', point, + srid=settings.GEO_DEFAULT_SRID)) def annotate_intermediate_public_mark(self): """ @@ -110,8 +115,8 @@ class EstablishmentQuerySet(models.QuerySet): If establishments in collection POP and its mark is null, then intermediate_mark is set to 10; """ - return self.annotate(intermediate_public_mark=models.Case( - models.When( + return self.annotate(intermediate_public_mark=Case( + When( collections__collection_type=Collection.POP, public_mark__isnull=True, then=settings.DEFAULT_ESTABLISHMENT_PUBLIC_MARK @@ -125,8 +130,8 @@ class EstablishmentQuerySet(models.QuerySet): Description: Similarity mark determined by comparison with compared establishment mark """ - return self.annotate(mark_similarity=models.ExpressionWrapper( - mark - models.F('intermediate_public_mark'), + return self.annotate(mark_similarity=ExpressionWrapper( + mark - F('intermediate_public_mark'), output_field=models.FloatField() )) @@ -154,6 +159,21 @@ class EstablishmentQuerySet(models.QuerySet): else: return self.none() + def last_reviewed(self, point: Point): + """ + Return QuerySet with last reviewed establishments. + :param point: location Point object, needs to ordering + """ + subquery_filter_by_distance = Subquery( + self.filter(image_url__isnull=False, public_mark__gte=10) + .has_published_reviews() + .annotate_distance(point=point) + .order_by('distance')[:settings.LIMITING_QUERY_NUMBER] + .values('id') + ) + return self.filter(id__in=subquery_filter_by_distance) \ + .order_by('-reviews__published_at') + def prefetch_actual_employees(self): """Prefetch actual employees.""" return self.prefetch_related( @@ -169,8 +189,8 @@ class EstablishmentQuerySet(models.QuerySet): favorite_establishments = user.favorites.by_content_type(app_label='establishment', model='establishment') \ .values_list('object_id', flat=True) - return self.annotate(in_favorites=models.Case( - models.When( + return self.annotate(in_favorites=Case( + When( id__in=favorite_establishments, then=True), default=False, @@ -194,7 +214,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): name = models.CharField(_('name'), max_length=255, default='') name_translated = models.CharField(_('Transliterated name'), - max_length=255, default='') + max_length=255, default='') description = TJSONField(blank=True, null=True, default=None, verbose_name=_('description'), help_text='{"en-GB":"some text"}') @@ -308,6 +328,19 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): return [{'id': tag.metadata.id, 'label': tag.metadata.label} for tag in self.tags.all()] + @property + def last_published_review(self): + """Return last published review""" + return self.reviews.published()\ + .order_by('-published_at').first() + + @property + def location(self): + """ + Return Point object of establishment location + """ + return self.address.coordinates + class Position(BaseAttributes, TranslatedFieldsMixin): """Position model.""" diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index bd53c7eb..c1161e92 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -8,6 +8,8 @@ app_name = 'establishment' urlpatterns = [ path('', views.EstablishmentListView.as_view(), name='list'), path('tags/', views.EstablishmentTagListView.as_view(), name='tags'), + path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(), + name='recent-reviews'), path('/', views.EstablishmentRetrieveView.as_view(), name='detail'), path('/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'), path('/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'), diff --git a/apps/establishment/views/common.py b/apps/establishment/views/common.py index b57d6ef6..922882da 100644 --- a/apps/establishment/views/common.py +++ b/apps/establishment/views/common.py @@ -11,6 +11,6 @@ class EstablishmentMixin: permission_classes = (permissions.AllowAny,) def get_queryset(self): - """Overrided method 'get_queryset'.""" + """Overridden method 'get_queryset'.""" return models.Establishment.objects.published() \ .prefetch_actual_employees() diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 62b12016..22ef3abc 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -27,6 +27,25 @@ class EstablishmentListView(EstablishmentMixin, generics.ListAPIView): .annotate_in_favorites(user=self.request.user) +class EstablishmentRecentReviewListView(EstablishmentListView): + """List view for last reviewed establishments.""" + pagination_class = EstablishmentPortionPagination + + def get_queryset(self): + """Overridden method 'get_queryset'.""" + qs = super().get_queryset() + user_ip = methods.get_user_ip(self.request) + query_params = self.request.query_params + if 'longitude' in query_params and 'latitude' in query_params: + longitude, latitude = query_params.get('longitude'), query_params.get('latitude') + else: + longitude, latitude = methods.determine_coordinates(user_ip) + if not longitude or not latitude: + return qs.none() + point = Point(x=float(longitude), y=float(latitude), srid=settings.GEO_DEFAULT_SRID) + return qs.last_reviewed(point=point) + + class EstablishmentSimilarListView(EstablishmentListView): """Resource for getting a list of establishments.""" serializer_class = serializers.EstablishmentListSerializer diff --git a/project/settings/base.py b/project/settings/base.py index 10eb3544..cfea18a5 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -327,15 +327,30 @@ FCM_DJANGO_SETTINGS = { # Thumbnail settings THUMBNAIL_ALIASES = { - '': { - 'tiny': {'size': (100, 0), }, - 'small': {'size': (480, 0), }, - 'middle': {'size': (700, 0), }, - 'large': {'size': (1500, 0), }, - 'default': {'size': (300, 200), 'crop': True}, - 'gallery': {'size': (240, 160), 'crop': True}, - 'establishment_preview': {'size': (300, 280), 'crop': True}, - } + 'news_preview': { + 'web': {'size': (300, 260), } + }, + 'news_promo_horizontal': { + 'web': {'size': (1900, 600), }, + 'mobile': {'size': (375, 260), }, + }, + 'news_tile_horizontal': { + 'web': {'size': (300, 275), }, + 'mobile': {'size': (343, 180), }, + }, + 'news_tile_vertical': { + 'web': {'size': (300, 380), }, + }, + 'news_highlight_vertical': { + 'web': {'size': (460, 630), }, + }, + 'news_editor': { + 'web': {'size': (940, 430), }, # при загрузке через контент эдитор + 'mobile': {'size': (343, 260), }, # через контент эдитор в мобильном браузерe + }, + 'avatar_comments': { + 'web': {'size': (116, 116), }, + }, } # Password reset