Merge branch 'develop' into feature/refactor-establishments

This commit is contained in:
evgeniy-st 2019-09-27 10:05:15 +03:00
commit 403a60623a
29 changed files with 826 additions and 157 deletions

View File

@ -1,27 +1,25 @@
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
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)

View File

@ -0,0 +1,49 @@
from rest_framework.test import APITestCase
from rest_framework import status
from authorization.tests.tests_authorization import get_tokens_for_user
from django.urls import reverse
from account.models import User
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)
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)

View File

View File

@ -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"]

View File

@ -1,19 +1,21 @@
"""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 main.models import MetaDataContent
from location.models import Address
from review.models import Review
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes)
@ -106,29 +108,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()))
def annotate_distance_mark(self):
"""
Return QuerySet with annotated field - distance_mark.
Required fields: distance.
Description:
If the radius of the establishments in QuerySet does not exceed 500 meters,
then distance_mark is set to 0.6, otherwise 0.
"""
return self.annotate(distance_mark=models.Case(
models.When(distance__lte=500,
then=0.6),
default=0,
output_field=models.FloatField()))
return self.annotate(distance=Distance('address__coordinates', point,
srid=settings.GEO_DEFAULT_SRID))
def annotate_intermediate_public_mark(self):
"""
@ -137,72 +130,65 @@ 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=10
then=settings.DEFAULT_ESTABLISHMENT_PUBLIC_MARK
),
default='public_mark',
output_field=models.FloatField()))
def annotate_additional_mark(self, public_mark: float):
def annotate_mark_similarity(self, mark):
"""
Return QuerySet with annotated field - additional_mark.
Required fields: intermediate_public_mark
Return a QuerySet with annotated field - mark_similarity
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.
Similarity mark determined by comparison with compared establishment mark
"""
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=ExpressionWrapper(
mark - F('intermediate_public_mark'),
output_field=models.FloatField()
))
def similar(self, establishment_slug: str):
"""
Return QuerySet with objects that similar to Establishment.
:param establishment_slug: str Establishment slug
"""
establishment_qs = Establishment.objects.filter(slug=establishment_slug)
establishment_qs = self.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_distance_mark() \
subquery_filter_by_distance = Subquery(
self.exclude(slug=establishment_slug)
.filter(image_url__isnull=False, public_mark__gte=10)
.has_published_reviews()
.annotate_distance(point=establishment.location)
.order_by('distance')[:settings.LIMITING_QUERY_NUMBER]
.values('id')
)
return self.filter(id__in=subquery_filter_by_distance) \
.annotate_intermediate_public_mark() \
.annotate_additional_mark(public_mark=establishment.public_mark) \
.annotate_total_mark()
.annotate_mark_similarity(mark=establishment.public_mark) \
.order_by('mark_similarity')
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(
@ -216,10 +202,10 @@ 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(
return self.annotate(in_favorites=Case(
When(
id__in=favorite_establishments,
then=True),
default=False,
@ -293,7 +279,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')
@ -357,6 +343,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."""

View File

@ -4,7 +4,11 @@ from establishment.serializers import (
EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer,
ContactPhonesSerializer, SocialNetworkRelatedSerializers,
EstablishmentTypeSerializer)
from utils.decorators import with_base_attributes
from main.models import Currency
from utils.serializers import TJSONSerializer
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
@ -85,7 +89,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
@ -122,7 +126,10 @@ class ContactEmailBackSerializers(PlateSerializer):
]
# TODO: test decorator
@with_base_attributes
class EmployeeBackSerializers(serializers.ModelSerializer):
"""Social network serializers."""
class Meta:
model = models.Employee
@ -130,4 +137,5 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
'id',
'user',
'name'
]
]

View File

@ -1,6 +1,6 @@
"""Establishment serializers."""
from rest_framework import serializers
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from comment import models as comment_models
from comment.serializers import common as comment_serializers
from establishment import models
@ -12,6 +12,7 @@ from review import models as review_models
from timetable.serialziers import ScheduleRUDSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import TranslatedField
from utils.serializers import TJSONSerializer
class ContactPhonesSerializer(serializers.ModelSerializer):
@ -60,7 +61,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 +75,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
@ -191,7 +192,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)
@ -223,12 +224,6 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer):
'transportation',
]
# todo: refactor this s (make as prefetch to model as attr or make as model property)
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
@ -308,20 +303,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):

View File

@ -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.
@ -27,7 +26,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 +92,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 +263,89 @@ 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_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_CR(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)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
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)

View File

@ -8,13 +8,15 @@ app_name = 'establishment'
urlpatterns = [
path('', views.EstablishmentListView.as_view(), name='list'),
path('tags/', views.EstablishmentTagListView.as_view(), name='tags'),
path('<slug:slug>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
path('<slug:slug>/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'),
path('<slug:slug>/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'),
path('<slug:slug>/comments/create/', views.EstablishmentCommentCreateView.as_view(),
path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(),
name='recent-reviews'),
path('slug/<slug:slug>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
path('slug/<slug:slug>/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'),
path('slug/<slug:slug>/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'),
path('slug/<slug:slug>/comments/create/', views.EstablishmentCommentCreateView.as_view(),
name='create-comment'),
path('<slug:slug>/comments/<int:comment_id>/', views.EstablishmentCommentRUDView.as_view(),
path('slug/<slug:slug>/comments/<int:comment_id>/', views.EstablishmentCommentRUDView.as_view(),
name='rud-comment'),
path('<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
name='add-to-favorites')
]

View File

@ -1 +0,0 @@
"""Establishment app views."""

View File

@ -1,12 +1,17 @@
"""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
from comment import models as comment_models
from establishment import filters, models, serializers
from establishment import filters
from establishment import models, serializers
from establishment.views import EstablishmentMixin
from main import methods
from main.models import MetaDataContent
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
from utils.pagination import EstablishmentPortionPagination
class EstablishmentMixinView:
@ -44,14 +49,35 @@ class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView
return super().get_queryset().with_extended_related()
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
pagination_class = EstablishmentPortionPagination
def get_queryset(self):
"""Override get_queryset method"""
return super().get_queryset().similar(establishment_slug=self.kwargs.get('slug'))\
.order_by('-total_mark')[:13]
qs = super().get_queryset()
return qs.similar(establishment_slug=self.kwargs.get('slug'))
class EstablishmentTypeListView(generics.ListAPIView):

View File

@ -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')

View File

@ -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, )

View File

@ -1,3 +1,193 @@
from django.test import TestCase
import json
# Create your tests here.
from rest_framework.test import APITestCase
from account.models import User
from rest_framework import status
from http.cookies import SimpleCookie
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 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 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/regions/', 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 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",
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):
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",
country=self.country
)
self.city = City.objects.create(
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 = {
'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, format='json')
response_data = response.json()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
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(f'/api/back/location/addresses/{response_data["id"]}/', data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete(f'/api/back/location/addresses/{response_data["id"]}/', format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

View File

@ -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:

View File

@ -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()

View File

@ -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)

View File

@ -6,6 +6,6 @@ app_name = 'news'
urlpatterns = [
path('', views.NewsListView.as_view(), name='list'),
path('<slug:slug>/', views.NewsDetailView.as_view(), name='rud'),
path('types/', views.NewsTypeListView.as_view(), name='type'),
path('slug/<slug:slug>/', views.NewsDetailView.as_view(), name='rud'),
]

116
apps/notification/tests.py Normal file
View File

@ -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)

View File

@ -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"""

View File

@ -84,6 +84,8 @@ class EstablishmentDocument(Document):
'name',
'toque_number',
'price_level',
'preview_image_url',
'slug',
)
def get_queryset(self):

View File

@ -63,6 +63,8 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
'collections',
'establishment_type',
'establishment_subtypes',
'preview_image_url',
'slug',
)
@staticmethod

21
apps/utils/decorators.py Normal file
View File

@ -0,0 +1,21 @@
def with_base_attributes(cls):
def validate(self, data):
user = None
request = self.context.get("request")
if request and hasattr(request, "user"):
user = request.user
if user is not None:
data.update({'modified_by': user})
if not self.instance:
data.update({'created_by': user})
return data
setattr(cls, "validate", validate)
return cls

View File

@ -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

View File

@ -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

View File

@ -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
@ -44,3 +46,10 @@ class ProjectMobilePagination(ProjectPageNumberPagination):
if not self.page.has_previous():
return None
return self.page.previous_page_number()
class EstablishmentPortionPagination(ProjectMobilePagination):
"""
Pagination for app establishments with limit page size equal to 12
"""
page_size = settings.LIMITING_OUTPUT_OBJECTS

View File

@ -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]

View File

@ -8,6 +8,13 @@ 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
class BaseTestCase(APITestCase):
@ -28,6 +35,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 +58,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 +69,90 @@ 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)
modify_user = User.objects.create_user(
username='sedragurda2',
password='sedragurdaredips192',
email='sedragurda2@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)
employee.refresh_from_db()
self.assertEqual(modify_user, employee.modified_by)
self.assertEqual(self.user, employee.created_by)
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")

View File

@ -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
@ -412,3 +427,14 @@ SOLO_CACHE_TIMEOUT = 300
SITE_REDIRECT_URL_UNSUBSCRIBE = '/unsubscribe/'
SITE_NAME = 'Gault & Millau'
# 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