Merge branch 'develop' into feature/product

This commit is contained in:
evgeniy-st 2019-10-22 15:58:51 +03:00
commit 45a3cd4b2b
222 changed files with 10382 additions and 1063 deletions

View File

@ -1,2 +1,30 @@
# gm-backend
## Build
1. ``git clone ssh://git@gl.id-east.ru:222/gm/gm-backend.git``
1. ``cd ./gm-backend``
1. ``git checkout develop``
1. ``docker-compose build``
1. First start database: ``docker-compose up db``
1. ``docker-compose up -d``
### Migrate data
1.Connect to container with django ``docker exec -it gm-backend_gm_app_1 bash``
#### In docker container(django)
1. Migrate ``python manage.py migrate``
1. Create super-user ``python manage.py createsuperuser``
Backend is available at localhost:8000 or 0.0.0.0:8000
URL for admin http://0.0.0.0:8000/admin
URL for swagger http://0.0.0.0:8000/docs/
URL for redocs http://0.0.0.0:8000/redocs/
## Start and stop backend containers
Demonize start ``docker-compose up -d``
Stop ``docker-compose down``
Stop and remove volumes ``docker-compose down -v``

View File

@ -2,10 +2,19 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.utils.translation import ugettext_lazy as _
from account import models
@admin.register(models.Role)
class RoleAdmin(admin.ModelAdmin):
list_display = ['role', 'country']
@admin.register(models.UserRole)
class UserRoleAdmin(admin.ModelAdmin):
list_display = ['user', 'role', 'establishment']
@admin.register(models.User)
class UserAdmin(BaseUserAdmin):
"""User model admin settings."""

View File

@ -0,0 +1,48 @@
# Generated by Django 2.2.4 on 2019-10-11 11:23
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('location', '0011_country_languages'),
('account', '0008_auto_20190912_1325'),
]
operations = [
migrations.CreateModel(
name='Role',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('role', models.PositiveIntegerField(choices=[(1, 'Standard user'), (2, 'Comments moderator')], verbose_name='Role')),
('country', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='location.Country', verbose_name='Country')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='UserRole',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('role', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='account.Role', verbose_name='Role')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='user',
name='roles',
field=models.ManyToManyField(through='account.UserRole', to='account.Role', verbose_name='Roles'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-10 14:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0008_auto_20190912_1325'),
]
operations = [
migrations.AddField(
model_name='user',
name='unconfirmed_email',
field=models.EmailField(blank=True, default=None, max_length=254, null=True, verbose_name='unconfirmed email'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-10 17:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0009_user_unconfirmed_email'),
]
operations = [
migrations.AddField(
model_name='user',
name='password_confirmed',
field=models.BooleanField(default=True, verbose_name='is new password confirmed'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-14 08:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0009_auto_20191011_1123'),
('account', '0010_user_password_confirmed'),
]
operations = [
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-14 12:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0009_auto_20191011_1123'),
('account', '0010_user_password_confirmed'),
]
operations = [
migrations.RemoveField(
model_name='user',
name='password_confirmed',
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-15 07:08
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('account', '0011_merge_20191014_0839'),
('account', '0011_merge_20191014_1258'),
]
operations = [
]

View File

@ -0,0 +1,30 @@
# Generated by Django 2.2.4 on 2019-10-16 08:10
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('establishment', '0033_auto_20191003_0943_squashed_0034_auto_20191003_1036'),
('account', '0012_merge_20191015_0708'),
]
operations = [
migrations.AddField(
model_name='userrole',
name='establishment',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='establishment.Establishment', verbose_name='Establishment'),
),
migrations.AlterField(
model_name='role',
name='country',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.Country', verbose_name='Country'),
),
migrations.AlterField(
model_name='role',
name='role',
field=models.PositiveIntegerField(choices=[(1, 'Standard user'), (2, 'Comments moderator'), (3, 'Country admin'), (4, 'Content page manager'), (5, 'Establishment manager')], verbose_name='Role'),
),
]

View File

@ -13,11 +13,42 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework.authtoken.models import Token
from authorization.models import Application
from establishment.models import Establishment
from location.models import Country
from utils.models import GMTokenGenerator
from utils.models import ImageMixin, ProjectBaseMixin, PlatformMixin
from utils.tokens import GMRefreshToken
class Role(ProjectBaseMixin):
"""Base Role model."""
STANDARD_USER = 1
COMMENTS_MODERATOR = 2
COUNTRY_ADMIN = 3
CONTENT_PAGE_MANAGER = 4
ESTABLISHMENT_MANAGER = 5
REVIEWER_MANGER = 6
RESTAURANT_REVIEWER = 7
ROLE_CHOICES = (
(STANDARD_USER, 'Standard user'),
(COMMENTS_MODERATOR, 'Comments moderator'),
(COUNTRY_ADMIN, 'Country admin'),
(CONTENT_PAGE_MANAGER, 'Content page manager'),
(ESTABLISHMENT_MANAGER, 'Establishment manager'),
(REVIEWER_MANGER, 'Reviewer manager'),
(RESTAURANT_REVIEWER, 'Restaurant reviewer')
)
role = models.PositiveIntegerField(verbose_name=_('Role'), choices=ROLE_CHOICES,
null=False, blank=False)
country = models.ForeignKey(Country, verbose_name=_('Country'),
null=True, blank=True, on_delete=models.SET_NULL)
# is_list = models.BooleanField(verbose_name=_('list'), default=True, null=False)
# is_create = models.BooleanField(verbose_name=_('create'), default=False, null=False)
# is_update = models.BooleanField(verbose_name=_('update'), default=False, null=False)
# is_delete = models.BooleanField(verbose_name=_('delete'), default=False, null=False)
class UserManager(BaseUserManager):
"""Extended manager for User model."""
@ -60,6 +91,7 @@ class User(AbstractUser):
blank=True, null=True, default=None)
email = models.EmailField(_('email address'), blank=True,
null=True, default=None)
unconfirmed_email = models.EmailField(_('unconfirmed email'), blank=True, null=True, default=None)
email_confirmed = models.BooleanField(_('email status'), default=False)
newsletter = models.NullBooleanField(default=True)
@ -67,6 +99,7 @@ class User(AbstractUser):
USERNAME_FIELD = 'username'
REQUIRED_FIELDS = ['email']
roles = models.ManyToManyField(Role, verbose_name=_('Roles'), through='UserRole')
objects = UserManager.from_queryset(UserQuerySet)()
class Meta:
@ -112,6 +145,9 @@ class User(AbstractUser):
def confirm_email(self):
"""Method to confirm user email address"""
if self.unconfirmed_email is not None:
self.email = self.unconfirmed_email
self.unconfirmed_email = None
self.email_confirmed = True
self.save()
@ -195,3 +231,11 @@ class User(AbstractUser):
return render_to_string(
template_name=settings.CHANGE_EMAIL_TEMPLATE,
context=context)
class UserRole(ProjectBaseMixin):
"""UserRole model."""
user = models.ForeignKey(User, verbose_name=_('User'), on_delete=models.CASCADE)
role = models.ForeignKey(Role, verbose_name=_('Role'), on_delete=models.SET_NULL, null=True)
establishment = models.ForeignKey(Establishment, verbose_name=_('Establishment'),
on_delete=models.SET_NULL, null=True, blank=True)

View File

@ -0,0 +1,21 @@
"""Back account serializers"""
from rest_framework import serializers
from account import models
class RoleSerializer(serializers.ModelSerializer):
class Meta:
model = models.Role
fields = [
'role',
'country'
]
class UserRoleSerializer(serializers.ModelSerializer):
class Meta:
model = models.UserRole
fields = [
'user',
'role'
]

View File

@ -1,5 +1,6 @@
"""Common account serializers"""
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from django.contrib.auth import password_validation as password_validators
from fcm_django.models import FCMDevice
from rest_framework import exceptions
@ -60,9 +61,12 @@ class UserSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
"""Override update method"""
old_email = instance.email
instance = super().update(instance, validated_data)
if 'email' in validated_data:
instance.email_confirmed = False
instance.email = old_email
instance.unconfirmed_email = validated_data['email']
instance.save()
# Send verification link on user email for change email address
if settings.USE_CELERY:
@ -76,27 +80,52 @@ class UserSerializer(serializers.ModelSerializer):
return instance
class UserBaseSerializer(serializers.ModelSerializer):
"""Serializer is used to display brief information about the user."""
fullname = serializers.CharField(source='get_full_name', read_only=True)
class Meta:
"""Meta class."""
model = models.User
fields = (
'fullname',
'cropped_image_url',
'image_url',
)
read_only_fields = fields
class ChangePasswordSerializer(serializers.ModelSerializer):
"""Serializer for model User."""
password = serializers.CharField(write_only=True)
old_password = serializers.CharField(write_only=True)
class Meta:
"""Meta class"""
model = models.User
fields = ('password', )
fields = (
'password',
'old_password',
)
def validate(self, attrs):
"""Override validate method"""
password = attrs.get('password')
old_password = attrs.get('old_password')
try:
# Check old password
if not self.instance.check_password(raw_password=old_password):
raise serializers.ValidationError(_('Old password mismatch.'))
# Compare new password with the old ones
if self.instance.check_password(raw_password=password):
raise utils_exceptions.PasswordsAreEqual()
raise serializers.ValidationError(_('Password is already in use'))
# Validate password
password_validators.validate_password(password=password)
except serializers.ValidationError as e:
raise serializers.ValidationError(str(e))
raise serializers.ValidationError({'detail': e.detail})
else:
return attrs
@ -146,38 +175,6 @@ class ChangeEmailSerializer(serializers.ModelSerializer):
return instance
class ConfirmEmailSerializer(serializers.ModelSerializer):
"""Confirm user email serializer"""
x = serializers.CharField(default=None)
class Meta:
"""Meta class"""
model = models.User
fields = (
'email',
)
def validate(self, attrs):
"""Override validate method"""
email_confirmed = self.instance.email_confirmed
if email_confirmed:
raise utils_exceptions.EmailConfirmedError()
return attrs
def update(self, instance, validated_data):
"""
Override update method
"""
# Send verification link on user email for change email address
if settings.USE_CELERY:
tasks.confirm_new_email_address.delay(instance.id)
else:
tasks.confirm_new_email_address(instance.id)
return instance
# Firebase Cloud Messaging serializers
class FCMDeviceSerializer(serializers.ModelSerializer):
"""FCM Device model serializer"""

View File

@ -0,0 +1,81 @@
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 http.cookies import SimpleCookie
from location.models import Country
from account.models import Role, User, UserRole
class RoleTests(APITestCase):
def setUp(self):
self.data = get_tokens_for_user()
self.client.cookies = SimpleCookie(
{'access_token': self.data['tokens'].get('access_token'),
'refresh_token': self.data['tokens'].get('access_token')})
def test_role_get(self):
url = reverse('back:account:role-list-create')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_role_post(self):
url = reverse('back:account:role-list-create')
country = Country.objects.create(
name='{"ru-RU":"Russia"}',
code='23',
low_price=15,
high_price=150000
)
country.save()
data = {
"role": 2,
"country": country.pk
}
response = self.client.post(url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
class UserRoleTests(APITestCase):
def setUp(self):
self.data = get_tokens_for_user()
self.client.cookies = SimpleCookie(
{'access_token': self.data['tokens'].get('access_token'),
'refresh_token': self.data['tokens'].get('access_token')})
self.country_ru = Country.objects.create(
name='{"ru-RU":"Russia"}',
code='23',
low_price=15,
high_price=150000
)
self.country_ru.save()
self.country_en = Country.objects.create(
name='{"en-GB":"England"}',
code='25',
low_price=15,
high_price=150000
)
self.country_en.save()
self.role = Role.objects.create(
role=2,
country=self.country_ru
)
self.role.save()
self.user_test = User.objects.create_user(username='test',
email='testemail@mail.com',
password='passwordtest')
def test_user_role_post(self):
url = reverse('back:account:user-role-list-create')
data = {
"user": self.user_test.id,
"role": self.role.id
}
response = self.client.post(url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)

View File

@ -0,0 +1,78 @@
from rest_framework.test import APITestCase
from rest_framework import status
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):
def setUp(self):
self.data = get_tokens_for_user()
def test_user_url(self):
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(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = {
"username": self.data["username"],
"first_name": "Test first name",
"last_name": "Test last name",
"cropped_image_url": "http://localhost/image/cropped.png",
"image_url": "http://localhost/image/avatar.png",
"email": "sedragurdatest@desoz.com",
"newsletter": self.data["newsletter"]
}
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(url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
class AccountChangePasswordTests(APITestCase):
def setUp(self):
self.data = get_tokens_for_user()
def test_change_password(self):
data = {
"old_password": self.data["password"],
"password": "new password"
}
url = reverse('web:account:change-password')
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(url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
class AccountChangePasswordTests(APITestCase):
def setUp(self):
self.data = get_tokens_for_user()
def test_confirm_email(self):
user = User.objects.get(email=self.data["email"])
token = user.confirm_email_token
uidb64 = user.get_user_uidb64
url = reverse('web:account:confirm-email', kwargs={
'uidb64': uidb64,
'token': token
})
response = self.client.get(url)
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)

12
apps/account/urls/back.py Normal file
View File

@ -0,0 +1,12 @@
"""Back account URLs"""
from django.urls import path
from account.views import back as views
app_name = 'account'
urlpatterns = [
path('role/', views.RoleLstView.as_view(), name='role-list-create'),
path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'),
]

View File

@ -8,5 +8,6 @@ app_name = 'account'
urlpatterns = [
path('user/', views.UserRetrieveUpdateView.as_view(), name='user-retrieve-update'),
path('change-password/', views.ChangePasswordView.as_view(), name='change-password'),
path('email/confirm/', views.SendConfirmationEmailView.as_view(), name='send-confirm-email'),
path('email/confirm/<uidb64>/<token>/', views.ConfirmEmailView.as_view(), name='confirm-email'),
]

View File

@ -0,0 +1,13 @@
from rest_framework import generics
from account.serializers import back as serializers
from account import models
class RoleLstView(generics.ListCreateAPIView):
serializer_class = serializers.RoleSerializer
queryset = models.Role.objects.all()
class UserRoleLstView(generics.ListCreateAPIView):
serializer_class = serializers.UserRoleSerializer
queryset = models.Role.objects.all()

View File

@ -1,4 +1,5 @@
"""Common account views"""
from django.conf import settings
from django.utils.encoding import force_text
from django.utils.http import urlsafe_base64_decode
from fcm_django.models import FCMDevice
@ -9,6 +10,7 @@ from rest_framework.response import Response
from account import models
from account.serializers import common as serializers
from authorization.tasks import send_confirm_email
from utils import exceptions as utils_exceptions
from utils.models import GMTokenGenerator
from utils.views import JWTGenericViewMixin
@ -38,19 +40,26 @@ class ChangePasswordView(generics.GenericAPIView):
return Response(status=status.HTTP_200_OK)
class SendConfirmationEmailView(JWTGenericViewMixin):
class SendConfirmationEmailView(generics.GenericAPIView):
"""Confirm email view."""
serializer_class = serializers.ConfirmEmailSerializer
queryset = models.User.objects.all()
def patch(self, request, *args, **kwargs):
"""Implement PATCH-method"""
# Get user instance
instance = self.request.user
def post(self, request, *args, **kwargs):
"""Override create method"""
user = self.request.user
country_code = self.request.country_code
serializer = self.get_serializer(data=request.data, instance=instance)
serializer.is_valid(raise_exception=True)
serializer.save()
if user.email_confirmed:
raise utils_exceptions.EmailConfirmedError()
# Send verification link on user email for change email address
if settings.USE_CELERY:
send_confirm_email.delay(
user_id=user.id,
country_code=country_code)
else:
send_confirm_email(
user_id=user.id,
country_code=country_code)
return Response(status=status.HTTP_200_OK)

View File

@ -40,8 +40,7 @@ class PasswordResetConfirmView(JWTGenericViewMixin):
queryset = models.User.objects.active()
def get_object(self):
"""Override get_object method
"""
"""Override get_object method"""
queryset = self.filter_queryset(self.get_queryset())
uidb64 = self.kwargs.get('uidb64')

View File

@ -4,6 +4,20 @@ from django.db import migrations, models
import uuid
def fill_uuid(apps, schemaeditor):
Advertisement = apps.get_model('advertisement', 'Advertisement')
for a in Advertisement.objects.all():
a.uuid = uuid.uuid4()
a.save()
def fill_block_level(apps, schemaeditor):
Advertisement = apps.get_model('advertisement', 'Advertisement')
for a in Advertisement.objects.all():
a.block_level = ''
a.save()
class Migration(migrations.Migration):
dependencies = [
@ -23,6 +37,12 @@ class Migration(migrations.Migration):
field=models.ManyToManyField(to='translation.Language'),
),
migrations.AddField(
model_name='advertisement',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False),
),
migrations.RunPython(fill_uuid, migrations.RunPython.noop),
migrations.AlterField(
model_name='advertisement',
name='uuid',
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
@ -32,8 +52,14 @@ class Migration(migrations.Migration):
name='block_level',
),
migrations.AddField(
model_name='advertisement',
name='block_level',
field=models.CharField(blank=True, null=True, max_length=10, verbose_name='Block level')
),
migrations.RunPython(fill_block_level, migrations.RunPython.noop),
migrations.AlterField(
model_name='advertisement',
name='block_level',
field=models.CharField(max_length=10, verbose_name='Block level')
)
),
]

View File

@ -4,7 +4,7 @@ from django.contrib.auth import authenticate
from django.contrib.auth import password_validation as password_validators
from django.db.models import Q
from rest_framework import serializers
from rest_framework import validators as rest_validators
from rest_framework.generics import get_object_or_404
from account import models as account_models
from authorization import tasks
@ -81,7 +81,6 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
"""Serializer for login user"""
# REQUEST
username_or_email = serializers.CharField(write_only=True)
password = serializers.CharField(write_only=True)
# For cookie properties (Max-Age)
remember = serializers.BooleanField(write_only=True)
@ -101,21 +100,24 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
'refresh_token',
'access_token',
)
extra_kwargs = {
'password': {'write_only': True}
}
def validate(self, attrs):
"""Override validate method"""
username_or_email = attrs.pop('username_or_email')
password = attrs.pop('password')
user_qs = account_models.User.objects.filter(Q(username=username_or_email) |
(Q(email=username_or_email)))
Q(email=username_or_email))
if not user_qs.exists():
raise utils_exceptions.UserNotFoundError()
raise utils_exceptions.WrongAuthCredentials()
else:
user = user_qs.first()
authentication = authenticate(username=user.get_username(),
password=password)
if not authentication:
raise utils_exceptions.UserNotFoundError()
raise utils_exceptions.WrongAuthCredentials()
self.instance = user
return attrs
@ -127,10 +129,6 @@ class LoginByUsernameOrEmailSerializer(SourceSerializerMixin,
return super().to_representation(instance)
class LogoutSerializer(SourceSerializerMixin):
"""Serializer for Logout endpoint."""
class RefreshTokenSerializer(SourceSerializerMixin):
"""Serializer for refresh token view"""
refresh_token = serializers.CharField(read_only=True)
@ -169,7 +167,3 @@ class RefreshTokenSerializer(SourceSerializerMixin):
class OAuth2Serialzier(SourceSerializerMixin):
"""Serializer OAuth2 authorization"""
token = serializers.CharField(max_length=255)
class OAuth2LogoutSerializer(SourceSerializerMixin):
"""Serializer for logout"""

View File

@ -1,7 +1,8 @@
"""Authorization app celery tasks."""
import logging
from django.utils.translation import gettext_lazy as _
from celery import shared_task
from django.utils.translation import gettext_lazy as _
from account import models as account_models
@ -10,12 +11,12 @@ logger = logging.getLogger(__name__)
@shared_task
def send_confirm_email(user_id, country_code):
def send_confirm_email(user_id: int, country_code: str):
"""Send verification email to user."""
try:
obj = account_models.User.objects.get(id=user_id)
obj.send_email(subject=_('Email confirmation'),
message=obj.confirm_email_template(country_code))
except:
except Exception as e:
logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n'
f'DETAIL: Exception occurred for user: {user_id}')
f'DETAIL: user {user_id}, - {e}')

View File

@ -1,25 +0,0 @@
from rest_framework.test import APITestCase
from account.models import User
# Create your tests here.
class AuthorizationTests(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)
self.tokkens = User.create_jwt_tokens(self.user)
def LoginTests(self):
data ={
'username_or_email': self.username,
'password': self.password,
'remember': True
}
response = self.client.post('/api/auth/login/', data=data)
self.assertEqual(response.data['access_token'], self.tokkens.get('access_token'))
self.assertEqual(response.data['refresh_token'], self.tokkens.get('refresh_token'))

View File

@ -0,0 +1,39 @@
from rest_framework.test import APITestCase
from account.models import User
from django.urls import reverse
# Create your tests here.
def get_tokens_for_user(
username='sedragurda', password='sedragurdaredips19', email='sedragurda@desoz.com'):
user = User.objects.create_user(username=username, email=email, password=password)
tokens = User.create_jwt_tokens(user)
return {
"username": username,
"password": password,
"email": email,
"newsletter": True,
"user": user,
"tokens": tokens
}
class AuthorizationTests(APITestCase):
def setUp(self):
data = get_tokens_for_user()
self.username = data["username"]
self.password = data["password"]
def LoginTests(self):
data ={
'username_or_email': self.username,
'password': self.password,
'remember': True
}
response = self.client.post(reverse('auth:authorization:login'), data=data)
self.assertEqual(response.data['access_token'], self.tokens.get('access_token'))
self.assertEqual(response.data['refresh_token'], self.tokens.get('refresh_token'))

View File

@ -27,24 +27,6 @@ from utils.permissions import IsAuthenticatedAndTokenIsValid
from utils.views import JWTGenericViewMixin
# Mixins
# JWTAuthView mixin
class JWTAuthViewMixin(JWTGenericViewMixin):
"""Mixin for authentication views"""
def post(self, request, *args, **kwargs):
"""Implement POST method"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
response = Response(serializer.data, status=status.HTTP_200_OK)
access_token = serializer.data.get('access_token')
refresh_token = serializer.data.get('refresh_token')
return self._put_cookies_in_response(
cookies=self._put_data_in_cookies(access_token=access_token,
refresh_token=refresh_token),
response=response)
# OAuth2
class BaseOAuth2ViewMixin(generics.GenericAPIView):
"""BaseMixin for classic auth views"""
@ -112,6 +94,7 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin):
# Preparing request data
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
request_data = self.prepare_request_data(serializer.validated_data)
source = serializer.validated_data.get('source')
request_data.update({
@ -144,7 +127,8 @@ class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin):
status=status.HTTP_200_OK)
return self._put_cookies_in_response(
cookies=self._put_data_in_cookies(access_token=access_token,
refresh_token=refresh_token),
refresh_token=refresh_token,
permanent=True),
response=response)
@ -195,7 +179,7 @@ class ConfirmationEmailView(JWTGenericViewMixin):
# Login by username|email + password
class LoginByUsernameOrEmailView(JWTAuthViewMixin):
class LoginByUsernameOrEmailView(JWTGenericViewMixin):
"""Login by email and password"""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.LoginByUsernameOrEmailSerializer
@ -204,10 +188,12 @@ class LoginByUsernameOrEmailView(JWTAuthViewMixin):
"""Implement POST method"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
response = Response(serializer.data, status=status.HTTP_200_OK)
access_token = serializer.data.get('access_token')
refresh_token = serializer.data.get('refresh_token')
is_permanent = serializer.validated_data.get('remember')
return self._put_cookies_in_response(
cookies=self._put_data_in_cookies(access_token=access_token,
refresh_token=refresh_token,
@ -242,9 +228,11 @@ class RefreshTokenView(JWTGenericViewMixin):
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
response = Response(serializer.data, status=status.HTTP_201_CREATED)
access_token = serializer.data.get('access_token')
refresh_token = serializer.data.get('refresh_token')
return self._put_cookies_in_response(
cookies=self._put_data_in_cookies(access_token=access_token,
refresh_token=refresh_token),

8
apps/booking/admin.py Normal file
View File

@ -0,0 +1,8 @@
from django.contrib import admin
from booking.models import models
@admin.register(models.Booking)
class BookingModelAdmin(admin.ModelAdmin):
"""Model admin for model Comment"""

7
apps/booking/apps.py Normal file
View File

@ -0,0 +1,7 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class BookingConfig(AppConfig):
name = 'booking'
verbose_name = _('Booking')

View File

@ -0,0 +1,37 @@
# Generated by Django 2.2.4 on 2019-10-03 10:38
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
replaces = [('booking', '0001_initial'), ('booking', '0002_booking_user'), ('booking', '0003_auto_20190916_1533'), ('booking', '0004_auto_20190916_1646'), ('booking', '0005_auto_20190918_1308'), ('booking', '0006_booking_country_code'), ('booking', '0007_booking_booking_id'), ('booking', '0008_auto_20190919_2008'), ('booking', '0009_booking_user'), ('booking', '0010_auto_20190920_1206')]
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Booking',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('type', models.CharField(choices=[('L', 'Lastable'), ('G', 'GuestOnline')], max_length=2, verbose_name='Guestonline or Lastable')),
('booking_user_locale', models.CharField(default='en', max_length=10, verbose_name='booking locale')),
('restaurant_id', models.PositiveIntegerField(default=None, verbose_name='booking service establishment id')),
('pending_booking_id', models.TextField(default=None, verbose_name='external service pending booking')),
('booking_id', models.TextField(db_index=True, default=None, null=True, verbose_name='external service booking id')),
('user', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to=settings.AUTH_USER_MODEL, verbose_name='booking owner')),
],
options={
'abstract': False,
'verbose_name': 'Booking',
'verbose_name_plural': 'Booking',
},
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.4 on 2019-10-03 16:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('booking', '0001_initial_squashed_0010_auto_20190920_1206'),
]
operations = [
migrations.RemoveField(
model_name='booking',
name='restaurant_id',
),
migrations.AddField(
model_name='booking',
name='restaurant_id',
field=models.TextField(default=None, verbose_name='booking service establishment id'),
),
]

View File

View File

@ -0,0 +1,67 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from utils.models import ProjectBaseMixin
from booking.models.services import LastableService, GuestonlineService
from account.models import User
class BookingManager(models.QuerySet):
def by_user(self, user: User):
return self.filter(user=user)
class Booking(ProjectBaseMixin):
LASTABLE = 'L'
GUESTONLINE = 'G'
AVAILABLE_SERVICES = (
(LASTABLE, 'Lastable'),
(GUESTONLINE, 'GuestOnline')
)
type = models.CharField(max_length=2, choices=AVAILABLE_SERVICES, verbose_name=_('Guestonline or Lastable'))
restaurant_id = models.TextField(verbose_name=_('booking service establishment id'), default=None)
booking_user_locale = models.CharField(verbose_name=_('booking locale'), default='en', max_length=10)
pending_booking_id = models.TextField(verbose_name=_('external service pending booking'), default=None)
booking_id = models.TextField(verbose_name=_('external service booking id'), default=None, null=True,
db_index=True, )
user = models.ForeignKey(
'account.User', verbose_name=_('booking owner'), null=True,
related_name='bookings',
blank=True, default=None, on_delete=models.CASCADE)
objects = BookingManager.as_manager()
@property
def accept_email_spam(self):
return False
@property
def accept_sms_spam(self):
return False
@classmethod
def get_service_by_type(cls, type):
if type == cls.GUESTONLINE:
return GuestonlineService()
elif type == cls.LASTABLE:
return LastableService()
else:
return None
@classmethod
def get_booking_id_by_type(cls, establishment, type):
if type == cls.GUESTONLINE:
return establishment.guestonline_id
elif type == cls.LASTABLE:
return establishment.lastable_id
else:
return None
def delete(self, using=None, keep_parents=False):
service = self.get_service_by_type(self.type)
if not service.cancel_booking(self.booking_id):
raise serializers.ValidationError(detail='Something went wrong! Unable to cancel.')
super().delete(using, keep_parents)
class Meta:
verbose_name = _('Booking')
verbose_name_plural = _('Booking')

View File

@ -0,0 +1,166 @@
from abc import ABC, abstractmethod
import json
import requests
from django.conf import settings
from rest_framework import status
import booking.models.models as models
from rest_framework import serializers
class AbstractBookingService(ABC):
""" Abstract class for Guestonline && Lastable booking services"""
def __init__(self, service):
self.service = None
self.response = None
if service not in [models.Booking.LASTABLE, models.Booking.GUESTONLINE]:
raise Exception('Service %s is not implemented yet' % service)
self.service = service
if service == models.Booking.GUESTONLINE:
self.token = settings.GUESTONLINE_TOKEN
self.url = settings.GUESTONLINE_SERVICE
elif service == models.Booking.LASTABLE:
self.token = settings.LASTABLE_TOKEN
self.url = settings.LASTABLE_SERVICE
@staticmethod
def get_certain_keys(d: dict, keys_to_preserve: set) -> dict:
""" Helper """
return {key: d[key] for key in d.keys() & keys_to_preserve}
@abstractmethod
def check_whether_booking_available(self, restaurant_id, date):
""" checks whether booking is available """
if date is None:
raise serializers.ValidationError(detail='date query param is required')
@abstractmethod
def cancel_booking(self, payload):
""" cancels booking and returns the result """
pass
@abstractmethod
def create_booking(self, payload):
""" returns pending booking id if created. otherwise False """
pass
@abstractmethod
def update_booking(self, payload):
""" updates pending booking with contacts """
pass
@abstractmethod
def get_common_headers(self):
pass
@abstractmethod
def get_booking_details(self, payload):
""" fetches booking details from external service """
pass
class GuestonlineService(AbstractBookingService):
def __init__(self):
super().__init__(models.Booking.GUESTONLINE)
def get_common_headers(self):
return {'X-Token': self.token, 'Content-type': 'application/json', 'Accept': 'application/json'}
def check_whether_booking_available(self, restaurant_id, date: str):
super().check_whether_booking_available(restaurant_id, date)
url = f'{self.url}v1/runtime_services'
params = {'restaurant_id': restaurant_id, 'date': date, 'expands[]': 'table_availabilities'}
r = requests.get(url, headers=self.get_common_headers(), params=params)
if not status.is_success(r.status_code):
return False
response = json.loads(r.content)['runtime_services']
keys_to_preserve = {'left_seats', 'table_availabilities', 'closed', 'start_time', 'end_time', 'last_booking'}
response = map(lambda x: self.get_certain_keys(x, keys_to_preserve), response)
self.response = response
return True
def commit_booking(self, payload):
url = f'{self.url}v1/pending_bookings/{payload}/commit'
r = requests.put(url, headers=self.get_common_headers())
self.response = json.loads(r.content)
if status.is_success(r.status_code) and self.response is None:
raise serializers.ValidationError(detail='Booking already committed.')
return status.is_success(r.status_code)
def update_booking(self, payload):
booking_id = payload.pop('pending_booking_id')
url = f'{self.url}v1/pending_bookings/{booking_id}'
payload['lastname'] = payload.pop('last_name')
payload['firstname'] = payload.pop('first_name')
payload['mobile_phone'] = payload.pop('phone')
headers = self.get_common_headers()
r = requests.put(url, headers=headers, data=json.dumps({'contact_info': payload}))
return status.is_success(r.status_code)
def create_booking(self, payload):
url = f'{self.url}v1/pending_bookings'
payload['hour'] = payload.pop('booking_time')
payload['persons'] = payload.pop('booked_persons_number')
payload['date'] = payload.pop('booking_date')
r = requests.post(url, headers=self.get_common_headers(), data=json.dumps(payload))
return json.loads(r.content)['id'] if status.is_success(r.status_code) else False
def cancel_booking(self, payload):
url = f'{self.url}v1/pending_bookings/{payload}'
r = requests.delete(url, headers=self.get_common_headers())
return status.is_success(r.status_code)
def get_booking_details(self, payload):
url = f'{self.url}v1/bookings/{payload}'
r = requests.get(url, headers=self.get_common_headers())
return json.loads(r.content)
class LastableService(AbstractBookingService):
def __init__(self):
super().__init__(models.Booking.LASTABLE)
self.proxies = {
'http': settings.LASTABLE_PROXY,
'https': settings.LASTABLE_PROXY,
}
def create_booking(self, payload):
url = f'{self.url}v1/partner/orders'
payload['places'] = payload.pop('booked_persons_number')
payload['hour'] = payload.pop('booking_time')
payload['firstName'] = payload.pop('first_name')
payload['lastName'] = payload.pop('last_name')
r = requests.post(url, headers=self.get_common_headers(), proxies=self.proxies, data=json.dumps(payload))
return json.loads(r.content)['data']['_id'] if status.is_success(r.status_code) else False
def get_common_headers(self):
return {'Authorization': f'Bearer {self.token}', 'Content-type': 'application/json',
'Accept': 'application/json'}
def check_whether_booking_available(self, restaurant_id, date):
super().check_whether_booking_available(restaurant_id, date)
url = f'{self.url}v1/restaurant/{restaurant_id}/offers'
r = requests.get(url, headers=self.get_common_headers(), proxies=self.proxies)
response = json.loads(r.content).get('data')
if not status.is_success(r.status_code) or not response:
return False
self.response = response
return True
def commit_booking(self, payload):
""" Lastable service has no pending booking to commit """
return False
def update_booking(self, payload):
""" Lastable service has no pending booking to update """
return False
def cancel_booking(self, payload):
url = f'{self.url}v1/partner/orders/{payload}'
r = requests.delete(url, headers=self.get_common_headers(), proxies=self.proxies)
return status.is_success(r.status_code)
def get_booking_details(self, payload):
url = f'{self.url}v1/partner/orders/{payload}'
r = requests.get(url, headers=self.get_common_headers(), proxies=self.proxies)
return json.loads(r.content)

View File

View File

@ -0,0 +1,64 @@
from rest_framework import serializers
from booking.models import models
class BookingSerializer(serializers.ModelSerializer):
class Meta:
model = models.Booking
fields = (
'id',
'type',
)
class CheckBookingSerializer(serializers.ModelSerializer):
available = serializers.BooleanField()
type = serializers.ChoiceField(choices=models.Booking.AVAILABLE_SERVICES, allow_null=True)
details = serializers.DictField()
class Meta:
model = models.Booking
fields = (
'available',
'type',
'details',
)
class PendingBookingSerializer(serializers.ModelSerializer):
restaurant_id = serializers.CharField()
booking_id = serializers.CharField(allow_null=True, allow_blank=True)
id = serializers.ReadOnlyField()
user = serializers.ReadOnlyField()
class Meta:
model = models.Booking
fields = (
'id',
'type',
'restaurant_id',
'booking_id',
'pending_booking_id',
'user',
)
class UpdateBookingSerializer(serializers.ModelSerializer):
id = serializers.ReadOnlyField()
class Meta:
model = models.Booking
fields = ('booking_id', 'id')
class GetBookingSerializer(serializers.ModelSerializer):
details = serializers.SerializerMethodField()
def get_details(self, obj):
booking = self.instance
service = booking.get_service_by_type(booking.type)
return service.get_booking_details(booking.booking_id)
class Meta:
model = models.Booking
fields = '__all__'

14
apps/booking/urls.py Normal file
View File

@ -0,0 +1,14 @@
"""Booking app urls."""
from django.urls import path
from booking import views
app = 'booking'
urlpatterns = [
path('<int:establishment_id>/check/', views.CheckWhetherBookingAvailable.as_view(), name='booking-check'),
path('<int:establishment_id>/create/', views.CreatePendingBooking.as_view(), name='create-pending-booking'),
path('<int:pk>/', views.UpdatePendingBooking.as_view(), name='update-pending-booking'),
path('<int:pk>/cancel/', views.CancelBooking.as_view(), name='cancel-existing-booking'),
path('last/', views.LastBooking.as_view(), name='last_booking-for-authorizer-user'),
path('retrieve/<int:pk>/', views.GetBookingById.as_view(), name='retrieves-booking-by-id'),
]

118
apps/booking/views.py Normal file
View File

@ -0,0 +1,118 @@
from rest_framework import generics, permissions, status, serializers
from django.shortcuts import get_object_or_404
from establishment.models import Establishment
from booking.models.models import Booking, GuestonlineService, LastableService
from rest_framework.response import Response
from booking.serializers.web import (PendingBookingSerializer,
UpdateBookingSerializer, GetBookingSerializer, CheckBookingSerializer)
class CheckWhetherBookingAvailable(generics.GenericAPIView):
""" Checks which service to use if establishmend is managed by any """
permission_classes = (permissions.AllowAny,)
serializer_class = CheckBookingSerializer
pagination_class = None
def get(self, request, *args, **kwargs):
is_booking_available = False
establishment = get_object_or_404(Establishment, pk=kwargs['establishment_id'])
service = None
date = request.query_params.get('date')
g_service = GuestonlineService()
l_service = LastableService()
if (not establishment.lastable_id is None) and l_service \
.check_whether_booking_available(establishment.lastable_id, date):
is_booking_available = True
service = l_service
service.service_id = establishment.lastable_id
elif (not establishment.guestonline_id is None) and g_service \
.check_whether_booking_available(establishment.guestonline_id, date):
is_booking_available = True
service = g_service
service.service_id = establishment.guestonline_id
response = {
'available': is_booking_available,
'type': service.service if service else None,
}
response.update({'details': service.response} if service and service.response else {})
return Response(data=response, status=200)
class CreatePendingBooking(generics.CreateAPIView):
""" Creates pending booking """
permission_classes = (permissions.AllowAny,)
serializer_class = PendingBookingSerializer
def post(self, request, *args, **kwargs):
data = request.data.copy()
if data.get('type') == Booking.LASTABLE and data.get("offer_id") is None:
raise serializers.ValidationError(detail='Offer_id is required field for Lastable service')
establishment = get_object_or_404(Establishment, pk=kwargs['establishment_id'])
data['restaurant_id'] = Booking.get_booking_id_by_type(establishment, data.get('type'))
service = Booking.get_service_by_type(request.data.get('type'))
data['user'] = request.user.pk if request.user else None
service_to_keys = {
Booking.GUESTONLINE: {'restaurant_id', 'booking_time', 'booking_date', 'booked_persons_number', },
Booking.LASTABLE: {'booking_time', 'booked_persons_number', 'offer_id', 'email', 'phone',
'first_name', 'last_name', },
}
data['pending_booking_id'] = service.create_booking(
service.get_certain_keys(data.copy(), service_to_keys[data.get('type')]))
if not data['pending_booking_id']:
return Response(status=status.HTTP_403_FORBIDDEN, data='Unable to create booking')
data['booking_id'] = data['pending_booking_id'] if data.get('type') == Booking.LASTABLE else None
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(status=status.HTTP_201_CREATED, data=serializer.data)
class UpdatePendingBooking(generics.UpdateAPIView):
""" Update pending booking with contacts """
queryset = Booking.objects.all()
permission_classes = (permissions.AllowAny,)
serializer_class = UpdateBookingSerializer
def patch(self, request, *args, **kwargs):
instance = self.get_object()
data = request.data.copy()
service = Booking.get_service_by_type(instance.type)
data['pending_booking_id'] = instance.pending_booking_id
service.update_booking(service.get_certain_keys(data, {
'email', 'phone', 'last_name', 'first_name', 'country_code', 'pending_booking_id',
}))
service.commit_booking(data['pending_booking_id'])
data = {
'booking_id': service.response.get('id'),
'id': instance.pk,
}
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
serializer.update(instance, data)
return Response(status=status.HTTP_200_OK, data=serializer.data)
class CancelBooking(generics.DestroyAPIView):
""" Cancel existing booking """
queryset = Booking.objects.all()
permission_classes = (permissions.AllowAny,)
class LastBooking(generics.RetrieveAPIView):
""" Get last booking by user credentials """
permission_classes = (permissions.IsAuthenticated,)
serializer_class = GetBookingSerializer
lookup_field = None
def get_object(self):
return Booking.objects.by_user(self.request.user).latest('modified')
class GetBookingById(generics.RetrieveAPIView):
""" Returns booking by its id"""
permission_classes = (permissions.AllowAny,)
serializer_class = GetBookingSerializer
queryset = Booking.objects.all()

View File

@ -8,11 +8,6 @@ class CollectionAdmin(admin.ModelAdmin):
"""Collection admin."""
@admin.register(models.CollectionItem)
class CollectionItemAdmin(admin.ModelAdmin):
"""CollectionItem admin."""
@admin.register(models.Guide)
class GuideAdmin(admin.ModelAdmin):
"""Guide admin."""

View File

@ -0,0 +1,16 @@
# Generated by Django 2.2.4 on 2019-09-20 08:55
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('collection', '0008_auto_20190916_1158'),
]
operations = [
migrations.DeleteModel(
name='CollectionItem',
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-09-20 09:27
from django.db import migrations
import utils.models
class Migration(migrations.Migration):
dependencies = [
('collection', '0009_delete_collectionitem'),
]
operations = [
migrations.AddField(
model_name='collection',
name='description',
field=utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='description'),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 2.2.4 on 2019-09-20 10:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('collection', '0010_collection_description'),
]
operations = [
migrations.RemoveField(
model_name='collection',
name='image',
),
migrations.AddField(
model_name='collection',
name='image_url',
field=models.URLField(blank=True, default=None, null=True, verbose_name='Image URL path'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-09-23 13:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('collection', '0011_auto_20190920_1059'),
]
operations = [
migrations.AlterField(
model_name='guide',
name='parent',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='collection.Guide', verbose_name='parent'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-24 08:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('collection', '0012_auto_20190923_1340'),
]
operations = [
migrations.AddField(
model_name='collection',
name='slug',
field=models.SlugField(null=True, unique=True, verbose_name='Collection slug'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.4 on 2019-10-22 12:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('collection', '0013_collection_slug'),
]
operations = [
migrations.AlterField(
model_name='collection',
name='end',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='end'),
),
migrations.AlterField(
model_name='guide',
name='end',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='end'),
),
]

View File

@ -1,10 +1,12 @@
from django.contrib.postgres.fields import JSONField
from django.contrib.contenttypes.fields import ContentType
from django.contrib.contenttypes import fields as generic
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.utils.translation import gettext_lazy as _
from utils.models import ProjectBaseMixin, ImageMixin
from utils.models import ProjectBaseMixin, URLImageMixin
from utils.models import TJSONField
from utils.models import TranslatedFieldsMixin
from utils.querysets import RelatedObjectsCountMixin
# Mixins
@ -20,7 +22,8 @@ class CollectionNameMixin(models.Model):
class CollectionDateMixin(models.Model):
"""CollectionDate mixin"""
start = models.DateTimeField(_('start'))
end = models.DateTimeField(_('end'))
end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('end'))
class Meta:
"""Meta class"""
@ -28,7 +31,7 @@ class CollectionDateMixin(models.Model):
# Models
class CollectionQuerySet(models.QuerySet):
class CollectionQuerySet(RelatedObjectsCountMixin):
"""QuerySet for model Collection"""
def by_country_code(self, code):
@ -40,7 +43,8 @@ class CollectionQuerySet(models.QuerySet):
return self.filter(is_publish=True)
class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin,
TranslatedFieldsMixin, URLImageMixin):
"""Collection model."""
ORDINARY = 0 # Ordinary collection
POP = 1 # POP collection
@ -53,9 +57,6 @@ class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
collection_type = models.PositiveSmallIntegerField(choices=COLLECTION_TYPES,
default=ORDINARY,
verbose_name=_('Collection type'))
image = models.ForeignKey(
'gallery.Image', null=True, blank=True, default=None,
verbose_name=_('Collection image'), on_delete=models.CASCADE)
is_publish = models.BooleanField(
default=False, verbose_name=_('Publish status'))
on_top = models.BooleanField(
@ -65,6 +66,11 @@ class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
block_size = JSONField(
_('collection block properties'), null=True, blank=True,
default=None, help_text='{"width": "250px", "height":"250px"}')
description = TJSONField(
_('description'), null=True, blank=True,
default=None, help_text='{"en-GB":"some text"}')
slug = models.SlugField(max_length=50, unique=True,
verbose_name=_('Collection slug'), editable=True, null=True)
objects = CollectionQuerySet.as_manager()
@ -78,26 +84,6 @@ class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
return f'{self.name}'
class CollectionItemQuerySet(models.QuerySet):
"""QuerySet for model CollectionItem."""
def by_collection(self, collection_id):
"""Filter by collection id"""
return self.filter(collection=collection_id)
class CollectionItem(ProjectBaseMixin):
"""CollectionItem model."""
collection = models.ForeignKey(
Collection, verbose_name=_('collection'), on_delete=models.CASCADE)
content_type = models.ForeignKey(ContentType, default=None,
null=True, blank=True, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField(default=None, null=True, blank=True)
content_object = generic.GenericForeignKey('content_type', 'object_id')
objects = CollectionItemQuerySet.as_manager()
class GuideQuerySet(models.QuerySet):
"""QuerySet for Guide."""
@ -109,7 +95,9 @@ class GuideQuerySet(models.QuerySet):
class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
"""Guide model."""
parent = models.ForeignKey(
'self', verbose_name=_('parent'), on_delete=models.CASCADE)
'self', verbose_name=_('parent'), on_delete=models.CASCADE,
null=True, blank=True, default=None
)
advertorials = JSONField(
_('advertorials'), null=True, blank=True,
default=None, help_text='{"key":"value"}')

View File

@ -1,19 +1,32 @@
from rest_framework import serializers
from collection import models
from gallery import models as gallery_models
from location import models as location_models
class CollectionSerializer(serializers.ModelSerializer):
"""Collection serializer"""
class CollectionBaseSerializer(serializers.ModelSerializer):
"""Collection base serializer"""
# RESPONSE
image_url = serializers.ImageField(source='image.image')
description_translated = serializers.CharField(read_only=True, allow_null=True)
class Meta:
model = models.Collection
fields = [
'id',
'name',
'description_translated',
'image_url',
'slug',
]
class CollectionSerializer(CollectionBaseSerializer):
"""Collection serializer"""
# COMMON
block_size = serializers.JSONField()
is_publish = serializers.BooleanField()
on_top = serializers.BooleanField()
slug = serializers.SlugField(allow_blank=False, required=True, max_length=50)
# REQUEST
start = serializers.DateTimeField(write_only=True)
@ -21,19 +34,12 @@ class CollectionSerializer(serializers.ModelSerializer):
country = serializers.PrimaryKeyRelatedField(
queryset=location_models.Country.objects.all(),
write_only=True)
image = serializers.PrimaryKeyRelatedField(
queryset=gallery_models.Image.objects.all(),
write_only=True)
class Meta:
model = models.Collection
fields = [
'id',
'name',
fields = CollectionBaseSerializer.Meta.fields + [
'start',
'end',
'image',
'image_url',
'is_publish',
'on_top',
'country',
@ -41,18 +47,6 @@ class CollectionSerializer(serializers.ModelSerializer):
]
class CollectionItemSerializer(serializers.ModelSerializer):
"""CollectionItem serializer"""
class Meta:
model = models.CollectionItem
fields = [
'id',
'collection',
'content_type',
'object_id',
]
class GuideSerializer(serializers.ModelSerializer):
"""Guide serializer"""
class Meta:

View File

@ -1,3 +1,112 @@
from django.test import TestCase
import json
import pytz
from datetime import datetime
from http.cookies import SimpleCookie
# Create your tests here.
from rest_framework import status
from rest_framework.test import APITestCase
from account.models import User
from collection.models import Collection, Guide
from establishment.models import Establishment, EstablishmentType
from location.models import 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
tokens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token'),
'country_code': 'en'})
class CollectionListTests(BaseTestCase):
def test_collection_list_Read(self):
response = self.client.get('/api/web/collections/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
class CollectionDetailTests(BaseTestCase):
def setUp(self):
super().setUp()
# country = Country.objects.first()
# if not country:
country = Country.objects.create(
name=json.dumps({"en-GB": "Test country"}),
code="en"
)
country.save()
self.collection = Collection.objects.create(
name='Test collection',
is_publish=True,
start=datetime.now(pytz.utc),
end=datetime.now(pytz.utc),
country=country,
slug='test-collection-slug',
)
self.collection.save()
def test_collection_detail_Read(self):
response = self.client.get(f'/api/web/collections/{self.collection.slug}/establishments/?country_code=en',
format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
class CollectionGuideTests(CollectionDetailTests):
def test_guide_list_Read(self):
response = self.client.get('/api/web/collections/guides/', format='json')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
class CollectionGuideDetailTests(CollectionDetailTests):
def setUp(self):
super().setUp()
self.guide = Guide.objects.create(
collection=self.collection,
start=datetime.now(pytz.utc),
end=datetime.now(pytz.utc)
)
self.guide.save()
def test_guide_detail_Read(self):
response = self.client.get(f'/api/web/collections/guides/{self.guide.id}/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
class CollectionWebHomeTests(CollectionDetailTests):
def setUp(self):
super().setUp()
self.establishment_type = EstablishmentType.objects.create(name="Test establishment type")
for i in range(1, 5):
setattr(self, f"establishment{i}",
Establishment.objects.create(
name=f"Test establishment {i}",
establishment_type_id=self.establishment_type.id,
is_publish=True,
slug=f"test-establishment-{i}"
)
)
getattr(self, f"establishment{i}").collections.add(self.collection)
def test_collection_list_filter(self):
response = self.client.get('/api/web/collections/?country_code=en', format='json')
data = response.json()
self.assertIn('count', data)
self.assertGreater(data['count'], 0)

View File

@ -6,12 +6,10 @@ from collection.views import common as views
app_name = 'collection'
urlpatterns = [
path('', views.CollectionListView.as_view(), name='list'),
path('<int:pk>/', views.CollectionRetrieveView.as_view(), name='detail'),
path('items/', views.CollectionItemListView.as_view(), name='collection-items-list'),
path('items/<int:pk>/', views.CollectionItemRetrieveView.as_view(),
name='collection-items-detail'),
path('', views.CollectionHomePageView.as_view(), name='list'),
path('<slug:slug>/', views.CollectionDetailView.as_view(), name='detail'),
path('<slug:slug>/establishments/', views.CollectionEstablishmentListView.as_view(),
name='detail'),
path('guides/', views.GuideListView.as_view(), name='guides-list'),
path('guides/<int:pk>/', views.GuideRetrieveView.as_view(), name='guides-detail'),

View File

@ -2,6 +2,9 @@ from rest_framework import generics
from rest_framework import permissions
from collection import models
from utils.pagination import ProjectPageNumberPagination
from django.shortcuts import get_object_or_404
from establishment.serializers import EstablishmentBaseSerializer
from collection.serializers import common as serializers
@ -9,13 +12,14 @@ from collection.serializers import common as serializers
class CollectionViewMixin(generics.GenericAPIView):
"""Mixin for Collection view"""
model = models.Collection
queryset = models.Collection.objects.all()
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.CollectionSerializer
class CollectionItemViewMixin(generics.GenericAPIView):
"""Mixin for CollectionItem view"""
model = models.CollectionItem
queryset = models.CollectionItem.objects.all()
def get_queryset(self):
"""Override get_queryset method."""
return models.Collection.objects.published() \
.by_country_code(code=self.request.country_code) \
.order_by('-on_top', '-modified')
class GuideViewMixin(generics.GenericAPIView):
@ -27,35 +31,42 @@ class GuideViewMixin(generics.GenericAPIView):
# Views
# Collections
class CollectionListView(CollectionViewMixin, generics.ListAPIView):
"""List Collection view"""
pagination_class = None
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.CollectionSerializer
"""List Collection view."""
class CollectionHomePageView(CollectionListView):
"""Collection list view for home page."""
def get_queryset(self):
"""Override get_queryset method"""
return models.Collection.objects.published()\
.by_country_code(code=self.request.country_code)\
.order_by('-on_top', '-created')
"""Override get_queryset."""
return super(CollectionHomePageView, self).get_queryset() \
.filter_all_related_gt(3)
class CollectionRetrieveView(CollectionViewMixin, generics.RetrieveAPIView):
"""Retrieve Collection view"""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.CollectionSerializer
class CollectionDetailView(CollectionViewMixin, generics.RetrieveAPIView):
"""Retrieve detail of Collection instance."""
lookup_field = 'slug'
serializer_class = serializers.CollectionBaseSerializer
# CollectionItem
class CollectionItemListView(CollectionItemViewMixin, generics.ListAPIView):
"""List CollectionItem view"""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.CollectionItemSerializer
class CollectionEstablishmentListView(CollectionListView):
"""Retrieve list of establishment for collection."""
lookup_field = 'slug'
pagination_class = ProjectPageNumberPagination
serializer_class = EstablishmentBaseSerializer
def get_queryset(self):
"""
Override get_queryset method.
"""
queryset = super(CollectionEstablishmentListView, self).get_queryset()
# Perform the lookup filtering.
collection = get_object_or_404(queryset, slug=self.kwargs['slug'])
class CollectionItemRetrieveView(CollectionItemViewMixin, generics.RetrieveAPIView):
"""Retrieve CollectionItem view"""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.CollectionItemSerializer
# May raise a permission denied
self.check_object_permissions(self.request, collection)
return collection.establishments.all()
# Guide

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.4 on 2019-10-10 11:46
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('translation', '0003_auto_20190901_1032'),
('comment', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='comment',
name='language',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='translation.Language', verbose_name='Locale'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.4 on 2019-10-15 07:04
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('location', '0012_data_migrate'),
('comment', '0002_comment_language'),
]
operations = [
migrations.RemoveField(
model_name='comment',
name='language',
),
migrations.AddField(
model_name='comment',
name='country',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.Country', verbose_name='Country'),
),
]

View File

@ -6,6 +6,8 @@ from django.utils.translation import gettext_lazy as _
from account.models import User
from utils.models import ProjectBaseMixin
from utils.querysets import ContentTypeQuerySetMixin
from translation.models import Language
from location.models import Country
class CommentQuerySet(ContentTypeQuerySetMixin):
@ -41,6 +43,8 @@ class Comment(ProjectBaseMixin):
content_object = generic.GenericForeignKey('content_type', 'object_id')
objects = CommentQuerySet.as_manager()
country = models.ForeignKey(Country, verbose_name=_('Country'),
on_delete=models.SET_NULL, null=True)
class Meta:
"""Meta class"""

View File

@ -0,0 +1,9 @@
"""Comment app common serializers."""
from comment import models
from rest_framework import serializers
class CommentBaseSerializer(serializers.ModelSerializer):
class Meta:
model = models.Comment
fields = ('id', 'text', 'mark', 'user')

View File

@ -1 +1,107 @@
# Create your tests here.
from utils.tests.tests_permissions import BasePermissionTests
from rest_framework import status
from authorization.tests.tests_authorization import get_tokens_for_user
from django.urls import reverse
from django.contrib.contenttypes.models import ContentType
from http.cookies import SimpleCookie
from account.models import Role, User, UserRole
from comment.models import Comment
class CommentModeratorPermissionTests(BasePermissionTests):
def setUp(self):
super().setUp()
self.role = Role.objects.create(
role=2,
country=self.country_ru
)
self.role.save()
self.moderator = User.objects.create_user(username='moderator',
email='moderator@mail.com',
password='passwordmoderator')
self.userRole = UserRole.objects.create(
user=self.moderator,
role=self.role
)
self.userRole.save()
content_type = ContentType.objects.get(app_label='location', model='country')
self.user_test = get_tokens_for_user()
self.comment = Comment.objects.create(text='Test comment', mark=1,
user=self.user_test["user"],
object_id= self.country_ru.pk,
content_type_id=content_type.id,
country=self.country_ru
)
self.comment.save()
self.url = reverse('back:comment:comment-crud', kwargs={"id": self.comment.id})
def test_put_moderator(self):
tokens = User.create_jwt_tokens(self.moderator)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('access_token')})
data = {
"id": self.comment.id,
"text": "test text moderator",
"mark": 1,
"user": self.moderator.id
}
response = self.client.put(self.url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_get(self):
response = self.client.get(self.url, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_put_other_user(self):
other_user = User.objects.create_user(username='test',
email='test@mail.com',
password='passwordtest')
tokens = User.create_jwt_tokens(other_user)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('access_token')})
data = {
"id": self.comment.id,
"text": "test text moderator",
"mark": 1,
"user": other_user.id
}
response = self.client.put(self.url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_put_super_user(self):
super_user = User.objects.create_user(username='super',
email='super@mail.com',
password='passwordtestsuper',
is_superuser=True)
tokens = User.create_jwt_tokens(super_user)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('access_token')})
data = {
"id": self.comment.id,
"text": "test text moderator",
"mark": 1,
"user": super_user.id
}
response = self.client.put(self.url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

11
apps/comment/urls/back.py Normal file
View File

@ -0,0 +1,11 @@
"""Back comment URLs"""
from django.urls import path
from comment.views import back as views
app_name = 'comment'
urlpatterns = [
path('', views.CommentLstView.as_view(), name='comment-list-create'),
path('<int:id>/', views.CommentRUDView.as_view(), name='comment-crud'),
]

View File

@ -0,0 +1,19 @@
from rest_framework import generics, permissions
from comment.serializers import back as serializers
from comment import models
from utils.permissions import IsCommentModerator, IsCountryAdmin
class CommentLstView(generics.ListCreateAPIView):
"""Comment list create view."""
serializer_class = serializers.CommentBaseSerializer
queryset = models.Comment.objects.all()
permission_classes = [permissions.IsAuthenticatedOrReadOnly,]
class CommentRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Comment RUD view."""
serializer_class = serializers.CommentBaseSerializer
queryset = models.Comment.objects.all()
permission_classes = [IsCountryAdmin|IsCommentModerator]
lookup_field = 'id'

View File

@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from comment.models import Comment
from establishment import models
from main.models import Award, MetaDataContent
from main.models import Award
from review import models as review_models
@ -24,11 +24,6 @@ class AwardInline(GenericTabularInline):
extra = 0
class MetaDataContentInline(GenericTabularInline):
model = MetaDataContent
extra = 0
class ContactPhoneInline(admin.TabularInline):
"""Contact phone inline admin."""
model = models.ContactPhone
@ -56,8 +51,7 @@ class EstablishmentAdmin(admin.ModelAdmin):
"""Establishment admin."""
list_display = ['id', '__str__', 'image_tag', ]
inlines = [
AwardInline, MetaDataContentInline,
ContactPhoneInline, ContactEmailInline,
AwardInline, ContactPhoneInline, ContactEmailInline,
ReviewInline, CommentInline]
@ -84,4 +78,4 @@ class MenuAdmin(admin.ModelAdmin):
"""Get user's short name."""
return obj.category_translated
category_translated.short_description = _('category')
category_translated.short_description = _('category')

View File

@ -10,6 +10,10 @@ class EstablishmentFilter(filters.FilterSet):
tag_id = filters.NumberFilter(field_name='tags__metadata__id',)
award_id = filters.NumberFilter(field_name='awards__id',)
search = filters.CharFilter(method='search_text')
est_type = filters.ChoiceFilter(choices=models.EstablishmentType.INDEX_NAME_TYPES,
method='by_type')
est_subtype = filters.ChoiceFilter(choices=models.EstablishmentSubType.INDEX_NAME_TYPES,
method='by_subtype')
class Meta:
"""Meta class."""
@ -19,6 +23,8 @@ class EstablishmentFilter(filters.FilterSet):
'tag_id',
'award_id',
'search',
'est_type',
'est_subtype',
)
def search_text(self, queryset, name, value):
@ -26,3 +32,23 @@ class EstablishmentFilter(filters.FilterSet):
if value not in EMPTY_VALUES:
return queryset.search(value, locale=self.request.locale)
return queryset
def by_type(self, queryset, name, value):
return queryset.by_type(value)
def by_subtype(self, queryset, name, value):
return queryset.by_subtype(value)
class EstablishmentTypeTagFilter(filters.FilterSet):
"""Establishment tag filter set."""
type_id = filters.NumberFilter(field_name='id')
class Meta:
"""Meta class."""
model = models.EstablishmentType
fields = (
'type_id',
)

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.4 on 2019-09-16 15:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0019_establishment_is_publish'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='guestonline_id',
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='guestonline id'),
),
migrations.AddField(
model_name='establishment',
name='lastable_id',
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='lastable id'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-09-20 08:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('collection', '0009_delete_collectionitem'),
('establishment', '0023_merge_20190919_1136'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='collections',
field=models.ManyToManyField(related_name='establishments', to='collection.Collection', verbose_name='Collections'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-09-20 10:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0024_establishment_collections'),
('establishment', '0024_merge_20190919_1456'),
]
operations = [
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-20 10:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0025_merge_20190920_1012'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='preview_image_url',
field=models.URLField(blank=True, default=None, null=True, verbose_name='Preview image URL path'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-20 11:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0026_establishment_preview_image_url'),
]
operations = [
migrations.AlterField(
model_name='establishment',
name='collections',
field=models.ManyToManyField(blank=True, default=None, null=True, related_name='establishments', to='collection.Collection', verbose_name='Collections'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-20 12:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0027_auto_20190920_1120'),
]
operations = [
migrations.AlterField(
model_name='establishment',
name='collections',
field=models.ManyToManyField(blank=True, default=None, related_name='establishments', to='collection.Collection', verbose_name='Collections'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-23 09:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0028_auto_20190920_1205'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='name_transliterated',
field=models.CharField(default='', max_length=255, verbose_name='Transliterated name'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-23 13:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0029_establishment_name_transliterated'),
]
operations = [
migrations.RenameField(
model_name='establishment',
old_name='name_transliterated',
new_name='name_translated',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-09-24 08:06
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0030_auto_20190923_1340'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='slug',
field=models.SlugField(null=True, unique=True, verbose_name='Establishment slug'),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 2.2.4 on 2019-10-09 07:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('establishment', '0031_establishment_slug'),
]
operations = [
migrations.CreateModel(
name='EstablishmentTag',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
options={
'verbose_name': 'establishment tag',
'verbose_name_plural': 'establishment tags',
},
),
migrations.CreateModel(
name='EstablishmentTypeTagCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('establishment_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tag_categories', to='establishment.EstablishmentType', verbose_name='establishment type')),
],
options={
'verbose_name': 'establishment type tag categories',
'verbose_name_plural': 'establishment type tag categories',
},
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-01 15:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0020_auto_20190916_1532'),
('establishment', '0031_establishment_slug'),
]
operations = [
]

View File

@ -0,0 +1,24 @@
# Generated by Django 2.2.4 on 2019-10-03 10:40
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [('establishment', '0033_auto_20191003_0943'), ('establishment', '0034_auto_20191003_1036')]
dependencies = [
('establishment', '0032_merge_20191001_1530'),
]
operations = [
migrations.RemoveField(
model_name='establishment',
name='lastable_id',
),
migrations.AddField(
model_name='establishment',
name='lastable_id',
field=models.TextField(blank=True, default=None, null=True, unique=True, verbose_name='lastable id'),
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 2.2.4 on 2019-10-09 07:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tag', '0001_initial'),
('establishment', '0032_establishmenttag_establishmenttypetagcategory'),
]
operations = [
migrations.AddField(
model_name='establishmenttypetagcategory',
name='tag_category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='est_type_tag_categories', to='tag.TagCategory', verbose_name='tag category'),
),
migrations.AddField(
model_name='establishmenttag',
name='establishment',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tags', to='establishment.Establishment', verbose_name='establishment'),
),
migrations.AddField(
model_name='establishmenttag',
name='tag',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tags', to='tag.Tag', verbose_name='tag'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-09 14:57
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0033_auto_20191009_0715'),
('establishment', '0033_auto_20191003_0943_squashed_0034_auto_20191003_1036'),
]
operations = [
]

View File

@ -0,0 +1,27 @@
# Generated by Django 2.2.4 on 2019-10-11 10:47
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tag', '0002_auto_20191009_1408'),
('establishment', '0034_merge_20191009_1457'),
]
operations = [
migrations.CreateModel(
name='EstablishmentSubTypeTagCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('establishment_subtype', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tag_categories', to='establishment.EstablishmentSubType', verbose_name='establishment subtype')),
('tag_category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='est_subtype_tag_categories', to='tag.TagCategory', verbose_name='tag category')),
],
options={
'verbose_name': 'establishment subtype tag categories',
'verbose_name_plural': 'establishment subtype tag categories',
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-11 13:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0035_establishmentsubtypetagcategory'),
]
operations = [
migrations.AlterField(
model_name='establishment',
name='establishment_subtypes',
field=models.ManyToManyField(blank=True, related_name='subtype_establishment', to='establishment.EstablishmentSubType', verbose_name='subtype'),
),
]

View File

@ -0,0 +1,54 @@
# Generated by Django 2.2.4 on 2019-10-15 14:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0002_auto_20191009_1408'),
('establishment', '0036_auto_20191011_1356'),
]
operations = [
migrations.RemoveField(
model_name='establishmenttag',
name='establishment',
),
migrations.RemoveField(
model_name='establishmenttag',
name='tag',
),
migrations.RemoveField(
model_name='establishmenttypetagcategory',
name='establishment_type',
),
migrations.RemoveField(
model_name='establishmenttypetagcategory',
name='tag_category',
),
migrations.AddField(
model_name='establishment',
name='tags',
field=models.ManyToManyField(related_name='establishments', to='tag.Tag', verbose_name='Tag'),
),
migrations.AddField(
model_name='establishmentsubtype',
name='tag_categories',
field=models.ManyToManyField(related_name='establishment_subtypes', to='tag.TagCategory', verbose_name='Tag'),
),
migrations.AddField(
model_name='establishmenttype',
name='tag_categories',
field=models.ManyToManyField(related_name='establishment_types', to='tag.TagCategory', verbose_name='Tag'),
),
migrations.DeleteModel(
name='EstablishmentSubTypeTagCategory',
),
migrations.DeleteModel(
name='EstablishmentTag',
),
migrations.DeleteModel(
name='EstablishmentTypeTagCategory',
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 2.2.4 on 2019-10-16 11:33
from django.db import migrations, models
def fill_establishment_type(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
EstablishmentType = apps.get_model('establishment', 'EstablishmentType')
for n, et in enumerate(EstablishmentType.objects.all()):
et.index_name = f'Type {n}'
et.save()
class Migration(migrations.Migration):
dependencies = [
('establishment', '0037_auto_20191015_1404'),
]
operations = [
migrations.AddField(
model_name='establishmenttype',
name='index_name',
field=models.CharField(blank=True, db_index=True, max_length=50, null=True, unique=True, default=None, verbose_name='Index name'),
),
migrations.RunPython(fill_establishment_type, migrations.RunPython.noop),
migrations.AlterField(
model_name='establishmenttype',
name='index_name',
field=models.CharField(choices=[('restaurant', 'Restaurant'), ('artisan', 'Artisan'),
('producer', 'Producer')], db_index=True, max_length=50,
unique=True, verbose_name='Index name'),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 2.2.4 on 2019-10-18 13:47
from django.db import migrations, models
def fill_establishment_subtype(apps, schema_editor):
# We can't import the Person model directly as it may be a newer
# version than this migration expects. We use the historical version.
EstablishmentSubType = apps.get_model('establishment', 'EstablishmentSubType')
for n, et in enumerate(EstablishmentSubType.objects.all()):
et.index_name = f'Type {n}'
et.save()
class Migration(migrations.Migration):
dependencies = [
('establishment', '0038_establishmenttype_index_name'),
]
operations = [
migrations.AddField(
model_name='establishmentsubtype',
name='index_name',
field=models.CharField(blank=True, db_index=True, max_length=50, null=True, unique=True, default=None, verbose_name='Index name'),
),
migrations.RunPython(fill_establishment_subtype, migrations.RunPython.noop),
migrations.AlterField(
model_name='establishmentsubtype',
name='index_name',
field=models.CharField(choices=[('winery', 'Winery'), ], db_index=True, max_length=50,
unique=True, verbose_name='Index name'),
),
]

View File

@ -1,20 +1,23 @@
"""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, Q
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 main.models import Award
from review.models import Review
from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField, URLImageMixin,
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes)
@ -24,9 +27,26 @@ class EstablishmentType(TranslatedFieldsMixin, ProjectBaseMixin):
STR_FIELD_NAME = 'name'
# INDEX NAME CHOICES
RESTAURANT = 'restaurant'
ARTISAN = 'artisan'
PRODUCER = 'producer'
INDEX_NAME_TYPES = (
(RESTAURANT, _('Restaurant')),
(ARTISAN, _('Artisan')),
(PRODUCER, _('Producer')),
)
name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
help_text='{"en-GB":"some text"}')
index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES,
unique=True, db_index=True,
verbose_name=_('Index name'))
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='establishment_types',
verbose_name=_('Tag'))
class Meta:
"""Meta class."""
@ -48,11 +68,24 @@ class EstablishmentSubTypeManager(models.Manager):
class EstablishmentSubType(ProjectBaseMixin, TranslatedFieldsMixin):
"""Establishment type model."""
# INDEX NAME CHOICES
WINERY = 'winery'
INDEX_NAME_TYPES = (
(WINERY, _('Winery')),
)
name = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
help_text='{"en-GB":"some text"}')
index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES,
unique=True, db_index=True,
verbose_name=_('Index name'))
establishment_type = models.ForeignKey(EstablishmentType,
on_delete=models.CASCADE,
verbose_name=_('Type'))
tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='establishment_subtypes',
verbose_name=_('Tag'))
objects = EstablishmentSubTypeManager()
@ -70,6 +103,20 @@ class EstablishmentSubType(ProjectBaseMixin, TranslatedFieldsMixin):
class EstablishmentQuerySet(models.QuerySet):
"""Extended queryset for Establishment model."""
def with_base_related(self):
"""Return qs with related objects."""
return self.select_related('address', 'establishment_type').\
prefetch_related('tags')
def with_extended_related(self):
return self.select_related('establishment_type').\
prefetch_related('establishment_subtypes', 'awards', 'schedule',
'phones').\
prefetch_actual_employees()
def with_type_related(self):
return self.prefetch_related('establishment_subtypes')
def search(self, value, locale=None):
"""Search text in JSON fields."""
if locale is not None:
@ -81,6 +128,16 @@ class EstablishmentQuerySet(models.QuerySet):
else:
return self.none()
# def es_search(self, value, locale=None):
# """Search text via ElasticSearch."""
# from search_indexes.documents import EstablishmentDocument
# search = EstablishmentDocument.search().filter(
# Elastic_Q('match', name=value) |
# Elastic_Q('match', **{f'description.{locale}': value})
# ).execute()
# ids = [result.meta.id for result in search]
# return self.filter(id__in=ids)
def by_country_code(self, code):
"""Return establishments by country code"""
return self.filter(address__city__country__code=code)
@ -91,29 +148,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):
"""
@ -122,72 +170,66 @@ 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(
collections__collection__collection_type=Collection.POP,
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()))
return self.annotate(mark_similarity=ExpressionWrapper(
mark - F('intermediate_public_mark'),
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'))
def similar(self, establishment_pk: int):
def similar(self, establishment_slug: str):
"""
Return QuerySet with objects that similar to Establishment.
:param establishment_pk: integer
:param establishment_slug: str Establishment slug
"""
establishment_qs = Establishment.objects.filter(pk=establishment_pk)
establishment_qs = self.filter(slug=establishment_slug,
public_mark__isnull=False)
if establishment_qs.exists():
establishment = establishment_qs.first()
return self.exclude(pk=establishment_pk) \
.filter(is_publish=True,
image__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') \
.distinct('mark_similarity', 'id')
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(
@ -201,10 +243,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,
@ -219,16 +261,41 @@ class EstablishmentQuerySet(models.QuerySet):
:return: all establishments within the specified radius of specified point
:param unit: length unit e.g. m, km. Default is 'm'.
"""
from django.contrib.gis.measure import Distance
kwargs = {unit: radius}
return self.filter(address__coordinates__distance_lte=(center, Distance(**kwargs)))
return self.filter(address__coordinates__distance_lte=(center, DistanceMeasure(**kwargs)))
def artisans(self):
"""Return artisans."""
return self.filter(establishment_type__index_name=EstablishmentType.ARTISAN)
def producers(self):
"""Return producers."""
return self.filter(establishment_type__index_name=EstablishmentType.PRODUCER)
def restaurants(self):
"""Return restaurants."""
return self.filter(establishment_type__index_name=EstablishmentType.RESTAURANT)
def wineries(self):
"""Return wineries."""
return self.producers().filter(
establishment_subtypes__index_name=EstablishmentSubType.WINERY)
def by_type(self, value):
"""Return QuerySet with type by value."""
return self.filter(establishment_type__index_name=value)
def by_subtype(self, value):
"""Return QuerySet with subtype by value."""
return self.filter(establishment_subtypes__index_name=value)
class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
"""Establishment model."""
name = models.CharField(_('name'), max_length=255, default='')
name_translated = models.CharField(_('Transliterated name'),
max_length=255, default='')
description = TJSONField(blank=True, null=True, default=None,
verbose_name=_('description'),
help_text='{"en-GB":"some text"}')
@ -243,6 +310,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
on_delete=models.PROTECT,
verbose_name=_('type'))
establishment_subtypes = models.ManyToManyField(EstablishmentSubType,
blank=True,
related_name='subtype_establishment',
verbose_name=_('subtype'))
address = models.ForeignKey(Address, blank=True, null=True, default=None,
@ -259,6 +327,10 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
verbose_name=_('Twitter URL'))
lafourchette = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Lafourchette URL'))
guestonline_id = models.PositiveIntegerField(blank=True, verbose_name=_('guestonline id'),
null=True, default=None,)
lastable_id = models.TextField(blank=True, verbose_name=_('lastable id'), unique=True,
null=True, default=None,)
booking = models.URLField(blank=True, null=True, default=None,
verbose_name=_('Booking URL'))
is_publish = models.BooleanField(default=False, verbose_name=_('Publish status'))
@ -269,13 +341,26 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
# help_text=_('Holidays closing date from'))
# holidays_to = models.DateTimeField(verbose_name=_('Holidays to'),
# help_text=_('Holidays closing date to'))
awards = generic.GenericRelation(to='main.Award')
tags = generic.GenericRelation(to='main.MetaDataContent')
reviews = generic.GenericRelation(to='review.Review')
comments = generic.GenericRelation(to='comment.Comment')
transportation = models.TextField(blank=True, null=True, default=None,
verbose_name=_('Transportation'))
collections = generic.GenericRelation(to='collection.CollectionItem')
collections = models.ManyToManyField(to='collection.Collection',
related_name='establishments',
blank=True, default=None,
verbose_name=_('Collections'))
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)
awards = generic.GenericRelation(to='main.Award', related_query_name='establishment')
# todo: remove after data merge
# tags = generic.GenericRelation(to='main.MetaDataContent')
tags = models.ManyToManyField('tag.Tag', related_name='establishments',
verbose_name=_('Tag'))
old_tags = generic.GenericRelation(to='main.MetaDataContent')
reviews = generic.GenericRelation(to='review.Review')
comments = generic.GenericRelation(to='comment.Comment')
favorites = generic.GenericRelation(to='favorites.Favorites')
objects = EstablishmentQuerySet.as_manager()
@ -303,12 +388,12 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
return country.low_price, country.high_price
# todo: make via prefetch
@property
def subtypes(self):
return EstablishmentSubType.objects.filter(
subtype_establishment=self,
establishment_type=self.establishment_type,
establishment_type__use_subtypes=True)
# @property
# def subtypes(self):
# return EstablishmentSubType.objects.filter(
# subtype_establishment=self,
# establishment_type=self.establishment_type,
# establishment_type__use_subtypes=True)
def set_establishment_type(self, establishment_type):
self.establishment_type = establishment_type
@ -320,6 +405,12 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
raise ValidationError('Establishment type of subtype does not match')
self.establishment_subtypes.add(establishment_subtype)
@property
def vintage_year(self):
last_review = self.reviews.by_status(Review.READY).last()
if last_review:
return last_review.vintage
@property
def best_price_menu(self):
return 150
@ -333,6 +424,38 @@ 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
@property
def the_most_recent_award(self):
return Award.objects.filter(Q(establishment=self) | Q(employees__establishments=self)).latest(
field_name='vintage_year')
@property
def country_id(self):
"""
Return Country id of establishment location
"""
return self.address.country_id
@property
def establishment_id(self):
"""
Return establishment id of establishment location
"""
return self.id
class Position(BaseAttributes, TranslatedFieldsMixin):
"""Position model."""
@ -386,8 +509,8 @@ class Employee(BaseAttributes):
verbose_name=_('User'))
name = models.CharField(max_length=255, verbose_name=_('Last name'))
establishments = models.ManyToManyField(Establishment, related_name='employees',
through=EstablishmentEmployee)
awards = generic.GenericRelation(to='main.Award')
through=EstablishmentEmployee,)
awards = generic.GenericRelation(to='main.Award', related_query_name='employees')
tags = generic.GenericRelation(to='main.MetaDataContent')
class Meta:
@ -428,6 +551,7 @@ class ContactEmail(models.Model):
def __str__(self):
return f'{self.email}'
#
# class Wine(TranslatedFieldsMixin, models.Model):
# """Wine model."""
@ -466,6 +590,10 @@ class Plate(TranslatedFieldsMixin, models.Model):
menu = models.ForeignKey(
'establishment.Menu', verbose_name=_('menu'), on_delete=models.CASCADE)
@property
def establishment_id(self):
return self.menu.establishment.id
class Meta:
verbose_name = _('plate')
verbose_name_plural = _('plates')
@ -501,3 +629,4 @@ class SocialNetwork(models.Model):
def __str__(self):
return self.title

View File

@ -1,13 +1,12 @@
import json
from rest_framework import serializers
from establishment import models
from timetable.models import Timetable
from establishment.serializers import (
EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer,
ContactPhonesSerializer, SocialNetworkRelatedSerializers, EstablishmentDetailSerializer
)
ContactPhonesSerializer, SocialNetworkRelatedSerializers,
EstablishmentTypeBaseSerializer)
from main.models import Currency
from utils.decorators import with_base_attributes
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
@ -20,6 +19,8 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
phones = ContactPhonesSerializer(read_only=True, many=True, )
emails = ContactEmailsSerializer(read_only=True, many=True, )
socials = SocialNetworkRelatedSerializers(read_only=True, many=True, )
slug = serializers.SlugField(required=True, allow_blank=False, max_length=50)
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
class Meta:
model = models.Establishment
@ -34,7 +35,43 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
'type_id',
'type',
'socials',
'image_url'
'image_url',
'slug',
# TODO: check in admin filters
'is_publish',
'guestonline_id',
'lastable_id',
]
class EstablishmentRUDSerializer(EstablishmentBaseSerializer):
"""Establishment create serializer"""
type_id = serializers.PrimaryKeyRelatedField(
source='establishment_type',
queryset=models.EstablishmentType.objects.all(), write_only=True
)
phones = ContactPhonesSerializer(read_only=False, many=True, )
emails = ContactEmailsSerializer(read_only=False, many=True, )
socials = SocialNetworkRelatedSerializers(read_only=False, many=True, )
type = EstablishmentTypeBaseSerializer(source='establishment_type')
class Meta:
model = models.Establishment
fields = [
'id',
'name',
'website',
'phones',
'emails',
'price_level',
'toque_number',
'type_id',
'type',
'socials',
'image_url',
# TODO: check in admin filters
'is_publish'
]
@ -52,13 +89,15 @@ class SocialNetworkSerializers(serializers.ModelSerializer):
class PlatesSerializers(PlateSerializer):
"""Social network serializers."""
name = serializers.JSONField()
currency_id = serializers.PrimaryKeyRelatedField(
source='currency',
queryset=Currency.objects.all(), write_only=True
)
class Meta:
"""Meta class."""
model = models.Plate
fields = PlateSerializer.Meta.fields + [
'name',
@ -89,7 +128,10 @@ class ContactEmailBackSerializers(PlateSerializer):
]
# TODO: test decorator
@with_base_attributes
class EmployeeBackSerializers(serializers.ModelSerializer):
"""Social network serializers."""
class Meta:
model = models.Employee
@ -97,4 +139,4 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
'id',
'user',
'name'
]
]

View File

@ -1,16 +1,19 @@
"""Establishment serializers."""
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from comment import models as comment_models
from comment.serializers import common as comment_serializers
from establishment import models
from favorites.models import Favorites
from location.serializers import AddressSerializer
from main.models import MetaDataContent
from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer
from location.serializers import AddressBaseSerializer
from main.serializers import AwardSerializer, CurrencySerializer
from review import models as review_models
from tag.serializers import TagBaseSerializer
from timetable.serialziers import ScheduleRUDSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import ProjectModelSerializer
from utils.serializers import TranslatedField
class ContactPhonesSerializer(serializers.ModelSerializer):
@ -42,9 +45,9 @@ class SocialNetworkRelatedSerializers(serializers.ModelSerializer):
]
class PlateSerializer(serializers.ModelSerializer):
class PlateSerializer(ProjectModelSerializer):
name_translated = serializers.CharField(allow_null=True, read_only=True)
name_translated = TranslatedField()
currency = CurrencySerializer(read_only=True)
class Meta:
@ -57,9 +60,8 @@ class PlateSerializer(serializers.ModelSerializer):
]
class MenuSerializers(serializers.ModelSerializer):
class MenuSerializers(ProjectModelSerializer):
plates = PlateSerializer(read_only=True, many=True, source='plate_set')
category = serializers.JSONField()
category_translated = serializers.CharField(read_only=True)
class Meta:
@ -73,9 +75,8 @@ class MenuSerializers(serializers.ModelSerializer):
]
class MenuRUDSerializers(serializers.ModelSerializer):
class MenuRUDSerializers(ProjectModelSerializer):
plates = PlateSerializer(read_only=True, many=True, source='plate_set')
category = serializers.JSONField()
class Meta:
model = models.Menu
@ -87,30 +88,6 @@ class MenuRUDSerializers(serializers.ModelSerializer):
]
class EstablishmentTypeSerializer(serializers.ModelSerializer):
"""Serializer for EstablishmentType model."""
name_translated = serializers.CharField(allow_null=True)
class Meta:
"""Meta class."""
model = models.EstablishmentType
fields = ('id', 'name_translated')
class EstablishmentSubTypeSerializer(serializers.ModelSerializer):
"""Serializer for EstablishmentSubType models."""
name_translated = serializers.CharField(allow_null=True)
class Meta:
"""Meta class."""
model = models.EstablishmentSubType
fields = ('id', 'name_translated')
class ReviewSerializer(serializers.ModelSerializer):
"""Serializer for model Review."""
text_translated = serializers.CharField(read_only=True)
@ -123,6 +100,45 @@ class ReviewSerializer(serializers.ModelSerializer):
)
class EstablishmentTypeBaseSerializer(serializers.ModelSerializer):
"""Serializer for EstablishmentType model."""
name_translated = TranslatedField()
class Meta:
"""Meta class."""
model = models.EstablishmentType
fields = [
'id',
'name',
'name_translated',
'use_subtypes'
]
extra_kwargs = {
'name': {'write_only': True},
'use_subtypes': {'write_only': True},
}
class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer):
"""Serializer for EstablishmentSubType models."""
name_translated = TranslatedField()
class Meta:
"""Meta class."""
model = models.EstablishmentSubType
fields = [
'id',
'name',
'name_translated',
'establishment_type'
]
extra_kwargs = {
'name': {'write_only': True},
'establishment_type': {'write_only': True}
}
class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
"""Serializer for actual employees."""
@ -139,13 +155,14 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
fields = ('id', 'name', 'position_translated', 'awards', 'priority')
class EstablishmentBaseSerializer(serializers.ModelSerializer):
class EstablishmentBaseSerializer(ProjectModelSerializer):
"""Base serializer for Establishment model."""
type = EstablishmentTypeSerializer(source='establishment_type', read_only=True)
subtypes = EstablishmentSubTypeSerializer(many=True)
address = AddressSerializer()
tags = MetaDataContentSerializer(many=True)
preview_image = serializers.SerializerMethodField()
preview_image = serializers.URLField(source='preview_image_url')
slug = serializers.SlugField(allow_blank=False, required=True, max_length=50)
address = AddressBaseSerializer()
in_favorites = serializers.BooleanField(allow_null=True)
tags = TagBaseSerializer(read_only=True, many=True)
class Meta:
"""Meta class."""
@ -154,62 +171,45 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer):
fields = [
'id',
'name',
'name_translated',
'price_level',
'toque_number',
'public_mark',
'type',
'subtypes',
'slug',
'preview_image',
'in_favorites',
'address',
'tags',
]
def get_preview_image(self, obj):
"""Get preview image"""
return obj.get_full_image_url(request=self.context.get('request'),
thumbnail_key='establishment_preview')
class EstablishmentListSerializer(EstablishmentBaseSerializer):
class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
"""Serializer for Establishment model."""
# Annotated fields
in_favorites = serializers.BooleanField(allow_null=True)
class Meta:
"""Meta class."""
model = models.Establishment
fields = EstablishmentBaseSerializer.Meta.fields + [
'in_favorites',
]
class EstablishmentDetailSerializer(EstablishmentListSerializer):
"""Serializer for Establishment model."""
description_translated = serializers.CharField(allow_null=True)
description_translated = TranslatedField()
image = serializers.URLField(source='image_url')
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes')
awards = AwardSerializer(many=True)
schedule = ScheduleRUDSerializer(many=True, allow_null=True)
phones = ContactPhonesSerializer(read_only=True, many=True, )
emails = ContactEmailsSerializer(read_only=True, many=True, )
review = serializers.SerializerMethodField()
phones = ContactPhonesSerializer(read_only=True, many=True)
emails = ContactEmailsSerializer(read_only=True, many=True)
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)
preview_image = serializers.SerializerMethodField()
best_price_menu = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True)
best_price_carte = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True)
vintage_year = serializers.ReadOnlyField()
in_favorites = serializers.SerializerMethodField()
class Meta:
class Meta(EstablishmentBaseSerializer.Meta):
"""Meta class."""
model = models.Establishment
fields = EstablishmentListSerializer.Meta.fields + [
fields = EstablishmentBaseSerializer.Meta.fields + [
'description_translated',
'price_level',
'image',
'subtypes',
'type',
'awards',
'schedule',
'website',
@ -225,23 +225,9 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer):
'best_price_menu',
'best_price_carte',
'transportation',
'vintage_year',
]
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"""
@ -262,10 +248,10 @@ class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer
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)
if not establishment_qs.exists():
return serializers.ValidationError()
raise serializers.ValidationError({'detail': _('Establishment not found.')})
attrs['establishment'] = establishment_qs.first()
return attrs
@ -311,20 +297,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():
return serializers.ValidationError()
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):
@ -335,17 +323,3 @@ class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer):
})
return super().create(validated_data)
class EstablishmentTagListSerializer(serializers.ModelSerializer):
"""List establishment tag serializer."""
id = serializers.IntegerField(source='metadata.id')
label_translated = serializers.CharField(
source='metadata.label_translated', read_only=True, allow_null=True)
class Meta:
"""Meta class."""
model = MetaDataContent
fields = [
'id',
'label_translated',
]

View File

@ -1,27 +1,106 @@
import json
from rest_framework.test import APITestCase
from account.models import User
from rest_framework import status
from http.cookies import SimpleCookie
from establishment.models import Employee
from main.models import Currency
from establishment.models import Establishment, EstablishmentType, Menu
# Create your tests here.
from translation.models import Language
from account.models import Role, UserRole
from location.models import Country, Address, City, Region
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)
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')})
self.client.cookies = SimpleCookie(
{'access_token': tokkens.get('access_token'),
'refresh_token': tokkens.get('refresh_token')})
self.establishment_type = EstablishmentType.objects.create(name="Test establishment type")
# Create lang object
self.lang = Language.objects.get(
title='Russia',
locale='ru-RU'
)
self.country_ru = Country.objects.get(
name={"en-GB": "Russian"}
)
self.region = Region.objects.create(name='Moscow area', code='01',
country=self.country_ru)
self.region.save()
self.city = City.objects.create(name='Mosocow', code='01',
region=self.region, country=self.country_ru)
self.city.save()
self.address = Address.objects.create(city=self.city, street_name_1='Krasnaya',
number=2, postal_code='010100')
self.address.save()
self.role = Role.objects.create(role=Role.ESTABLISHMENT_MANAGER)
self.role.save()
self.establishment = Establishment.objects.create(
name="Test establishment",
establishment_type_id=self.establishment_type.id,
is_publish=True,
slug="test",
address=self.address
)
self.establishment.save()
self.user_role = UserRole.objects.create(user=self.user, role=self.role,
establishment=self.establishment)
self.user_role.save()
class EstablishmentBTests(BaseTestCase):
def test_establishment_CRUD(self):
params = {'page': 1, 'page_size': 1,}
response = self.client.get('/api/back/establishments/', params, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = {
'name': 'Test establishment',
'type_id': self.establishment_type.id,
'is_publish': True,
'slug': 'test-establishment-slug'
}
response = self.client.post('/api/back/establishments/', data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get(f'/api/back/establishments/{self.establishment.id}/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'name': 'Test new establishment'
}
response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/',
data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete(f'/api/back/establishments/{self.establishment.id}/',
format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
class EmployeeTests(BaseTestCase):
def test_employee_CRD(self):
def test_employee_CRUD(self):
response = self.client.get('/api/back/establishments/employees/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
@ -36,9 +115,284 @@ class EmployeeTests(BaseTestCase):
response = self.client.get('/api/back/establishments/employees/1/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
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)
response = self.client.delete('/api/back/establishments/employees/1/', format='json')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
# Class to test childs
class ChildTestCase(BaseTestCase):
def setUp(self):
super().setUp()
# Test childs
class EmailTests(ChildTestCase):
def setUp(self):
super().setUp()
def test_get(self):
response = self.client.get('/api/back/establishments/emails/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_post(self):
data = {
'email': "test@test.com",
'establishment': self.establishment.id
}
response = self.client.post('/api/back/establishments/emails/', data=data)
self.id_email = response.json()['id']
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_get_by_pk(self):
self.test_post()
response = self.client.get(f'/api/back/establishments/emails/{self.id_email}/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_patch(self):
self.test_post()
update_data = {
'email': 'testnew@test.com'
}
response = self.client.patch(f'/api/back/establishments/emails/{self.id_email}/',
data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_email_CRUD(self):
self.test_post()
response = self.client.delete(f'/api/back/establishments/emails/{self.id_email}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
class PhoneTests(ChildTestCase):
def test_phone_CRUD(self):
response = self.client.get('/api/back/establishments/phones/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = {
'phone': "+79999999999",
'establishment': self.establishment.id
}
response = self.client.post('/api/back/establishments/phones/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get('/api/back/establishments/phones/1/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'phone': '+79999999998'
}
response = self.client.patch('/api/back/establishments/phones/1/', data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete('/api/back/establishments/phones/1/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
class SocialTests(ChildTestCase):
def test_social_CRUD(self):
response = self.client.get('/api/back/establishments/socials/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = {
'title': "Test social",
'url': 'https://testsocial.com',
'establishment': self.establishment.id
}
response = self.client.post('/api/back/establishments/socials/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get('/api/back/establishments/socials/1/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'title': 'Test new social'
}
response = self.client.patch('/api/back/establishments/socials/1/', data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete('/api/back/establishments/socials/1/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
class PlateTests(ChildTestCase):
def test_plate_CRUD(self):
response = self.client.get('/api/back/establishments/plates/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
menu = Menu.objects.create(
category=json.dumps({"en-GB": "Test category"}),
establishment=self.establishment
)
currency = Currency.objects.create(name="Test currency")
data = {
'name': json.dumps({"en-GB": "Test plate"}),
'establishment': self.establishment.id,
'price': 10,
'menu': menu.id,
'currency_id': currency.id
}
response = self.client.post('/api/back/establishments/plates/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get('/api/back/establishments/plates/1/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'name': json.dumps({"en-GB": "Test new plate"})
}
response = self.client.patch('/api/back/establishments/plates/1/', data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete('/api/back/establishments/plates/1/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
class MenuTests(ChildTestCase):
def test_menu_CRUD(self):
response = self.client.get('/api/back/establishments/menus/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = {
'category': json.dumps({"en-GB": "Test category"}),
'establishment': self.establishment.id
}
response = self.client.post('/api/back/establishments/menus/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get('/api/back/establishments/menus/1/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'category': json.dumps({"en-GB": "Test new category"})
}
response = self.client.patch('/api/back/establishments/menus/1/', data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete('/api/back/establishments/menus/1/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
class EstablishmentShedulerTests(ChildTestCase):
def test_shedule_CRUD(self):
data = {
'weekday': 1
}
response = self.client.post(f'/api/back/establishments/{self.establishment.id}/schedule/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get(f'/api/back/establishments/{self.establishment.id}/schedule/1/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'weekday': 2
}
response = self.client.patch(f'/api/back/establishments/{self.establishment.id}/schedule/1/', data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
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_404_NOT_FOUND)
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

@ -9,7 +9,7 @@ app_name = 'establishment'
urlpatterns = [
path('', views.EstablishmentListCreateView.as_view(), name='list'),
path('<int:pk>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
path('<int:pk>/', views.EstablishmentRUDView.as_view(), name='detail'),
path('<int:pk>/schedule/<int:schedule_id>/', views.EstablishmentScheduleRUDView.as_view(),
name='schedule-rud'),
path('<int:pk>/schedule/', views.EstablishmentScheduleCreateView.as_view(),
@ -17,7 +17,7 @@ urlpatterns = [
path('menus/', views.MenuListCreateView.as_view(), name='menu-list'),
path('menus/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'),
path('plates/', views.PlateListCreateView.as_view(), name='plates'),
path('plates/<int:pk>/', views.PlateListCreateView.as_view(), name='plate-rud'),
path('plates/<int:pk>/', views.PlateRUDView.as_view(), name='plate-rud'),
path('socials/', views.SocialListCreateView.as_view(), name='socials'),
path('socials/<int:pk>/', views.SocialRUDView.as_view(), name='social-rud'),
path('phones/', views.PhonesListCreateView.as_view(), name='phones'),
@ -26,4 +26,8 @@ urlpatterns = [
path('emails/<int:pk>/', views.EmailRUDView.as_view(), name='emails-rud'),
path('employees/', views.EmployeeListCreateView.as_view(), name='employees'),
path('employees/<int:pk>/', views.EmployeeRUDView.as_view(), name='employees-rud'),
]
path('types/', views.EstablishmentTypeListCreateView.as_view(), name='type-list'),
path('types/<int:pk>/', views.EstablishmentTypeRUDView.as_view(), name='type-rud'),
path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'),
path('subtypes/<int:pk>/', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'),
]

View File

@ -7,14 +7,16 @@ app_name = 'establishment'
urlpatterns = [
path('', views.EstablishmentListView.as_view(), name='list'),
path('tags/', views.EstablishmentTagListView.as_view(), name='tags'),
path('<int:pk>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
path('<int:pk>/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'),
path('<int:pk>/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'),
path('<int:pk>/comments/create/', views.EstablishmentCommentCreateView.as_view(),
path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(),
name='recent-reviews'),
# path('wineries/', views.WineriesListView.as_view(), name='wineries-list'),
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('<int:pk>/comments/<int:comment_id>/', views.EstablishmentCommentRUDView.as_view(),
path('slug/<slug:slug>/comments/<int:comment_id>/', views.EstablishmentCommentRUDView.as_view(),
name='rud-comment'),
path('<int:pk>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
name='add-favorites')
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
name='add-to-favorites')
]

View File

@ -4,4 +4,4 @@ from establishment.urls.common import urlpatterns as common_urlpatterns
urlpatterns = []
urlpatterns.extend(common_urlpatterns)
urlpatterns.extend(common_urlpatterns)

View File

@ -1,28 +1,73 @@
"""Establishment app views."""
from django.shortcuts import get_object_or_404
from rest_framework import generics
from establishment import models
from establishment import serializers
from establishment.views.common import EstablishmentMixin
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
from establishment import models, serializers
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
class EstablishmentListCreateView(EstablishmentMixin, generics.ListCreateAPIView):
class EstablishmentMixinViews:
"""Establishment mixin."""
def get_queryset(self):
"""Overrided method 'get_queryset'."""
return models.Establishment.objects.published().with_base_related()
class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAPIView):
"""Establishment list/create view."""
queryset = models.Establishment.objects.all()
serializer_class = serializers.EstablishmentListCreateSerializer
permission_classes = [IsCountryAdmin|IsEstablishmentManager]
class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView):
queryset = models.Establishment.objects.all()
serializer_class = serializers.EstablishmentRUDSerializer
permission_classes = [IsCountryAdmin|IsEstablishmentManager]
class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment schedule RUD view"""
serializer_class = ScheduleRUDSerializer
def get_object(self):
"""
Returns the object the view is displaying.
"""
establishment_pk = self.kwargs['pk']
schedule_id = self.kwargs['schedule_id']
establishment = get_object_or_404(klass=models.Establishment.objects.all(),
pk=establishment_pk)
schedule = get_object_or_404(klass=establishment.schedule,
id=schedule_id)
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
self.check_object_permissions(self.request, schedule)
return schedule
class EstablishmentScheduleCreateView(generics.CreateAPIView):
"""Establishment schedule Create view"""
serializer_class = ScheduleCreateSerializer
class MenuListCreateView(generics.ListCreateAPIView):
"""Menu list create view."""
serializer_class = serializers.MenuSerializers
queryset = models.Menu.objects.all()
permission_classes = [IsEstablishmentManager]
class MenuRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Menu RUD view."""
serializer_class = serializers.MenuRUDSerializers
queryset = models.Menu.objects.all()
permission_classes = [IsEstablishmentManager]
class SocialListCreateView(generics.ListCreateAPIView):
@ -30,12 +75,14 @@ class SocialListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.SocialNetworkSerializers
queryset = models.SocialNetwork.objects.all()
pagination_class = None
permission_classes = [IsEstablishmentManager]
class SocialRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Social RUD view."""
serializer_class = serializers.SocialNetworkSerializers
queryset = models.SocialNetwork.objects.all()
permission_classes = [IsEstablishmentManager]
class PlateListCreateView(generics.ListCreateAPIView):
@ -43,12 +90,14 @@ class PlateListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.PlatesSerializers
queryset = models.Plate.objects.all()
pagination_class = None
permission_classes = [IsEstablishmentManager]
class PlateRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Social RUD view."""
serializer_class = serializers.PlatesSerializers
queryset = models.Plate.objects.all()
permission_classes = [IsEstablishmentManager]
class PhonesListCreateView(generics.ListCreateAPIView):
@ -56,12 +105,14 @@ class PhonesListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.ContactPhoneBackSerializers
queryset = models.ContactPhone.objects.all()
pagination_class = None
permission_classes = [IsEstablishmentManager]
class PhonesRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Social RUD view."""
serializer_class = serializers.ContactPhoneBackSerializers
queryset = models.ContactPhone.objects.all()
permission_classes = [IsEstablishmentManager]
class EmailListCreateView(generics.ListCreateAPIView):
@ -69,12 +120,14 @@ class EmailListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.ContactEmailBackSerializers
queryset = models.ContactEmail.objects.all()
pagination_class = None
permission_classes = [IsEstablishmentManager]
class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Social RUD view."""
serializer_class = serializers.ContactEmailBackSerializers
queryset = models.ContactEmail.objects.all()
permission_classes = [IsEstablishmentManager]
class EmployeeListCreateView(generics.ListCreateAPIView):
@ -83,7 +136,34 @@ class EmployeeListCreateView(generics.ListCreateAPIView):
queryset = models.Employee.objects.all()
pagination_class = None
class EmployeeRUDView(generics.RetrieveDestroyAPIView):
class EmployeeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Social RUD view."""
serializer_class = serializers.EmployeeBackSerializers
queryset = models.Employee.objects.all()
queryset = models.Employee.objects.all()
class EstablishmentTypeListCreateView(generics.ListCreateAPIView):
"""Establishment type list/create view."""
serializer_class = serializers.EstablishmentTypeBaseSerializer
queryset = models.EstablishmentType.objects.all()
pagination_class = None
class EstablishmentTypeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment type retrieve/update/destroy view."""
serializer_class = serializers.EstablishmentTypeBaseSerializer
queryset = models.EstablishmentType.objects.all()
class EstablishmentSubtypeListCreateView(generics.ListCreateAPIView):
"""Establishment subtype list/create view."""
serializer_class = serializers.EstablishmentSubTypeBaseSerializer
queryset = models.EstablishmentSubType.objects.all()
pagination_class = None
class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment subtype retrieve/update/destroy view."""
serializer_class = serializers.EstablishmentSubTypeBaseSerializer
queryset = models.EstablishmentSubType.objects.all()

View File

@ -1,16 +0,0 @@
"""Establishment app views."""
from rest_framework import permissions
from establishment import models
class EstablishmentMixin:
"""Establishment mixin."""
permission_classes = (permissions.AllowAny,)
def get_queryset(self):
"""Overrided method 'get_queryset'."""
return models.Establishment.objects.published() \
.prefetch_actual_employees()

View File

@ -1,68 +1,113 @@
"""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
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
from utils.permissions import IsCountryAdmin
class EstablishmentMixinView:
"""Establishment mixin."""
permission_classes = (permissions.AllowAny,)
def get_queryset(self):
"""Overridden method 'get_queryset'."""
return models.Establishment.objects.published() \
.with_base_related() \
.annotate_in_favorites(self.request.user)
class EstablishmentListView(EstablishmentMixin, generics.ListAPIView):
class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
"""Resource for getting a list of establishments."""
serializer_class = serializers.EstablishmentListSerializer
filter_class = filters.EstablishmentFilter
serializer_class = serializers.EstablishmentBaseSerializer
def get_queryset(self):
"""Overridden method 'get_queryset'."""
qs = super(EstablishmentListView, self).get_queryset()
return qs.by_country_code(code=self.request.country_code) \
.annotate_in_favorites(user=self.request.user)
if self.request.country_code:
qs = qs.by_country_code(self.request.country_code)
return qs
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
"""Resource for getting a establishment."""
lookup_field = 'slug'
serializer_class = serializers.EstablishmentDetailSerializer
def get_queryset(self):
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
serializer_class = serializers.EstablishmentBaseSerializer
pagination_class = EstablishmentPortionPagination
def get_queryset(self):
"""Override get_queryset method"""
qs = super().get_queryset()
return qs.similar(establishment_pk=self.kwargs.get('pk'))\
.order_by('-total_mark')[:13]
class EstablishmentRetrieveView(EstablishmentMixin, generics.RetrieveAPIView):
"""Resource for getting a establishment."""
serializer_class = serializers.EstablishmentDetailSerializer
return qs.similar(establishment_slug=self.kwargs.get('slug'))
class EstablishmentTypeListView(generics.ListAPIView):
"""Resource for getting a list of establishment types."""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentTypeSerializer
serializer_class = serializers.EstablishmentTypeBaseSerializer
queryset = models.EstablishmentType.objects.all()
class EstablishmentCommentCreateView(generics.CreateAPIView):
"""View for create new comment."""
lookup_field = 'slug'
serializer_class = serializers.EstablishmentCommentCreateSerializer
queryset = comment_models.Comment.objects.all()
class EstablishmentCommentListView(generics.ListAPIView):
"""View for return list of establishment comments."""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentCommentCreateSerializer
def get_queryset(self):
"""Override get_queryset method"""
establishment = get_object_or_404(models.Establishment, slug=self.kwargs['slug'])
return comment_models.Comment.objects.by_content_type(app_label='establishment',
model='establishment')\
.by_object_id(object_id=self.kwargs.get('pk'))\
.by_object_id(object_id=establishment.pk)\
.order_by('-created')
@ -76,17 +121,9 @@ class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
Returns the object the view is displaying.
"""
queryset = self.filter_queryset(self.get_queryset())
lookup_url_kwargs = ('pk', 'comment_id')
assert lookup_url_kwargs not in self.kwargs.keys(), (
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwargs)
)
establishment_obj = get_object_or_404(queryset,
pk=self.kwargs['pk'])
slug=self.kwargs['slug'])
comment_obj = get_object_or_404(establishment_obj.comments.by_user(self.request.user),
pk=self.kwargs['comment_id'])
@ -99,93 +136,52 @@ class EstablishmentCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPIView):
"""View for create/destroy establishment from favorites."""
serializer_class = serializers.EstablishmentFavoritesCreateSerializer
lookup_field = 'slug'
def get_object(self):
"""
Returns the object the view is displaying.
"""
lookup_url_kwargs = ('pk',)
assert lookup_url_kwargs not in self.kwargs.keys(), (
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwargs)
)
establishment_obj = get_object_or_404(models.Establishment,
slug=self.kwargs['slug'])
obj = get_object_or_404(
self.request.user.favorites.by_user(user=self.request.user)
.by_content_type(app_label='establishment',
self.request.user.favorites.by_content_type(app_label='establishment',
model='establishment')
.by_object_id(object_id=self.kwargs['pk']))
.by_object_id(object_id=establishment_obj.pk))
# May raise a permission denied
self.check_object_permissions(self.request, obj)
return obj
class EstablishmentNearestRetrieveView(EstablishmentMixin, generics.ListAPIView):
class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView):
"""Resource for getting list of nearest establishments."""
serializer_class = serializers.EstablishmentListSerializer
serializer_class = serializers.EstablishmentBaseSerializer
filter_class = filters.EstablishmentFilter
def get_queryset(self):
"""Overrided method 'get_queryset'."""
from django.contrib.gis.geos import Point
"""Overridden method 'get_queryset'."""
# todo: latitude and longitude
lat = self.request.query_params.get('lat')
lon = self.request.query_params.get('lon')
radius = self.request.query_params.get('radius')
unit = self.request.query_params.get('unit')
center = Point(float(self.request.query_params["lat"]), float(self.request.query_params["lon"]))
radius = float(self.request.query_params["radius"])
unit = self.request.query_params.get("unit", None)
by_distance_from_point_kwargs = {"center": center, "radius": radius, "unit": unit}
return super(EstablishmentNearestRetrieveView, self).get_queryset() \
.by_distance_from_point(**{k: v for k, v in by_distance_from_point_kwargs.items() if v is not None}) \
.by_country_code(code=self.request.country_code) \
.annotate_in_favorites(user=self.request.user)
qs = super(EstablishmentNearestRetrieveView, self).get_queryset()
if lat and lon and radius and unit:
center = Point(float(lat), float(lon))
filter_kwargs = {'center': center, 'radius': float(radius), 'unit': unit}
return qs.by_distance_from_point(**{k: v for k, v in filter_kwargs.items()
if v is not None})
return qs
class EstablishmentTagListView(generics.ListAPIView):
"""List view for establishment tags."""
serializer_class = serializers.EstablishmentTagListSerializer
permission_classes = (permissions.AllowAny,)
pagination_class = None
def get_queryset(self):
"""Override get_queryset method"""
return MetaDataContent.objects.by_content_type(app_label='establishment',
model='establishment')\
.distinct('metadata__label')
class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment schedule RUD view"""
serializer_class = ScheduleRUDSerializer
def get_object(self):
"""
Returns the object the view is displaying.
"""
lookup_url_kwargs = ('pk', 'schedule_id')
assert lookup_url_kwargs not in self.kwargs.keys(), (
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
(self.__class__.__name__, lookup_url_kwargs)
)
establishment_pk = self.kwargs['pk']
schedule_id = self.kwargs['schedule_id']
establishment = get_object_or_404(klass=models.Establishment.objects.all(),
pk=establishment_pk)
schedule = get_object_or_404(klass=establishment.schedule,
id=schedule_id)
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
self.check_object_permissions(self.request, schedule)
return schedule
class EstablishmentScheduleCreateView(generics.CreateAPIView):
"""Establishment schedule Create view"""
serializer_class = ScheduleCreateSerializer
# Wineries
# todo: find out about difference between subtypes data
# class WineriesListView(EstablishmentListView):
# """Return list establishments with type Wineries"""
#
# def get_queryset(self):
# """Overridden get_queryset method."""
# qs = super(WineriesListView, self).get_queryset()
# return qs.with_type_related().wineries()

View File

@ -2,16 +2,3 @@
from .models import Favorites
from rest_framework import serializers
from establishment.serializers import EstablishmentBaseSerializer
class FavoritesEstablishmentListSerializer(serializers.ModelSerializer):
"""Serializer for model Favorites"""
detail = EstablishmentBaseSerializer(source='content_object')
class Meta:
"""Meta class."""
model = Favorites
fields = (
'id',
'detail',
)

View File

@ -1 +1,54 @@
# Create your tests here.
from http.cookies import SimpleCookie
from django.contrib.contenttypes.models import ContentType
from rest_framework.test import APITestCase
from rest_framework import status
from account.models import User
from favorites.models import Favorites
from establishment.models import Establishment, EstablishmentType, EstablishmentSubType
from news.models import NewsType, News
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)
tokkens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie({'access_token': tokkens.get('access_token'),
'refresh_token': tokkens.get('refresh_token')})
self.test_news_type = NewsType.objects.create(name="Test news type")
self.test_news = News.objects.create(created_by=self.user, modified_by=self.user, title={"en-GB": "Test news"},
news_type=self.test_news_type,
description={"en-GB": "Description test news"},
playlist=1, start="2020-12-03 12:00:00", end="2020-12-13 12:00:00",
state=News.PUBLISHED, slug='test-news')
self.test_content_type = ContentType.objects.get(app_label="news", model="news")
self.test_favorites = Favorites.objects.create(user=self.user, content_type=self.test_content_type,
object_id=self.test_news.id)
self.test_establishment_type = EstablishmentType.objects.create(name={"en-GB": "test establishment type"},
use_subtypes=False)
self.test_establishment = Establishment.objects.create(name="test establishment",
description={"en-GB": "description of test establishment"},
establishment_type=self.test_establishment_type,
is_publish=True)
# value for GenericRelation(reverse side) field must be iterable
# value for GenericRelation(reverse side) field must be assigned through "set" method of field
self.test_establishment.favorites.set([self.test_favorites])
class FavoritesTestCase(BaseTestCase):
def test_func(self):
response = self.client.get("/api/web/favorites/establishments/")
print(response.json())
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -8,5 +8,4 @@ app_name = 'favorites'
urlpatterns = [
path('establishments/', views.FavoritesEstablishmentListView.as_view(),
name='establishment-list'),
path('remove/<int:pk>/', views.FavoritesDestroyView.as_view(), name='remove-from-favorites'),
]

View File

@ -1,25 +1,24 @@
"""Views for app favorites."""
from rest_framework import generics
from .serializers import FavoritesEstablishmentListSerializer
from establishment.models import Establishment
from establishment.serializers import EstablishmentBaseSerializer
from .models import Favorites
class FavoritesBaseView(generics.GenericAPIView):
"""Base view for Favorites."""
def get_queryset(self):
"""Override get_queryset method."""
return Favorites.objects.by_user(self.request.user)
class FavoritesEstablishmentListView(FavoritesBaseView, generics.ListAPIView):
class FavoritesEstablishmentListView(generics.ListAPIView):
"""List views for favorites"""
serializer_class = FavoritesEstablishmentListSerializer
serializer_class = EstablishmentBaseSerializer
def get_queryset(self):
"""Override get_queryset method"""
return super().get_queryset().by_content_type(app_label='establishment',
model='establishment')
class FavoritesDestroyView(FavoritesBaseView, generics.DestroyAPIView):
"""Destroy view for favorites"""
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

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-10-10 12:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('translation', '0003_auto_20190901_1032'),
('location', '0010_auto_20190904_0711'),
]
operations = [
migrations.AddField(
model_name='country',
name='languages',
field=models.ManyToManyField(to='translation.Language', verbose_name='Languages'),
),
]

View File

@ -0,0 +1,25 @@
from django.db import migrations, connection
import os
class Migration(migrations.Migration):
# Check migration
def load_data_from_sql(apps, schema_editor):
file_path = os.path.join(os.path.dirname(__file__), 'migrate_lang.sql')
sql_statement = open(file_path).read()
with connection.cursor() as c:
c.execute(sql_statement)
def revert_data(apps, schema_editor):
file_path = os.path.join(os.path.dirname(__file__), 'remigrate_lang.sql')
sql_statement = open(file_path).read()
with connection.cursor() as c:
c.execute(sql_statement)
dependencies = [
('location', '0011_country_languages'),
]
operations = [
migrations.RunPython(load_data_from_sql, revert_data),
]

View File

@ -0,0 +1,382 @@
SET search_path TO gm, public;
CREATE TABLE codelang (
code varchar(100) NULL,
country varchar(10000) NULL
);
INSERT INTO codelang (code,country) VALUES
('af','Afrikaans')
,('af-ZA','Afrikaans (South Africa)')
,('ar','Arabic')
,('ar-AE','Arabic (U.A.E.)')
,('ar-BH','Arabic (Bahrain)')
,('ar-DZ','Arabic (Algeria)')
,('ar-EG','Arabic (Egypt)')
,('ar-IQ','Arabic (Iraq)')
,('ar-JO','Arabic (Jordan)')
,('ar-KW','Arabic (Kuwait)')
;
INSERT INTO codelang (code,country) VALUES
('ar-LB','Arabic (Lebanon)')
,('ar-LY','Arabic (Libya)')
,('ar-MA','Arabic (Morocco)')
,('ar-OM','Arabic (Oman)')
,('ar-QA','Arabic (Qatar)')
,('ar-SA','Arabic (Saudi Arabia)')
,('ar-SY','Arabic (Syria)')
,('ar-TN','Arabic (Tunisia)')
,('ar-YE','Arabic (Yemen)')
,('az','Azeri (Latin)')
;
INSERT INTO codelang (code,country) VALUES
('az-AZ','Azeri (Latin) (Azerbaijan)')
,('az-AZ','Azeri (Cyrillic) (Azerbaijan)')
,('be','Belarusian')
,('be-BY','Belarusian (Belarus)')
,('bg','Bulgarian')
,('bg-BG','Bulgarian (Bulgaria)')
,('bs-BA','Bosnian (Bosnia and Herzegovina)')
,('ca','Catalan')
,('ca-ES','Catalan (Spain)')
,('cs','Czech')
;
INSERT INTO codelang (code,country) VALUES
('cs-CZ','Czech (Czech Republic)')
,('cy','Welsh')
,('cy-GB','Welsh (United Kingdom)')
,('da','Danish')
,('da-DK','Danish (Denmark)')
,('de','German')
,('de-AT','German (Austria)')
,('de-CH','German (Switzerland)')
,('de-DE','German (Germany)')
,('de-LI','German (Liechtenstein)')
;
INSERT INTO codelang (code,country) VALUES
('de-LU','German (Luxembourg)')
,('dv','Divehi')
,('dv-MV','Divehi (Maldives)')
,('el','Greek')
,('el-GR','Greek (Greece)')
,('en','English')
,('en-AU','English (Australia)')
,('en-BZ','English (Belize)')
,('en-CA','English (Canada)')
,('en-CB','English (Caribbean)')
;
INSERT INTO codelang (code,country) VALUES
('en-GB','English (United Kingdom)')
,('en-IE','English (Ireland)')
,('en-JM','English (Jamaica)')
,('en-NZ','English (New Zealand)')
,('en-PH','English (Republic of the Philippines)')
,('en-TT','English (Trinidad and Tobago)')
,('en-US','English (United States)')
,('en-ZA','English (South Africa)')
,('en-ZW','English (Zimbabwe)')
,('eo','Esperanto')
;
INSERT INTO codelang (code,country) VALUES
('es','Spanish')
,('es-AR','Spanish (Argentina)')
,('es-BO','Spanish (Bolivia)')
,('es-CL','Spanish (Chile)')
,('es-CO','Spanish (Colombia)')
,('es-CR','Spanish (Costa Rica)')
,('es-DO','Spanish (Dominican Republic)')
,('es-EC','Spanish (Ecuador)')
,('es-ES','Spanish (Spain)')
;
INSERT INTO codelang (code,country) VALUES
('es-GT','Spanish (Guatemala)')
,('es-HN','Spanish (Honduras)')
,('es-MX','Spanish (Mexico)')
,('es-NI','Spanish (Nicaragua)')
,('es-PA','Spanish (Panama)')
,('es-PE','Spanish (Peru)')
,('es-PR','Spanish (Puerto Rico)')
,('es-PY','Spanish (Paraguay)')
,('es-SV','Spanish (El Salvador)')
,('es-UY','Spanish (Uruguay)')
;
INSERT INTO codelang (code,country) VALUES
('es-VE','Spanish (Venezuela)')
,('et','Estonian')
,('et-EE','Estonian (Estonia)')
,('eu','Basque')
,('eu-ES','Basque (Spain)')
,('fa','Farsi')
,('fa-IR','Farsi (Iran)')
,('fi','Finnish')
,('fi-FI','Finnish (Finland)')
,('fo','Faroese')
;
INSERT INTO codelang (code,country) VALUES
('fo-FO','Faroese (Faroe Islands)')
,('fr','French')
,('fr-BE','French (Belgium)')
,('fr-CA','French (Canada)')
,('fr-CH','French (Switzerland)')
,('fr-FR','French (France)')
,('fr-LU','French (Luxembourg)')
,('fr-MC','French (Principality of Monaco)')
,('gl','Galician')
,('gl-ES','Galician (Spain)')
;
INSERT INTO codelang (code,country) VALUES
('gu','Gujarati')
,('gu-IN','Gujarati (India)')
,('he','Hebrew')
,('he-IL','Hebrew (Israel)')
,('hi','Hindi')
,('hi-IN','Hindi (India)')
,('hr','Croatian')
,('hr-BA','Croatian (Bosnia and Herzegovina)')
,('hr-HR','Croatian (Croatia)')
,('hu','Hungarian')
;
INSERT INTO codelang (code,country) VALUES
('hu-HU','Hungarian (Hungary)')
,('hy','Armenian')
,('hy-AM','Armenian (Armenia)')
,('id','Indonesian')
,('id-ID','Indonesian (Indonesia)')
,('is','Icelandic')
,('is-IS','Icelandic (Iceland)')
,('it','Italian')
,('it-CH','Italian (Switzerland)')
,('it-IT','Italian (Italy)')
;
INSERT INTO codelang (code,country) VALUES
('ja','Japanese')
,('ja-JP','Japanese (Japan)')
,('ka','Georgian')
,('ka-GE','Georgian (Georgia)')
,('kk','Kazakh')
,('kk-KZ','Kazakh (Kazakhstan)')
,('kn','Kannada')
,('kn-IN','Kannada (India)')
,('ko','Korean')
,('ko-KR','Korean (Korea)')
;
INSERT INTO codelang (code,country) VALUES
('kok','Konkani')
,('kok-IN','Konkani (India)')
,('ky','Kyrgyz')
,('ky-KG','Kyrgyz (Kyrgyzstan)')
,('lt','Lithuanian')
,('lt-LT','Lithuanian (Lithuania)')
,('lv','Latvian')
,('lv-LV','Latvian (Latvia)')
,('mi','Maori')
,('mi-NZ','Maori (New Zealand)')
;
INSERT INTO codelang (code,country) VALUES
('mk','FYRO Macedonian')
,('mk-MK','FYRO Macedonian (Former Yugoslav Republic of Macedonia)')
,('mn','Mongolian')
,('mn-MN','Mongolian (Mongolia)')
,('mr','Marathi')
,('mr-IN','Marathi (India)')
,('ms','Malay')
,('ms-BN','Malay (Brunei Darussalam)')
,('ms-MY','Malay (Malaysia)')
,('mt','Maltese')
;
INSERT INTO codelang (code,country) VALUES
('mt-MT','Maltese (Malta)')
,('nb','Norwegian (Bokm?l)')
,('nb-NO','Norwegian (Bokm?l) (Norway)')
,('nl','Dutch')
,('nl-BE','Dutch (Belgium)')
,('nl-NL','Dutch (Netherlands)')
,('nn-NO','Norwegian (Nynorsk) (Norway)')
,('ns','Northern Sotho')
,('ns-ZA','Northern Sotho (South Africa)')
,('pa','Punjabi')
;
INSERT INTO codelang (code,country) VALUES
('pa-IN','Punjabi (India)')
,('pl','Polish')
,('pl-PL','Polish (Poland)')
,('ps','Pashto')
,('ps-AR','Pashto (Afghanistan)')
,('pt','Portuguese')
,('pt-BR','Portuguese (Brazil)')
,('pt-PT','Portuguese (Portugal)')
,('qu','Quechua')
,('qu-BO','Quechua (Bolivia)')
;
INSERT INTO codelang (code,country) VALUES
('qu-EC','Quechua (Ecuador)')
,('qu-PE','Quechua (Peru)')
,('ro','Romanian')
,('ro-RO','Romanian (Romania)')
,('ru','Russian')
,('ru-RU','Russian (Russia)')
,('sa','Sanskrit')
,('sa-IN','Sanskrit (India)')
,('se','Sami (Northern)')
,('se-FI','Sami (Northern) (Finland)')
;
INSERT INTO codelang (code,country) VALUES
('se-FI','Sami (Skolt) (Finland)')
,('se-FI','Sami (Inari) (Finland)')
,('se-NO','Sami (Northern) (Norway)')
,('se-NO','Sami (Lule) (Norway)')
,('se-NO','Sami (Southern) (Norway)')
,('se-SE','Sami (Northern) (Sweden)')
,('se-SE','Sami (Lule) (Sweden)')
,('se-SE','Sami (Southern) (Sweden)')
,('sk','Slovak')
,('sk-SK','Slovak (Slovakia)')
;
INSERT INTO codelang (code,country) VALUES
('sl','Slovenian')
,('sl-SI','Slovenian (Slovenia)')
,('sq','Albanian')
,('sq-AL','Albanian (Albania)')
,('sr-BA','Serbian (Latin) (Bosnia and Herzegovina)')
,('sr-BA','Serbian (Cyrillic) (Bosnia and Herzegovina)')
,('sr-SP','Serbian (Latin) (Serbia and Montenegro)')
,('sr-SP','Serbian (Cyrillic) (Serbia and Montenegro)')
,('sv','Swedish')
,('sv-FI','Swedish (Finland)')
;
INSERT INTO codelang (code,country) VALUES
('sv-SE','Swedish (Sweden)')
,('sw','Swahili')
,('sw-KE','Swahili (Kenya)')
,('syr','Syriac')
,('syr-SY','Syriac (Syria)')
,('ta','Tamil')
,('ta-IN','Tamil (India)')
,('te','Telugu')
,('te-IN','Telugu (India)')
,('th','Thai')
;
INSERT INTO codelang (code,country) VALUES
('th-TH','Thai (Thailand)')
,('tl','Tagalog')
,('tl-PH','Tagalog (Philippines)')
,('tn','Tswana')
,('tn-ZA','Tswana (South Africa)')
,('tr','Turkish')
,('tr-TR','Turkish (Turkey)')
,('tt','Tatar')
,('tt-RU','Tatar (Russia)')
,('ts','Tsonga')
;
INSERT INTO codelang (code,country) VALUES
('uk','Ukrainian')
,('uk-UA','Ukrainian (Ukraine)')
,('ur','Urdu')
,('ur-PK','Urdu (Islamic Republic of Pakistan)')
,('uz','Uzbek (Latin)')
,('uz-UZ','Uzbek (Latin) (Uzbekistan)')
,('uz-UZ','Uzbek (Cyrillic) (Uzbekistan)')
,('vi','Vietnamese')
,('vi-VN','Vietnamese (Viet Nam)')
,('xh','Xhosa')
;
INSERT INTO codelang (code,country) VALUES
('xh-ZA','Xhosa (South Africa)')
,('zh','Chinese')
,('zh-CN','Chinese (S)')
,('zh-HK','Chinese (Hong Kong)')
,('zh-MO','Chinese (Macau)')
,('zh-SG','Chinese (Singapore)')
,('zh-TW','Chinese (T)')
,('zu','Zulu')
,('zu-ZA','Zulu (South Africa)')
;
/***************************/
-- Manual migrate
CREATE TABLE country_code (
code varchar(100) NULL,
country varchar(10000) NULL
);
insert into country_code(code, country)
select distinct
t.code,
coalesce(
case when length(t.country_name2) = 1 then null else t.country_name2 end,
case when length(t.contry_name1) = 1 then null else t.contry_name1 end,
t.country
) as country
from
(
select trim(c.code) as code,
substring(trim(c.country) from '\((.+)\)') as contry_name1,
substring(
substring(trim(c.country) from '\((.+)\)')
from '\((.*)$') as country_name2,
trim(c.country) as country
from codelang as c
) t;
commit;
--delete from location_country as lc
INSERT INTO location_country
(code, "name", low_price, high_price, created, modified)
select distinct
lpad((row_number() over (order by t.country asc))::text, 3, '0') as code,
jsonb_build_object('en-GB', t.country),
0 as low_price,
100 as high_price,
now() as created,
now() as modified
from
(
select
distinct c.country
from country_code c
) t
;
commit;
--delete from translation_language as tl;
INSERT INTO translation_language
(title, locale)
select
distinct
t.country as title,
t.code as locale
from
(
select
distinct c.country, c.code
from country_code c
) t
;
commit;
--delete from location_country_languages
INSERT INTO location_country_languages
(country_id, language_id)
select lc.id as country_id,
l.id as language_id
from location_country as lc
join (
select tl.*, '"'||tl.title||'"' as country
from translation_language as tl
) l on l.country = (lc."name"::json->'en-GB')::text
;
commit;
drop table country_code;
drop table codelang;
commit;

Some files were not shown because too many files have changed in this diff Show More