Merge branch 'develop' into origin/feature/migrate-etablishment

# Conflicts:
#	apps/establishment/transfer_data.py
#	apps/transfer/management/commands/transfer.py
This commit is contained in:
littlewolf 2019-10-31 07:44:07 +03:00
commit 8f18320171
98 changed files with 2029 additions and 385 deletions

View File

@ -243,6 +243,20 @@ class User(AbstractUser):
template_name=settings.CHANGE_EMAIL_TEMPLATE,
context=context)
@property
def favorite_establishment_ids(self):
"""Return establishment IDs that in favorites for current user."""
return self.favorites.by_content_type(app_label='establishment',
model='establishment')\
.values_list('object_id', flat=True)
@property
def favorite_recipe_ids(self):
"""Return recipe IDs that in favorites for current user."""
return self.favorites.by_content_type(app_label='recipe',
model='recipe')\
.values_list('object_id', flat=True)
class UserRole(ProjectBaseMixin):
"""UserRole model."""

View File

@ -1,10 +1,13 @@
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 django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from account.models import Role, User
from authorization.tests.tests_authorization import get_tokens_for_user
from location.models import Country
from account.models import Role, User, UserRole
class RoleTests(APITestCase):
def setUp(self):
@ -65,9 +68,11 @@ class UserRoleTests(APITestCase):
)
self.role.save()
self.user_test = User.objects.create_user(username='test',
email='testemail@mail.com',
password='passwordtest')
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')

View File

@ -1,9 +1,11 @@
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
from rest_framework import status
from rest_framework.test import APITestCase
from account.models import User
from authorization.tests.tests_authorization import get_tokens_for_user
class AccountUserTests(APITestCase):
@ -62,7 +64,7 @@ class AccountChangePasswordTests(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
class AccountChangePasswordTests(APITestCase):
class AccountConfirmEmail(APITestCase):
def setUp(self):
self.data = get_tokens_for_user()

View File

@ -1,7 +1,7 @@
from rest_framework.test import APITestCase
from account.models import User
from django.urls import reverse
# Create your tests here.
from rest_framework.test import APITestCase
from account.models import User
def get_tokens_for_user(
@ -28,7 +28,7 @@ class AuthorizationTests(APITestCase):
self.password = data["password"]
def LoginTests(self):
data ={
data = {
'username_or_email': self.username,
'password': self.password,
'remember': True

View File

@ -6,4 +6,4 @@ from rest_framework import serializers
class CommentBaseSerializer(serializers.ModelSerializer):
class Meta:
model = models.Comment
fields = ('id', 'text', 'mark', 'user')
fields = ('id', 'text', 'mark', 'user', 'object_id', 'content_type')

View File

@ -1,11 +1,13 @@
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 django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from rest_framework import status
from account.models import Role, User, UserRole
from authorization.tests.tests_authorization import get_tokens_for_user
from comment.models import Comment
from utils.tests.tests_permissions import BasePermissionTests
class CommentModeratorPermissionTests(BasePermissionTests):
@ -28,18 +30,47 @@ class CommentModeratorPermissionTests(BasePermissionTests):
)
self.userRole.save()
content_type = ContentType.objects.get(app_label='location', model='country')
self.content_type = ContentType.objects.get(app_label='location', model='country')
self.user_test = get_tokens_for_user()
self.comment = Comment.objects.create(text='Test comment', mark=1,
user=self.user_test["user"],
object_id= self.country_ru.pk,
content_type_id=content_type.id,
object_id=self.country_ru.pk,
content_type_id=self.content_type.id,
country=self.country_ru
)
self.comment.save()
self.url = reverse('back:comment:comment-crud', kwargs={"id": self.comment.id})
def test_post(self):
self.url = reverse('back:comment:comment-list-create')
comment = {
"text": "Test comment POST",
"user": self.user_test["user"].id,
"object_id": self.country_ru.pk,
"content_type": self.content_type.id,
"country_id": self.country_ru.id
}
response = self.client.post(self.url, format='json', data=comment)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
comment = {
"text": "Test comment POST moder",
"user": self.moderator.id,
"object_id": self.country_ru.id,
"content_type": self.content_type.id,
"country_id": self.country_ru.id
}
tokens = User.create_jwt_tokens(self.moderator)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('access_token')})
response = self.client.post(self.url, format='json', data=comment)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_put_moderator(self):
tokens = User.create_jwt_tokens(self.moderator)
@ -51,7 +82,9 @@ class CommentModeratorPermissionTests(BasePermissionTests):
"id": self.comment.id,
"text": "test text moderator",
"mark": 1,
"user": self.moderator.id
"user": self.moderator.id,
"object_id": self.comment.country_id,
"content_type": self.content_type.id
}
response = self.client.put(self.url, data=data, format='json')
@ -59,7 +92,7 @@ class CommentModeratorPermissionTests(BasePermissionTests):
def test_get(self):
response = self.client.get(self.url, format='json')
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_put_other_user(self):
other_user = User.objects.create_user(username='test',
@ -98,9 +131,10 @@ class CommentModeratorPermissionTests(BasePermissionTests):
"id": self.comment.id,
"text": "test text moderator",
"mark": 1,
"user": super_user.id
"user": super_user.id,
"object_id": self.country_ru.id,
"content_type": self.content_type.id,
}
response = self.client.put(self.url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -8,12 +8,13 @@ class CommentLstView(generics.ListCreateAPIView):
"""Comment list create view."""
serializer_class = serializers.CommentBaseSerializer
queryset = models.Comment.objects.all()
permission_classes = [permissions.IsAuthenticatedOrReadOnly,]
permission_classes = [permissions.IsAuthenticatedOrReadOnly| IsCommentModerator|IsCountryAdmin]
class CommentRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Comment RUD view."""
serializer_class = serializers.CommentBaseSerializer
queryset = models.Comment.objects.all()
permission_classes = [IsCountryAdmin|IsCommentModerator]
permission_classes = [IsCountryAdmin | IsCommentModerator]
lookup_field = 'id'

View File

@ -7,6 +7,7 @@ from comment.models import Comment
from utils.admin import BaseModelAdminMixin
from establishment import models
from main.models import Award
from product.models import Product
from review import models as review_models
@ -47,13 +48,18 @@ class CommentInline(GenericTabularInline):
extra = 0
class ProductInline(admin.TabularInline):
model = Product
extra = 0
@admin.register(models.Establishment)
class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""Establishment admin."""
list_display = ['id', '__str__', 'image_tag', ]
inlines = [
AwardInline, ContactPhoneInline, ContactEmailInline,
ReviewInline, CommentInline]
ReviewInline, CommentInline, ProductInline]
raw_id_fields = ('address',)

View File

@ -0,0 +1,30 @@
from django.core.management.base import BaseCommand
from establishment.models import Establishment
from transfer.models import EstablishmentAssets
class Command(BaseCommand):
help = 'Add preview_image_url values from old db to new db'
def handle(self, *args, **kwargs):
count = 0
url = 'https://1dc3f33f6d-3.optimicdn.com/gaultmillau.com/'
raw_qs = EstablishmentAssets.objects.raw(
'''SELECT establishment_assets.id, establishment_id, attachment_suffix_url
FROM establishment_assets
WHERE attachment_file_name IS NOT NULL
AND attachment_suffix_url IS NOT NULL
AND scope = 'public'
AND type = 'Photo'
AND menu_id IS NULL
GROUP BY establishment_id;'''
)
queryset = [vars(query) for query in raw_qs]
for obj in queryset:
establishment = Establishment.objects.filter(old_id=obj['establishment_id']).first()
if establishment:
establishment.preview_image_url = url + obj['attachment_suffix_url']
establishment.save()
count += 1
self.stdout.write(self.style.WARNING(f'Updated {count} objects.'))

View File

@ -0,0 +1,19 @@
from django.core.management.base import BaseCommand
from establishment.models import Establishment
from transfer.models import Establishments
class Command(BaseCommand):
help = 'Add publish values from old db to new db'
def handle(self, *args, **kwargs):
old_establishments = Establishments.objects.all()
count = 0
for item in old_establishments:
obj = Establishment.objects.filter(old_id=item.id).first()
if obj:
count += 1
obj.is_publish = item.state == 'published'
obj.save()
self.stdout.write(self.style.WARNING(f'Updated {count} objects.'))

View File

@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand
from establishment.models import Establishment
class Command(BaseCommand):
help = 'Remove old establishments from new bd'
def handle(self, *args, **kwargs):
old_establishments = Establishment.objects.exclude(old_id__isnull=True)
count = old_establishments.count()
old_establishments.delete()
self.stdout.write(self.style.WARNING(f'Deleted {count} objects.'))

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-24 14:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('establishment', '0043_establishment_currency'),
]
operations = [
migrations.AddField(
model_name='position',
name='index_name',
field=models.CharField(db_index=True, max_length=255, null=True, unique=True, verbose_name='Index name'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-30 08:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0045_auto_20191029_0719'),
('establishment', '0044_position_index_name'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-30 16:18
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0045_auto_20191029_0719'),
('establishment', '0044_position_index_name'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-30 17:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('establishment', '0046_merge_20191030_1618'),
('establishment', '0046_merge_20191030_0858'),
]
operations = [
]

View File

@ -121,6 +121,11 @@ class EstablishmentQuerySet(models.QuerySet):
def with_type_related(self):
return self.prefetch_related('establishment_subtypes')
def with_es_related(self):
"""Return qs with related for ES indexing objects."""
return self.select_related('address', 'establishment_type', 'address__city', 'address__city__country').\
prefetch_related('tags', 'schedule')
def search(self, value, locale=None):
"""Search text in JSON fields."""
if locale is not None:
@ -244,14 +249,12 @@ class EstablishmentQuerySet(models.QuerySet):
def annotate_in_favorites(self, user):
"""Annotate flag in_favorites"""
favorite_establishments = []
favorite_establishment_ids = []
if user.is_authenticated:
favorite_establishments = user.favorites.by_content_type(app_label='establishment',
model='establishment') \
.values_list('object_id', flat=True)
favorite_establishment_ids = user.favorite_establishment_ids
return self.annotate(in_favorites=Case(
When(
id__in=favorite_establishments,
id__in=favorite_establishment_ids,
then=True),
default=False,
output_field=models.BooleanField(default=False)))
@ -438,7 +441,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
@property
def works_now(self):
""" Is establishment working now """
now_at_est_tz = datetime.now(tz=ptz(self.tz))
now_at_est_tz = datetime.now(tz=self.tz)
current_week = now_at_est_tz.weekday()
schedule_for_today = self.schedule.filter(weekday=current_week).first()
if schedule_for_today is None or schedule_for_today.closed_at is None or schedule_for_today.opening_at is None:
@ -483,6 +486,11 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
"""
return self.id
@property
def wines(self):
"""Return list products with type wine"""
return self.products.wines()
class Position(BaseAttributes, TranslatedFieldsMixin):
"""Position model."""
@ -494,6 +502,9 @@ class Position(BaseAttributes, TranslatedFieldsMixin):
priority = models.IntegerField(unique=True, null=True, default=None)
index_name = models.CharField(max_length=255, db_index=True, unique=True,
null=True, verbose_name=_('Index name'))
class Meta:
"""Meta class."""

View File

@ -5,15 +5,14 @@ from rest_framework import serializers
from comment import models as comment_models
from comment.serializers import common as comment_serializers
from establishment import models
from favorites.models import Favorites
from location.serializers import AddressBaseSerializer
from main.serializers import AwardSerializer, CurrencySerializer
from review import models as review_models
from tag.serializers import TagBaseSerializer
from timetable.serialziers import ScheduleRUDSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import ProjectModelSerializer
from utils.serializers import TranslatedField
from utils.serializers import (ProjectModelSerializer, TranslatedField,
FavoritesCreateSerializer)
class ContactPhonesSerializer(serializers.ModelSerializer):
@ -118,6 +117,18 @@ class EstablishmentTypeBaseSerializer(serializers.ModelSerializer):
'use_subtypes': {'write_only': True},
}
class EstablishmentTypeGeoSerializer(EstablishmentTypeBaseSerializer):
"""Serializer for EstablishmentType model w/ index_name."""
class Meta(EstablishmentTypeBaseSerializer.Meta):
fields = EstablishmentTypeBaseSerializer.Meta.fields + [
'index_name'
]
extra_kwargs = {
**EstablishmentTypeBaseSerializer.Meta.extra_kwargs,
'index_name': {'read_only': True},
}
class EstablishmentSubTypeBaseSerializer(serializers.ModelSerializer):
"""Serializer for EstablishmentSubType models."""
@ -147,12 +158,13 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer):
position_translated = serializers.CharField(source='position.name_translated')
awards = AwardSerializer(source='employee.awards', many=True)
priority = serializers.IntegerField(source='position.priority')
position_index_name = serializers.CharField(source='position.index_name')
class Meta:
"""Meta class."""
model = models.Employee
fields = ('id', 'name', 'position_translated', 'awards', 'priority')
fields = ('id', 'name', 'position_translated', 'awards', 'priority', 'position_index_name')
class EstablishmentBaseSerializer(ProjectModelSerializer):
@ -184,6 +196,19 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
]
class EstablishmentGeoSerializer(EstablishmentBaseSerializer):
"""Serializer for Geo view."""
type = EstablishmentTypeGeoSerializer(source='establishment_type', read_only=True)
class Meta(EstablishmentBaseSerializer.Meta):
"""Meta class."""
fields = EstablishmentBaseSerializer.Meta.fields + [
'type'
]
class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
"""Serializer for Establishment model."""
@ -280,26 +305,13 @@ class EstablishmentCommentRUDSerializer(comment_serializers.CommentSerializer):
]
class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer):
"""Create comment serializer"""
class Meta:
"""Serializer for model Comment"""
model = Favorites
fields = [
'id',
'created',
]
def get_user(self):
"""Get user from request"""
return self.context.get('request').user
class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
"""Serializer to favorite object w/ model Establishment."""
def validate(self, attrs):
"""Override validate method"""
"""Overridden validate method"""
# Check establishment object
establishment_slug = self.context.get('request').parser_context.get('kwargs').get('slug')
establishment_qs = models.Establishment.objects.filter(slug=establishment_slug)
establishment_qs = models.Establishment.objects.filter(slug=self.slug)
# Check establishment obj by slug from lookup_kwarg
if not establishment_qs.exists():
@ -308,18 +320,16 @@ class EstablishmentFavoritesCreateSerializer(serializers.ModelSerializer):
establishment = establishment_qs.first()
# Check existence in favorites
if self.get_user().favorites.by_content_type(app_label='establishment',
model='establishment')\
.by_object_id(object_id=establishment.id).exists():
if establishment.favorites.filter(user=self.user).exists():
raise utils_exceptions.FavoritesError()
attrs['establishment'] = establishment
return attrs
def create(self, validated_data, *args, **kwargs):
"""Override create method"""
"""Overridden create method"""
validated_data.update({
'user': self.get_user(),
'user': self.user,
'content_object': validated_data.pop('establishment')
})
return super().create(validated_data)

View File

@ -1,10 +1,15 @@
"""Establishment app tasks."""
import logging
from celery import shared_task
from celery.schedules import crontab
from celery.task import periodic_task
from django.core import management
from django_elasticsearch_dsl.management.commands import search_index
from establishment import models
from location.models import Country
logger = logging.getLogger(__name__)
@ -12,10 +17,15 @@ logger = logging.getLogger(__name__)
def recalculate_price_levels_by_country(country_id):
try:
country = Country.objects.get(pk=country_id)
except Country.DoesNotExist as ex:
except Country.DoesNotExist as _:
logger.error(f'ESTABLISHMENT. Country does not exist. ID {country_id}')
else:
qs = models.Establishment.objects.filter(address__city__country=country)
for establishment in qs:
establishment.recalculate_price_level(low_price=country.low_price,
high_price=country.high_price)
@periodic_task(run_every=crontab(minute=59))
def rebuild_establishment_indices():
management.call_command(search_index.Command(), action='rebuild', models=[models.Establishment.__name__],
force=True)

View File

@ -20,13 +20,14 @@ class BaseTestCase(APITestCase):
self.newsletter = True
self.user = User.objects.create_user(
username=self.username, email=self.email, password=self.password)
#get tokkens
tokkens = User.create_jwt_tokens(self.user)
# get tokens
tokens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie(
{'access_token': tokkens.get('access_token'),
'refresh_token': tokkens.get('refresh_token')})
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')})
self.establishment_type = EstablishmentType.objects.create(name="Test establishment type")
self.establishment_type = EstablishmentType.objects.create(
name="Test establishment type")
# Create lang object
self.lang = Language.objects.get(
@ -42,12 +43,15 @@ class BaseTestCase(APITestCase):
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 = 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 = 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)
@ -63,8 +67,9 @@ class BaseTestCase(APITestCase):
self.establishment.save()
self.user_role = UserRole.objects.create(user=self.user, role=self.role,
establishment=self.establishment)
self.user_role = UserRole.objects.create(
user=self.user, role=self.role,
establishment=self.establishment)
self.user_role.save()
@ -128,12 +133,11 @@ class EmployeeTests(BaseTestCase):
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()

View File

@ -1,17 +1,20 @@
from pprint import pprint
from django.db.models import Q
from django.db.models import Q, F
from transfer.models import Establishments
from transfer.models import Establishments, Dishes
from transfer.serializers.establishment import EstablishmentSerializer
from establishment.models import Establishment
from location.models import Address
from transfer.serializers.plate import PlateSerializer
def transfer_establishment():
result = []
old_establishments = Establishments.objects.exclude(
old_establishments = Establishments.objects.filter(
location__city__name__icontains='paris',
).exclude(
Q(type='Wineyard') |
Q(location__timezone__isnull=True)
).prefetch_related(
@ -19,6 +22,7 @@ def transfer_establishment():
'schedules_set',
'descriptions_set',
)
for item in old_establishments:
data = {
'old_id': item.id,
@ -28,6 +32,7 @@ def transfer_establishment():
'type': item.type,
'phone': item.phone,
'created': item.created_at,
'state': item.state,
'description': {},
'website': None,
'facebook': None,
@ -79,6 +84,25 @@ def transfer_establishment():
pprint(f"Establishment serializer errors: {serialized_data.errors}")
def transfer_menu():
dishes = Dishes.objects.exclude(
Q(establishment_id__isnull=True) |
Q(dish_type__isnull=True) |
Q(price__isnull=True) |
Q(currency__isnull=True) |
Q(name__isnull=True) |
Q(name__exact='')
).annotate(
country_code=F('establishment__location__country_code'),
)
serialized_data = PlateSerializer(data=list(dishes.values()), many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
pprint(f"Menu serializer errors: {serialized_data.errors}")
def transfer_establishment_addresses():
old_establishments = Establishments.objects.only("id", "location_id").exclude(
Q(type='Wineyard') |
@ -105,4 +129,5 @@ data_types = {
transfer_establishment,
transfer_establishment_addresses
]
"menu": [transfer_menu],
}

View File

@ -9,7 +9,6 @@ urlpatterns = [
path('', views.EstablishmentListView.as_view(), name='list'),
path('recent-reviews/', views.EstablishmentRecentReviewListView.as_view(),
name='recent-reviews'),
# path('wineries/', views.WineriesListView.as_view(), name='wineries-list'),
path('slug/<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'),
@ -18,5 +17,5 @@ urlpatterns = [
path('slug/<slug:slug>/comments/<int:comment_id>/', views.EstablishmentCommentRUDView.as_view(),
name='rud-comment'),
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
name='add-to-favorites')
name='create-destroy-favorites')
]

View File

@ -138,21 +138,18 @@ class EstablishmentFavoritesCreateDestroyView(generics.CreateAPIView, generics.D
"""
Returns the object the view is displaying.
"""
establishment_obj = get_object_or_404(models.Establishment,
slug=self.kwargs['slug'])
obj = get_object_or_404(
self.request.user.favorites.by_content_type(app_label='establishment',
model='establishment')
.by_object_id(object_id=establishment_obj.pk))
establishment = get_object_or_404(models.Establishment,
slug=self.kwargs['slug'])
favorites = get_object_or_404(establishment.favorites.filter(user=self.request.user))
# May raise a permission denied
self.check_object_permissions(self.request, obj)
return obj
self.check_object_permissions(self.request, favorites)
return favorites
class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIView):
"""Resource for getting list of nearest establishments."""
serializer_class = serializers.EstablishmentBaseSerializer
serializer_class = serializers.EstablishmentGeoSerializer
filter_class = filters.EstablishmentFilter
def get_queryset(self):
@ -170,14 +167,3 @@ class EstablishmentNearestRetrieveView(EstablishmentListView, generics.ListAPIVi
return qs.by_distance_from_point(**{k: v for k, v in filter_kwargs.items()
if v is not None})
return qs
# Wineries
# todo: find out about difference between subtypes data
# class WineriesListView(EstablishmentListView):
# """Return list establishments with type Wineries"""
#
# def get_queryset(self):
# """Overridden get_queryset method."""
# qs = super(WineriesListView, self).get_queryset()
# return qs.with_type_related().wineries()

View File

@ -61,9 +61,6 @@ class BaseTestCase(APITestCase):
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])

View File

@ -8,4 +8,6 @@ app_name = 'favorites'
urlpatterns = [
path('establishments/', views.FavoritesEstablishmentListView.as_view(),
name='establishment-list'),
path('products/', views.FavoritesProductListView.as_view(),
name='product-list'),
]

View File

@ -1,7 +1,11 @@
"""Views for app favorites."""
from rest_framework import generics
from establishment.models import Establishment
from establishment.filters import EstablishmentFilter
from establishment.serializers import EstablishmentBaseSerializer
from product.models import Product
from product.serializers import ProductBaseSerializer
from product.filters import ProductFilterSet
from .models import Favorites
@ -14,11 +18,24 @@ class FavoritesBaseView(generics.GenericAPIView):
class FavoritesEstablishmentListView(generics.ListAPIView):
"""List views for favorites"""
"""List views for establishments in favorites."""
serializer_class = EstablishmentBaseSerializer
filter_class = EstablishmentFilter
def get_queryset(self):
"""Override get_queryset method"""
return Establishment.objects.filter(favorites__user=self.request.user)\
.order_by('-favorites')
class FavoritesProductListView(generics.ListAPIView):
"""List views for products in favorites."""
serializer_class = ProductBaseSerializer
filter_class = ProductFilterSet
def get_queryset(self):
"""Override get_queryset method"""
return Product.objects.filter(favorites__user=self.request.user)\
.order_by('-favorites')

View File

@ -19,6 +19,22 @@ class CityAdmin(admin.ModelAdmin):
"""City admin."""
class WineAppellationInline(admin.TabularInline):
model = models.WineAppellation
extra = 0
@admin.register(models.WineRegion)
class WineRegionAdmin(admin.ModelAdmin):
"""WineRegion admin."""
inlines = [WineAppellationInline, ]
@admin.register(models.WineAppellation)
class WineAppellationAdmin(admin.ModelAdmin):
"""WineAppellation admin."""
@admin.register(models.Address)
class AddressAdmin(admin.OSMGeoAdmin):
"""Address admin."""

View File

@ -0,0 +1,41 @@
# Generated by Django 2.2.4 on 2019-10-28 07:27
from django.db import migrations, models
import django.db.models.deletion
import utils.models
class Migration(migrations.Migration):
dependencies = [
('location', '0012_data_migrate'),
]
operations = [
migrations.CreateModel(
name='WineRegion',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', utils.models.TJSONField(help_text='{"en-GB":"some text"}', verbose_name='Name')),
('country', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='location.Country', verbose_name='country')),
],
options={
'verbose_name': 'wine region',
'verbose_name_plural': 'wine regions',
},
bases=(utils.models.TranslatedFieldsMixin, models.Model),
),
migrations.CreateModel(
name='WineAppellation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', utils.models.TJSONField(help_text='{"en-GB":"some text"}', verbose_name='Name')),
('wine_region', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='appellations', to='location.WineRegion', verbose_name='wine region')),
],
options={
'verbose_name': 'wine appellation',
'verbose_name_plural': 'wine appellations',
},
bases=(utils.models.TranslatedFieldsMixin, models.Model),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-30 08:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('location', '0018_address_old_id'),
('location', '0013_wineappellation_wineregion'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-30 16:18
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('location', '0018_address_old_id'),
('location', '0013_wineappellation_wineregion'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-30 17:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('location', '0019_merge_20191030_0858'),
('location', '0019_merge_20191030_1618'),
]
operations = [
]

View File

@ -134,6 +134,49 @@ class Address(models.Model):
return self.city.country_id
class WineRegionQuerySet(models.QuerySet):
"""Wine region queryset."""
class WineRegion(TranslatedFieldsMixin, models.Model):
"""Wine region model."""
STR_FIELD_NAME = 'name'
name = TJSONField(verbose_name=_('Name'),
help_text='{"en-GB":"some text"}')
country = models.ForeignKey(Country, on_delete=models.PROTECT,
verbose_name=_('country'))
objects = WineRegionQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name_plural = _('wine regions')
verbose_name = _('wine region')
class WineAppellationQuerySet(models.QuerySet):
"""Wine appellation queryset."""
class WineAppellation(TranslatedFieldsMixin, models.Model):
"""Wine appellation model."""
STR_FIELD_NAME = 'name'
name = TJSONField(verbose_name=_('Name'),
help_text='{"en-GB":"some text"}')
wine_region = models.ForeignKey(WineRegion, on_delete=models.PROTECT,
related_name='appellations',
verbose_name=_('wine region'))
objects = WineAppellationQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name_plural = _('wine appellations')
verbose_name = _('wine appellation')
# todo: Make recalculate price levels
@receiver(post_save, sender=Country)
def run_recalculate_price_levels(sender, instance, **kwargs):

View File

@ -16,4 +16,5 @@ class CountryBackSerializer(common.CountrySerializer):
'code',
'svg_image',
'name',
'country_id'
]

View File

@ -148,3 +148,31 @@ class AddressDetailSerializer(AddressBaseSerializer):
'city_id',
'city',
)
class WineAppellationBaseSerializer(serializers.ModelSerializer):
"""Wine appellations."""
name_translated = TranslatedField()
class Meta:
"""Meta class."""
model = models.WineAppellation
fields = [
'id',
'name_translated',
]
class WineRegionBaseSerializer(serializers.ModelSerializer):
"""Wine region serializer."""
name_translated = TranslatedField()
country = CountrySerializer()
class Meta:
"""Meta class."""
model = models.WineRegion
fields = [
'id',
'name_translated',
'country',
]

View File

@ -19,15 +19,11 @@ class BaseTestCase(APITestCase):
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.user.is_superuser = True
# self.user.save()
tokkens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie(
{'access_token': tokkens.get('access_token'),
'refresh_token': tokkens.get('refresh_token')})
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')})
self.lang = Language.objects.get(
title='Russia',
@ -102,7 +98,6 @@ class RegionTests(BaseTestCase):
user_role.save()
def test_region_CRUD(self):
response = self.client.get('/api/back/location/regions/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -4,44 +4,48 @@ from rest_framework import generics
from location import models, serializers
from location.views import common
from utils.permissions import IsCountryAdmin
from rest_framework.permissions import IsAuthenticatedOrReadOnly
# Address
class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView):
"""Create view for model Address."""
serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all()
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Address."""
serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all()
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
# City
class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City."""
serializer_class = serializers.CitySerializer
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model City."""
serializer_class = serializers.CitySerializer
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
# Region
class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region"""
serializer_class = serializers.RegionSerializer
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""Retrieve view for model Region"""
serializer_class = serializers.RegionSerializer
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
# Country
@ -50,10 +54,11 @@ class CountryListCreateView(generics.ListCreateAPIView):
queryset = models.Country.objects.all()
serializer_class = serializers.CountryBackSerializer
pagination_class = None
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class CountryRUDView(generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Country."""
serializer_class = serializers.CountryBackSerializer
permission_classes = [IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
queryset = models.Country.objects.all()

View File

@ -63,6 +63,8 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
# todo: remove this
country_code = serializers.CharField(source='subdomain', read_only=True)
country_name = serializers.CharField(source='country.name_translated', read_only=True)
class Meta:
"""Meta class."""
@ -78,7 +80,8 @@ class SiteSettingsSerializer(serializers.ModelSerializer):
'config',
'ad_config',
'published_features',
'currency'
'currency',
'country_name'
)

View File

@ -29,6 +29,7 @@ class NewsAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""News admin."""
raw_id_fields = ('address',)
actions = [send_email_action]
raw_id_fields = ('news_type', 'address', 'country')
@admin.register(models.NewsGallery)

View File

View File

@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand
from news.models import News
class Command(BaseCommand):
help = 'Remove all news from new bd'
def handle(self, *args, **kwargs):
old_news = News.objects.all()
count = old_news.count()
old_news.delete()
self.stdout.write(self.style.WARNING(f'Deleted {count} objects.'))

View File

@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand
from news.models import News
class Command(BaseCommand):
help = 'Remove old news from new bd'
def handle(self, *args, **kwargs):
old_news = News.objects.exclude(old_id__isnull=True)
count = old_news.count()
old_news.delete()
self.stdout.write(self.style.WARNING(f'Deleted {count} objects.'))

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-25 12:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('gallery', '0003_auto_20191003_1228'),
('news', '0028_auto_20191024_1649'),
]
operations = [
migrations.AlterUniqueTogether(
name='newsgallery',
unique_together={('news', 'image'), ('news', 'is_main')},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-29 08:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0029_merge_20191025_0906'),
]
operations = [
migrations.AddField(
model_name='news',
name='old_id',
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='odl id'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-29 08:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0030_news_old_id'),
('news', '0030_merge_20191028_0725'),
]
operations = [
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-30 11:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0031_merge_20191029_0858'),
]
operations = [
migrations.AlterField(
model_name='news',
name='old_id',
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-30 08:58
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0029_auto_20191025_1241'),
('news', '0031_merge_20191029_0858'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-30 16:18
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0032_auto_20191030_1149'),
('news', '0029_auto_20191025_1241'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.4 on 2019-10-30 17:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0033_merge_20191030_1618'),
('news', '0032_merge_20191030_0858'),
]
operations = [
]

View File

@ -126,6 +126,7 @@ class News(BaseAttributes, TranslatedFieldsMixin):
(PUBLISHED_EXCLUSIVE, _('Published exclusive')),
)
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
news_type = models.ForeignKey(NewsType, on_delete=models.PROTECT,
verbose_name=_('news type'))
title = TJSONField(blank=True, null=True, default=None,
@ -235,4 +236,4 @@ class NewsGallery(models.Model):
"""NewsGallery meta class."""
verbose_name = _('news gallery')
verbose_name_plural = _('news galleries')
unique_together = ('news', 'is_main')
unique_together = (('news', 'is_main'), ('news', 'image'))

View File

@ -97,6 +97,7 @@ class CropImageSerializer(serializers.Serializer):
class NewsImageSerializer(serializers.ModelSerializer):
"""Serializer for returning crop images of news image."""
orientation_display = serializers.CharField(source='get_orientation_display',
read_only=True)
original_url = serializers.URLField(source='image.url')
@ -149,6 +150,17 @@ class NewsBaseSerializer(ProjectModelSerializer):
)
class NewsSimilarListSerializer(NewsBaseSerializer):
"""List serializer for News model."""
preview_image_url = serializers.URLField()
class Meta(NewsBaseSerializer.Meta):
"""Meta class."""
fields = NewsBaseSerializer.Meta.fields + (
'preview_image_url',
)
class NewsListSerializer(NewsBaseSerializer):
"""List serializer for News model."""
@ -191,8 +203,8 @@ class NewsDetailSerializer(NewsBaseSerializer):
class NewsDetailWebSerializer(NewsDetailSerializer):
"""News detail serializer for web users.."""
same_theme = NewsBaseSerializer(many=True, read_only=True)
should_read = NewsBaseSerializer(many=True, read_only=True)
same_theme = NewsSimilarListSerializer(many=True, read_only=True)
should_read = NewsSimilarListSerializer(many=True, read_only=True)
agenda = AgendaSerializer()
banner = NewsBannerSerializer()
@ -265,7 +277,6 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
"""Override validate method."""
news_pk = self.get_request_kwargs().get('pk')
image_id = self.get_request_kwargs().get('image_id')
is_main = attrs.get('is_main')
news_qs = models.News.objects.filter(pk=news_pk)
image_qs = Image.objects.filter(id=image_id)
@ -278,12 +289,6 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
news = news_qs.first()
image = image_qs.first()
if news.news_gallery.filter(image=image).exists():
raise serializers.ValidationError({'detail': _('Image is already added')})
if is_main and news.news_gallery.main_image().exists():
raise serializers.ValidationError({'detail': _('Main image is already added')})
attrs['news'] = news
attrs['image'] = image

View File

@ -66,6 +66,22 @@ class NewsTestCase(BaseTestCase):
def setUp(self):
super().setUp()
def test_news_post(self):
test_news ={
"title": {"en-GB": "Test news POST"},
"news_type_id": self.test_news_type.id,
"description": {"en-GB": "Description test news"},
"start": datetime.now() + timedelta(hours=-2),
"end": datetime.now() + timedelta(hours=2),
"state": News.PUBLISHED,
"slug": 'test-news-slug_post',
"country_id": self.country_ru.id,
}
url = reverse("back:news:list-create")
response = self.client.post(url, data=test_news, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_web_news(self):
response = self.client.get(reverse('web:news:list'))
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -1,27 +1,52 @@
from pprint import pprint
from django.db.models import Value, IntegerField, F
from django.db.models import Aggregate, CharField, Value
from django.db.models import IntegerField, F
from news.models import NewsType
from tag.models import TagCategory
from transfer.models import PageTexts
from transfer.serializers.news import NewsSerializer
def transfer_news():
news_type, _ = NewsType.objects.get_or_create(name="News")
class GroupConcat(Aggregate):
function = 'GROUP_CONCAT'
template = '%(function)s(%(expressions)s)'
queryset = PageTexts.objects.filter(page__type="News").annotate(
news_type=Value(news_type.id, output_field=IntegerField()),
def __init__(self, expression, **extra):
output_field = extra.pop('output_field', CharField())
super().__init__(expression, output_field=output_field, **extra)
def as_postgresql(self, compiler, connection):
self.function = 'STRING_AGG'
return super().as_sql(compiler, connection)
def transfer_news():
news_type, _ = NewsType.objects.get_or_create(name='News')
tag_cat, _ = TagCategory.objects.get_or_create(index_name='tag', public=True)
news_type.tag_categories.add(tag_cat)
news_type.save()
queryset = PageTexts.objects.filter(
page__type='News',
).annotate(
tag_cat_id=Value(tag_cat.id, output_field=IntegerField()),
news_type_id=Value(news_type.id, output_field=IntegerField()),
country_code=F('page__site__country_code_2'),
news_title=F('page__root_title'),
image=F('page__attachment_suffix_url'),
template=F('page__template'),
tags=GroupConcat('page__tags__id'),
)
queryset = queryset.annotate(template=F('page__template'))
serialized_data = NewsSerializer(data=list(queryset.values()), many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
pprint(f"News serializer errors: {serialized_data.errors}")
pprint(f'News serializer errors: {serialized_data.errors}')
data_types = {
"news": [transfer_news]
'news': [transfer_news]
}

View File

@ -119,7 +119,7 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
return gallery
def create(self, request, *args, **kwargs):
"""Override create method"""
"""Overridden create method"""
super().create(request, *args, **kwargs)
return Response(status=status.HTTP_201_CREATED)

View File

@ -15,10 +15,10 @@ class BaseTestCase(APITestCase):
self.password = 'sedragurdaredips19'
self.email = 'sedragurda@desoz.com'
self.user = User.objects.create_user(username=self.username, email=self.email, password=self.password)
# get tokkens
tokkens = User.create_jwt_tokens(self.user)
self.client.cookies = SimpleCookie({'access_token': tokkens.get('access_token'),
'refresh_token': tokkens.get('refresh_token')})
# 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')})
class NotificationAnonSubscribeTestCase(APITestCase):

19
apps/product/admin.py Normal file
View File

@ -0,0 +1,19 @@
"""Product admin conf."""
from django.contrib import admin
from utils.admin import BaseModelAdminMixin
from .models import Product, ProductType, ProductSubType
@admin.register(Product)
class ProductAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""Admin page for model Product."""
@admin.register(ProductType)
class ProductTypeAdmin(admin.ModelAdmin):
"""Admin page for model ProductType."""
@admin.register(ProductSubType)
class ProductSubTypeAdmin(admin.ModelAdmin):
"""Admin page for model ProductSubType."""

34
apps/product/filters.py Normal file
View File

@ -0,0 +1,34 @@
"""Filters for app Product."""
from django.core.validators import EMPTY_VALUES
from django_filters import rest_framework as filters
from product import models
class ProductFilterSet(filters.FilterSet):
"""Product filter set."""
establishment_id = filters.NumberFilter()
product_type = filters.ChoiceFilter(method='by_product_type',
choices=models.ProductType.INDEX_NAME_TYPES)
product_subtype = filters.ChoiceFilter(method='by_product_subtype',
choices=models.ProductSubType.INDEX_NAME_TYPES)
class Meta:
"""Meta class."""
model = models.Product
fields = [
'establishment_id',
'product_type',
'product_subtype',
]
def by_product_type(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_product_type(value)
return queryset
def by_product_subtype(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_product_subtype(value)
return queryset

View File

View File

@ -0,0 +1,54 @@
from django.core.management.base import BaseCommand
from product.models import ProductType, ProductSubType
def add_type():
product_type = ProductType.objects.create(
name={'"en-GB"': "Wine"},
index_name=ProductType.WINE
)
return product_type.save()
def add_subtype(id_type):
subtypes = ProductSubType.objects.bulk_create([
ProductSubType(product_type=id_type,
name={"en-GB", ProductSubType.EXTRA_BRUT},
index_name=ProductSubType.EXTRA_BRUT),
ProductSubType(product_type=id_type,
name={"en-GB", ProductSubType.BRUT},
index_name=ProductSubType.BRUT),
ProductSubType(product_type=id_type,
name={"en-GB", ProductSubType.BRUT_NATURE},
index_name=ProductSubType.BRUT_NATURE),
ProductSubType(product_type=id_type,
name={"en-GB", ProductSubType.DEMI_SEC},
index_name=ProductSubType.DEMI_SEC),
ProductSubType(product_type=id_type,
name={"en-GB", ProductSubType.EXTRA_DRY},
index_name=ProductSubType.EXTRA_DRY),
ProductSubType(product_type=id_type,
name={"en-GB", ProductSubType.DOSAGE_ZERO},
index_name=ProductSubType.DOSAGE_ZERO),
ProductSubType(product_type=id_type,
name={"en-GB", ProductSubType.SEC},
index_name=ProductSubType.SEC),
ProductSubType(product_type=id_type,
name={"en-GB", ProductSubType.SEC},
index_name=ProductSubType.SEC),
ProductSubType(product_type=id_type,
name={"en-GB", ProductSubType.MOELLEUX},
index_name=ProductSubType.MOELLEUX),
])
class Command(BaseCommand):
help = 'Add product data'
def handle(self, *args, **kwarg):
product_type = add_type()
add_subtype(product_type.id)

View File

@ -0,0 +1,94 @@
# Generated by Django 2.2.4 on 2019-10-28 07:27
from django.conf import settings
import django.contrib.postgres.fields.jsonb
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import utils.models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('location', '0013_wineappellation_wineregion'),
('establishment', '0044_position_index_name'),
]
operations = [
migrations.CreateModel(
name='ProductType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Name')),
('index_name', models.CharField(choices=[('food', 'Food'), ('wine', 'Wine'), ('liquor', 'Liquor')], db_index=True, max_length=50, unique=True, verbose_name='Index name')),
('use_subtypes', models.BooleanField(default=True, verbose_name='Use subtypes')),
],
options={
'verbose_name': 'Product type',
'verbose_name_plural': 'Product types',
},
bases=(utils.models.TranslatedFieldsMixin, models.Model),
),
migrations.CreateModel(
name='ProductSubType',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('name', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Name')),
('index_name', models.CharField(choices=[('rum', 'Rum'), ('other', 'Other')], db_index=True, max_length=50, unique=True, verbose_name='Index name')),
('product_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subtypes', to='product.ProductType', verbose_name='Product type')),
],
options={
'verbose_name': 'Product subtype',
'verbose_name_plural': 'Product subtypes',
},
bases=(utils.models.TranslatedFieldsMixin, models.Model),
),
migrations.CreateModel(
name='Product',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Date created')),
('modified', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('category', models.PositiveIntegerField(choices=[(0, 'Common'), (1, 'Online')], default=0)),
('name', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Name')),
('description', utils.models.TJSONField(blank=True, default=None, help_text='{"en-GB":"some text"}', null=True, verbose_name='Description')),
('characteristics', django.contrib.postgres.fields.jsonb.JSONField(verbose_name='Characteristics')),
('available', models.BooleanField(default=True, verbose_name='Available')),
('public_mark', models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='public mark')),
('country', models.ManyToManyField(to='location.Country', verbose_name='Country')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_records_created', to=settings.AUTH_USER_MODEL, verbose_name='created by')),
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='establishment.Establishment', verbose_name='establishment')),
('modified_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='product_records_modified', to=settings.AUTH_USER_MODEL, verbose_name='modified by')),
('product_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='product.ProductType', verbose_name='Type')),
('subtypes', models.ManyToManyField(blank=True, related_name='products', to='product.ProductSubType', verbose_name='Subtypes')),
('wine_appellation', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='location.WineAppellation', verbose_name='wine appellation')),
('wine_region', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='wines', to='location.WineRegion', verbose_name='wine region')),
],
options={
'verbose_name': 'Product',
'verbose_name_plural': 'Products',
},
bases=(utils.models.TranslatedFieldsMixin, models.Model),
),
migrations.CreateModel(
name='OnlineProduct',
fields=[
],
options={
'verbose_name': 'Online product',
'verbose_name_plural': 'Online products',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('product.product',),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-29 14:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='product',
name='slug',
field=models.SlugField(max_length=255, null=True, unique=True, verbose_name='Establishment slug'),
),
]

View File

@ -1,5 +1,7 @@
"""Product app models."""
from django.db import models
from django.contrib.contenttypes import fields as generic
from django.core.exceptions import ValidationError
from django.contrib.postgres.fields import JSONField
from django.utils.translation import gettext_lazy as _
from utils.models import (BaseAttributes, ProjectBaseMixin,
@ -9,9 +11,23 @@ from utils.models import (BaseAttributes, ProjectBaseMixin,
class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
"""ProductType model."""
STR_FIELD_NAME = 'name'
# INDEX NAME CHOICES
FOOD = 'food'
WINE = 'wine'
LIQUOR = 'liquor'
INDEX_NAME_TYPES = (
(FOOD, _('Food')),
(WINE, _('Wine')),
(LIQUOR, _('Liquor')),
)
name = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
index_name = models.CharField(max_length=50, unique=True, db_index=True,
index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES,
unique=True, db_index=True,
verbose_name=_('Index name'))
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
@ -25,19 +41,53 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
class ProductSubType(TranslatedFieldsMixin, ProjectBaseMixin):
"""ProductSubtype model."""
STR_FIELD_NAME = 'name'
# INDEX NAME CHOICES
RUM = 'rum'
OTHER = 'other'
EXTRA_BRUT = 'extra brut'
BRUT = 'brut'
BRUT_NATURE = 'brut nature'
DEMI_SEC = 'demi-sec'
EXTRA_DRY = 'Extra Dry'
DOSAGE_ZERO = 'dosage zero'
SEC = 'sec'
DOUX = 'doux'
MOELLEUX= 'moelleux'
INDEX_NAME_TYPES = (
(RUM, _('Rum')),
(OTHER, _('Other')),
(EXTRA_BRUT, _('extra brut')),
(BRUT, _('brut')),
(BRUT_NATURE, _('brut nature')),
(DEMI_SEC, _('demi-sec')),
(EXTRA_DRY, _('Extra Dry')),
(DOSAGE_ZERO, _('dosage zero')),
(SEC, _('sec')),
(DOUX, _('doux')),
(MOELLEUX, _('moelleux'))
)
product_type = models.ForeignKey(ProductType, on_delete=models.CASCADE,
related_name='subtypes',
verbose_name=_('Product type'))
name = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
index_name = models.CharField(max_length=50, unique=True, db_index=True,
index_name = models.CharField(max_length=50, choices=INDEX_NAME_TYPES,
unique=True, db_index=True,
verbose_name=_('Index name'))
class Meta:
"""Meta class."""
verbose_name = _('Product type')
verbose_name_plural = _('Product types')
verbose_name = _('Product subtype')
verbose_name_plural = _('Product subtypes')
def clean_fields(self, exclude=None):
if not self.product_type.use_subtypes:
raise ValidationError(_('Product type is not use subtypes.'))
class ProductManager(models.Manager):
@ -47,16 +97,33 @@ class ProductManager(models.Manager):
class ProductQuerySet(models.QuerySet):
"""Product queryset."""
def with_base_related(self):
return self.select_related('product_type', 'establishment') \
.prefetch_related('product_type__subtypes', 'country')
def common(self):
return self.filter(category=self.model.COMMON)
def online(self):
return self.filter(category=self.model.ONLINE)
def wines(self):
return self.filter(type__index_name=ProductType.WINE)
def by_product_type(self, product_type: str):
"""Filter by type."""
return self.filter(product_type__index_name=product_type)
def by_product_subtype(self, product_subtype: str):
"""Filter by subtype."""
return self.filter(subtypes__index_name=product_subtype)
class Product(TranslatedFieldsMixin, BaseAttributes):
"""Product models."""
STR_FIELD_NAME = 'name'
COMMON = 0
ONLINE = 1
@ -72,13 +139,30 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
description = TJSONField(_('Description'), null=True, blank=True,
default=None, help_text='{"en-GB":"some text"}')
characteristics = JSONField(_('Characteristics'))
country = models.ForeignKey('location.Country', on_delete=models.PROTECT,
verbose_name=_('Country'))
country = models.ManyToManyField('location.Country',
verbose_name=_('Country'))
available = models.BooleanField(_('Available'), default=True)
type = models.ForeignKey(ProductType, on_delete=models.PROTECT,
related_name='products', verbose_name=_('Type'))
subtypes = models.ManyToManyField(ProductSubType, related_name='products',
product_type = models.ForeignKey(ProductType, on_delete=models.PROTECT,
related_name='products', verbose_name=_('Type'))
subtypes = models.ManyToManyField(ProductSubType, blank=True,
related_name='products',
verbose_name=_('Subtypes'))
establishment = models.ForeignKey('establishment.Establishment',
on_delete=models.PROTECT,
related_name='products',
verbose_name=_('establishment'))
public_mark = models.PositiveIntegerField(blank=True, null=True, default=None,
verbose_name=_('public mark'),)
wine_region = models.ForeignKey('location.WineRegion', on_delete=models.PROTECT,
related_name='wines',
blank=True, null=True,
verbose_name=_('wine region'))
wine_appellation = models.ForeignKey('location.WineAppellation', on_delete=models.PROTECT,
blank=True, null=True,
verbose_name=_('wine appellation'))
slug = models.SlugField(unique=True, max_length=255, null=True,
verbose_name=_('Establishment slug'))
favorites = generic.GenericRelation(to='favorites.Favorites')
objects = ProductManager.from_queryset(ProductQuerySet)()
@ -88,12 +172,22 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
verbose_name = _('Product')
verbose_name_plural = _('Products')
def clean_fields(self, exclude=None):
super().clean_fields(exclude=exclude)
if self.product_type.index_name == ProductType.WINE and not self.wine_region:
raise ValidationError(_('wine_region field must be specified.'))
if not self.product_type.index_name == ProductType.WINE and self.wine_region:
raise ValidationError(_('wine_region field must not be specified.'))
if (self.wine_region and self.wine_appellation) and \
self.wine_appellation not in self.wine_region.appellations.all():
raise ValidationError(_('Wine appellation not exists in wine region.'))
class OnlineProductManager(ProductManager):
"""Extended manger for OnlineProduct model."""
def get_queryset(self):
"""Overrided get_queryset method."""
"""Overridden get_queryset method."""
return super().get_queryset().online()

View File

@ -0,0 +1,3 @@
from .common import *
from .web import *
from .mobile import *

View File

@ -1 +1,96 @@
"""Product app serializers."""
from rest_framework import serializers
from utils.serializers import TranslatedField, FavoritesCreateSerializer
from product.models import Product, ProductSubType, ProductType
from utils import exceptions as utils_exceptions
from django.utils.translation import gettext_lazy as _
from location.serializers import (WineRegionBaseSerializer, WineAppellationBaseSerializer,
CountrySimpleSerializer)
class ProductSubTypeBaseSerializer(serializers.ModelSerializer):
"""ProductSubType base serializer"""
name_translated = TranslatedField()
index_name_display = serializers.CharField(source='get_index_name_display')
class Meta:
model = ProductSubType
fields = [
'id',
'name_translated',
'index_name_display',
]
class ProductTypeBaseSerializer(serializers.ModelSerializer):
"""ProductType base serializer"""
name_translated = TranslatedField()
index_name_display = serializers.CharField(source='get_index_name_display')
class Meta:
model = ProductType
fields = [
'id',
'name_translated',
'index_name_display',
]
class ProductBaseSerializer(serializers.ModelSerializer):
"""Product base serializer."""
name_translated = TranslatedField()
description_translated = TranslatedField()
category_display = serializers.CharField(source='get_category_display')
product_type = ProductTypeBaseSerializer()
subtypes = ProductSubTypeBaseSerializer(many=True)
wine_region = WineRegionBaseSerializer(allow_null=True)
wine_appellation = WineAppellationBaseSerializer(allow_null=True)
available_countries = CountrySimpleSerializer(source='country', many=True)
class Meta:
"""Meta class."""
model = Product
fields = [
'id',
'slug',
'name_translated',
'category_display',
'description_translated',
'available',
'product_type',
'subtypes',
'public_mark',
'wine_region',
'wine_appellation',
'available_countries',
]
class ProductFavoritesCreateSerializer(FavoritesCreateSerializer):
"""Serializer to create favorite object w/ model Product."""
def validate(self, attrs):
"""Overridden validate method"""
# Check establishment object
product_qs = Product.objects.filter(slug=self.slug)
# Check establishment obj by slug from lookup_kwarg
if not product_qs.exists():
raise serializers.ValidationError({'detail': _('Object not found.')})
else:
product = product_qs.first()
# Check existence in favorites
if product.favorites.filter(user=self.user).exists():
raise utils_exceptions.FavoritesError()
attrs['product'] = product
return attrs
def create(self, validated_data, *args, **kwargs):
"""Overridden create method"""
validated_data.update({
'user': self.user,
'content_object': validated_data.pop('product')
})
return super().create(validated_data)

View File

@ -0,0 +1,12 @@
"""Product url patterns."""
from django.urls import path
from product import views
app_name = 'product'
urlpatterns = [
path('', views.ProductListView.as_view(), name='list'),
path('slug/<slug:slug>/favorites/', views.CreateFavoriteProductView.as_view(),
name='create-destroy-favorites')
]

View File

@ -0,0 +1,7 @@
"""Product web url patterns."""
from product.urls.common import urlpatterns as common_urlpatterns
urlpatterns = [
]
urlpatterns.extend(common_urlpatterns)

View File

@ -0,0 +1,4 @@
from .back import *
from .common import *
from .mobile import *
from .web import *

View File

@ -0,0 +1,39 @@
"""Product app views."""
from rest_framework import generics, permissions
from django.shortcuts import get_object_or_404
from product.models import Product
from product import serializers
from product import filters
class ProductBaseView(generics.GenericAPIView):
"""Product base view"""
def get_queryset(self):
"""Override get_queryset method."""
return Product.objects.with_base_related()
class ProductListView(ProductBaseView, generics.ListAPIView):
"""List view for model Product."""
permission_classes = (permissions.AllowAny, )
serializer_class = serializers.ProductBaseSerializer
filter_class = filters.ProductFilterSet
class CreateFavoriteProductView(generics.CreateAPIView,
generics.DestroyAPIView):
"""View for create/destroy product in favorites."""
serializer_class = serializers.ProductFavoritesCreateSerializer
lookup_field = 'slug'
def get_object(self):
"""
Returns the object the view is displaying.
"""
product = get_object_or_404(Product, slug=self.kwargs['slug'])
favorites = get_object_or_404(product.favorites.filter(user=self.request.user))
# May raise a permission denied
self.check_object_permissions(self.request, favorites)
return favorites

View File

@ -15,14 +15,12 @@ class RecipeQuerySet(models.QuerySet):
def annotate_in_favorites(self, user):
"""Annotate flag in_favorites"""
favorite_establishments = []
favorite_recipe_ids = []
if user.is_authenticated:
favorite_establishments = user.favorites.by_content_type(app_label='recipe',
model='recipe') \
.values_list('object_id', flat=True)
favorite_recipe_ids = user.favorite_recipe_ids
return self.annotate(in_favorites=models.Case(
models.When(
id__in=favorite_establishments,
id__in=favorite_recipe_ids,
then=True),
default=False,
output_field=models.BooleanField(default=False)))

View File

@ -27,7 +27,5 @@ class BaseTestCase(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_recipe_detail(self):
print(self.test_recipe.id)
response = self.client.get(f"/api/web/recipes/{self.test_recipe.id}/")
print(response.json())
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -1 +1 @@
# from search_indexes.signals import update_document
from search_indexes.signals import update_document

View File

@ -22,14 +22,13 @@ class EstablishmentDocument(Document):
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'index_name': fields.KeywordField(attr='index_name'),
})
establishment_subtypes = fields.ObjectField(
properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties={
'id': fields.IntegerField(),
}),
'name': fields.ObjectField(attr='name_indexing'),
'index_name': fields.KeywordField(attr='index_name'),
},
multi=True)
works_evening = fields.ListField(fields.IntegerField(
@ -82,15 +81,8 @@ class EstablishmentDocument(Document):
),
}
),
}
},
)
# todo: need to fix
# collections = fields.ObjectField(
# properties={
# 'id': fields.IntegerField(attr='collection.id'),
# 'collection_type': fields.IntegerField(attr='collection.collection_type'),
# },
# multi=True)
class Django:
@ -99,6 +91,7 @@ class EstablishmentDocument(Document):
'id',
'name',
'name_translated',
'is_publish',
'price_level',
'toque_number',
'public_mark',
@ -106,4 +99,4 @@ class EstablishmentDocument(Document):
)
def get_queryset(self):
return super().get_queryset().published()
return super().get_queryset().with_es_related()

View File

@ -1,5 +1,6 @@
"""Search indexes serializers."""
from rest_framework import serializers
from elasticsearch_dsl import AttrDict
from django_elasticsearch_dsl_drf.serializers import DocumentSerializer
from news.serializers import NewsTypeSerializer
from search_indexes.documents import EstablishmentDocument, NewsDocument
@ -13,6 +14,8 @@ class TagsDocumentSerializer(serializers.Serializer):
label_translated = serializers.SerializerMethodField()
def get_label_translated(self, obj):
if isinstance(obj, dict):
return get_translated_value(obj.get('label'))
return get_translated_value(obj.label)
@ -29,6 +32,13 @@ class AddressDocumentSerializer(serializers.Serializer):
geo_lon = serializers.FloatField(allow_null=True, source='coordinates.lon')
geo_lat = serializers.FloatField(allow_null=True, source='coordinates.lat')
# todo: refator
def to_representation(self, instance):
if instance != AttrDict(d={}) or \
(isinstance(instance, dict) and len(instance) != 0):
return super().to_representation(instance)
return None
class ScheduleDocumentSerializer(serializers.Serializer):
"""Schedule serializer for ES Document"""
@ -75,7 +85,7 @@ class NewsDocumentSerializer(DocumentSerializer):
class EstablishmentDocumentSerializer(DocumentSerializer):
"""Establishment document serializer."""
address = AddressDocumentSerializer()
address = AddressDocumentSerializer(allow_null=True)
tags = TagsDocumentSerializer(many=True)
schedule = ScheduleDocumentSerializer(many=True, allow_null=True)

View File

@ -4,74 +4,74 @@ from django.dispatch import receiver
from django_elasticsearch_dsl.registries import registry
# @receiver(post_save)
# def update_document(sender, **kwargs):
# from establishment.models import Establishment
# app_label = sender._meta.app_label
# model_name = sender._meta.model_name
# instance = kwargs['instance']
#
# if app_label == 'location':
# if model_name == 'country':
# establishments = Establishment.objects.filter(
# address__city__country=instance)
# for establishment in establishments:
# registry.update(establishment)
# if model_name == 'city':
# establishments = Establishment.objects.filter(
# address__city=instance)
# for establishment in establishments:
# registry.update(establishment)
# if model_name == 'address':
# establishments = Establishment.objects.filter(
# address=instance)
# for establishment in establishments:
# registry.update(establishment)
#
# if app_label == 'establishment':
# # todo: remove after migration
# from establishment import models as establishment_models
# if model_name == 'establishmenttype':
# if isinstance(instance, establishment_models.EstablishmentType):
# establishments = Establishment.objects.filter(
# establishment_type=instance)
# for establishment in establishments:
# registry.update(establishment)
# if model_name == 'establishmentsubtype':
# if isinstance(instance, establishment_models.EstablishmentSubType):
# establishments = Establishment.objects.filter(
# establishment_subtypes=instance)
# for establishment in establishments:
# registry.update(establishment)
#
# if app_label == 'tag':
# if model_name == 'tag':
# establishments = Establishment.objects.filter(tags=instance)
# for establishment in establishments:
# registry.update(establishment)
#
#
# @receiver(post_save)
# def update_news(sender, **kwargs):
# from news.models import News
# app_label = sender._meta.app_label
# model_name = sender._meta.model_name
# instance = kwargs['instance']
#
# if app_label == 'location':
# if model_name == 'country':
# qs = News.objects.filter(country=instance)
# for news in qs:
# registry.update(news)
#
# if app_label == 'news':
# if model_name == 'newstype':
# qs = News.objects.filter(news_type=instance)
# for news in qs:
# registry.update(news)
#
# if app_label == 'tag':
# if model_name == 'tag':
# qs = News.objects.filter(tags=instance)
# for news in qs:
# registry.update(news)
@receiver(post_save)
def update_document(sender, **kwargs):
from establishment.models import Establishment
app_label = sender._meta.app_label
model_name = sender._meta.model_name
instance = kwargs['instance']
if app_label == 'location':
if model_name == 'country':
establishments = Establishment.objects.filter(
address__city__country=instance)
for establishment in establishments:
registry.update(establishment)
if model_name == 'city':
establishments = Establishment.objects.filter(
address__city=instance)
for establishment in establishments:
registry.update(establishment)
if model_name == 'address':
establishments = Establishment.objects.filter(
address=instance)
for establishment in establishments:
registry.update(establishment)
if app_label == 'establishment':
# todo: remove after migration
from establishment import models as establishment_models
if model_name == 'establishmenttype':
if isinstance(instance, establishment_models.EstablishmentType):
establishments = Establishment.objects.filter(
establishment_type=instance)
for establishment in establishments:
registry.update(establishment)
if model_name == 'establishmentsubtype':
if isinstance(instance, establishment_models.EstablishmentSubType):
establishments = Establishment.objects.filter(
establishment_subtypes=instance)
for establishment in establishments:
registry.update(establishment)
if app_label == 'tag':
if model_name == 'tag':
establishments = Establishment.objects.filter(tags=instance)
for establishment in establishments:
registry.update(establishment)
@receiver(post_save)
def update_news(sender, **kwargs):
from news.models import News
app_label = sender._meta.app_label
model_name = sender._meta.model_name
instance = kwargs['instance']
if app_label == 'location':
if model_name == 'country':
qs = News.objects.filter(country=instance)
for news in qs:
registry.update(news)
if app_label == 'news':
if model_name == 'newstype':
qs = News.objects.filter(news_type=instance)
for news in qs:
registry.update(news)
if app_label == 'tag':
if model_name == 'tag':
qs = News.objects.filter(tags=instance)
for news in qs:
registry.update(news)

View File

@ -3,12 +3,13 @@ from rest_framework import permissions
from django_elasticsearch_dsl_drf import constants
from django_elasticsearch_dsl_drf.filter_backends import (
FilteringFilterBackend,
GeoSpatialFilteringFilterBackend
GeoSpatialFilteringFilterBackend,
DefaultOrderingFilterBackend,
)
from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet
from search_indexes import serializers, filters
from search_indexes.documents import EstablishmentDocument, NewsDocument
from utils.pagination import ProjectPageNumberPagination
from utils.pagination import ProjectMobilePagination
class NewsDocumentViewSet(BaseDocumentViewSet):
@ -16,7 +17,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
document = NewsDocument
lookup_field = 'slug'
pagination_class = ProjectPageNumberPagination
pagination_class = ProjectMobilePagination
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.NewsDocumentSerializer
ordering = ('id',)
@ -53,15 +54,20 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
document = EstablishmentDocument
lookup_field = 'slug'
pagination_class = ProjectPageNumberPagination
pagination_class = ProjectMobilePagination
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentDocumentSerializer
ordering = ('id',)
def get_queryset(self):
qs = super(EstablishmentDocumentViewSet, self).get_queryset()
qs = qs.filter('match', is_publish=True)
return qs
filter_backends = [
FilteringFilterBackend,
filters.CustomSearchFilterBackend,
GeoSpatialFilteringFilterBackend,
DefaultOrderingFilterBackend,
]
search_fields = {
@ -72,6 +78,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
translated_search_fields = (
'description',
)
ordering = 'id'
filter_fields = {
'slug': 'slug',
'tag': {
@ -124,6 +131,15 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
'establishment_subtypes': {
'field': 'establishment_subtypes.id'
},
'type': {
'field': 'establishment_type.index_name'
},
'subtype': {
'field': 'establishment_subtypes.index_name',
'lookups': [
constants.LOOKUP_QUERY_IN,
],
},
'works_noon': {
'field': 'works_noon',
'lookups': [

View File

View File

View File

@ -0,0 +1,69 @@
from django.core.management.base import BaseCommand
from establishment.models import Establishment, EstablishmentType
from transfer import models as legacy
from tag.models import Tag, TagCategory
class Command(BaseCommand):
help = 'Add tags values from old db to new db'
def handle(self, *args, **kwargs):
existing_establishment = Establishment.objects.filter(old_id__isnull=False)
ESTABLISHMENT = 1
SHOP = 2
RESTAURANT = 3
WINEYARD = 4
MAPPER = {
RESTAURANT: EstablishmentType.RESTAURANT,
WINEYARD: EstablishmentType.PRODUCER,
}
mapper_values_meta = legacy.KeyValueMetadatumKeyValueMetadatumEstablishments.objects.all()
for key, value in MAPPER.items():
values_meta_id_list = mapper_values_meta.filter(
key_value_metadatum_establishment_id=key
).values_list('key_value_metadatum_id')
est_type, _ = EstablishmentType.objects.get_or_create(index_name=value)
key_value_metadata = legacy.KeyValueMetadata.objects.filter(
id__in=values_meta_id_list)
# create TagCategory
for key_value in key_value_metadata:
tag_category, created = TagCategory.objects.get_or_create(
index_name=key_value.key_name,
)
if created:
tag_category.label = {
'en-GB': key_value.key_name,
'fr-FR': key_value.key_name,
'ru-RU': key_value.key_name,
},
tag_category.value_type = key_value.value_type
tag_category.save()
est_type.tag_categories.add(
tag_category
)
# create Tag
for tag in key_value.metadata_set.filter(
establishment__id__in=list(existing_establishment.values_list('old_id', flat=True))):
new_tag, _ = Tag.objects.get_or_create(
label={
'en-GB': tag.value,
'fr-FR': tag.value,
'ru-RU': tag.value,
},
value=tag.value,
category=tag_category,
)
est = existing_establishment.get(old_id=tag.establishment_id)
est.tags.add(new_tag)
est.save()

View File

@ -0,0 +1,23 @@
# Generated by Django 2.2.4 on 2019-10-30 11:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0005_tagcategory_name_indexing'),
]
operations = [
migrations.AddField(
model_name='tag',
name='value',
field=models.CharField(blank=True, default=None, max_length=255, null=True, unique=True, verbose_name='indexing name'),
),
migrations.AddField(
model_name='tagcategory',
name='value_type',
field=models.CharField(choices=[('string', 'string'), ('list', 'list'), ('integer', 'integer')], default='list', max_length=255, verbose_name='value type'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.4 on 2019-10-30 15:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tag', '0006_auto_20191030_1151'),
]
operations = [
migrations.AlterField(
model_name='tag',
name='value',
field=models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='indexing name'),
),
]

View File

@ -19,6 +19,8 @@ class Tag(TranslatedFieldsMixin, models.Model):
label = TJSONField(blank=True, null=True, default=None,
verbose_name=_('label'),
help_text='{"en-GB":"some text"}')
value = models.CharField(_('indexing name'), max_length=255, blank=True,
null=True, default=None)
category = models.ForeignKey('TagCategory', on_delete=models.CASCADE,
null=True, related_name='tags',
verbose_name=_('Category'))
@ -72,6 +74,16 @@ class TagCategoryQuerySet(models.QuerySet):
class TagCategory(TranslatedFieldsMixin, models.Model):
"""Tag base category model."""
STRING = 'string'
LIST = 'list'
INTEGER = 'integer'
VALUE_TYPE_CHOICES = (
(STRING, _('string')),
(LIST, _('list')),
(INTEGER, _('integer')),
)
label = TJSONField(blank=True, null=True, default=None,
verbose_name=_('label'),
help_text='{"en-GB":"some text"}')
@ -80,7 +92,10 @@ class TagCategory(TranslatedFieldsMixin, models.Model):
default=None)
public = models.BooleanField(default=False)
index_name = models.CharField(max_length=255, blank=True, null=True,
verbose_name=_('indexing name'), unique=True)
verbose_name=_('indexing name'), unique=True)
value_type = models.CharField(_('value type'), max_length=255,
choices=VALUE_TYPE_CHOICES, default=LIST, )
objects = TagCategoryQuerySet.as_manager()

View File

@ -27,6 +27,7 @@ class ScheduleRUDSerializer(serializers.ModelSerializer):
fields = [
'id',
'weekday_display',
'weekday',
'lunch_start',
'lunch_end',
'dinner_start',

View File

@ -19,7 +19,8 @@ class Command(BaseCommand):
'gallery',
'commercial',
'overlook',
'tmp'
'tmp',
'menu'
]
def handle(self, *args, **options):

View File

@ -99,7 +99,7 @@ class Ezuser(MigrateMixin):
password_hash = models.CharField(max_length=50, blank=True, null=True)
password_hash_type = models.IntegerField()
facebook_id = models.BigIntegerField()
#TODO: в legacy нету таблицы 'CadLevel'
# TODO: в legacy нету таблицы 'CadLevel'
# level = models.ForeignKey('CadLevel', models.DO_NOTHING)
points = models.IntegerField()
@ -461,25 +461,44 @@ class Descriptions(MigrateMixin):
db_table = 'descriptions'
# class EstablishmentAssets(MigrateMixin):
# using = 'legacy'
#
# establishment = models.ForeignKey('Establishments', models.DO_NOTHING)
# account = models.ForeignKey(Accounts, models.DO_NOTHING, blank=True, null=True)
# menu_id = models.IntegerField(blank=True, null=True)
# type = models.CharField(max_length=64)
# scope = models.CharField(max_length=32)
# created_at = models.DateTimeField()
# updated_at = models.DateTimeField()
# attachment_file_name = models.CharField(max_length=255, blank=True, null=True)
# attachment_content_type = models.CharField(max_length=255, blank=True, null=True)
# geometries = models.CharField(max_length=1024, blank=True, null=True)
# attachment_file_size = models.IntegerField(blank=True, null=True)
# attachment_updated_at = models.DateTimeField(blank=True, null=True)
#
# class Meta:
# managed = False
# db_table = 'establishment_assets'
class Dishes(MigrateMixin):
using = 'legacy'
name = models.CharField(max_length=255, blank=True, null=True)
price = models.FloatField(blank=True, null=True)
currency = models.CharField(max_length=255, blank=True, null=True)
dish_type = models.CharField(max_length=255, blank=True, null=True)
signature = models.IntegerField(blank=True, null=True)
establishment = models.ForeignKey('Establishments', models.DO_NOTHING, blank=True, null=True)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
class Meta:
managed = False
db_table = 'dishes'
class EstablishmentAssets(MigrateMixin):
using = 'legacy'
establishment = models.ForeignKey('Establishments', models.DO_NOTHING)
account = models.ForeignKey(Accounts, models.DO_NOTHING, blank=True, null=True)
menu_id = models.IntegerField(blank=True, null=True)
type = models.CharField(max_length=64)
scope = models.CharField(max_length=32)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
attachment_file_name = models.CharField(max_length=255, blank=True, null=True)
attachment_content_type = models.CharField(max_length=255, blank=True, null=True)
geometries = models.CharField(max_length=1024, blank=True, null=True)
attachment_file_size = models.IntegerField(blank=True, null=True)
attachment_updated_at = models.DateTimeField(blank=True, null=True)
attachment_suffix_url = models.TextField(blank=True, null=True)
class Meta:
managed = False
db_table = 'establishment_assets'
class EstablishmentBacklinks(MigrateMixin):
using = 'legacy'
@ -734,7 +753,7 @@ class Pages(MigrateMixin):
using = 'legacy'
root_title = models.CharField(max_length=255, blank=True, null=True)
site_id = models.IntegerField()
site = models.ForeignKey(Sites, models.DO_NOTHING, blank=True, null=True)
account_id = models.IntegerField(blank=True, null=True)
state = models.CharField(max_length=255, blank=True, null=True)
template = models.CharField(max_length=255, blank=True, null=True)
@ -742,6 +761,7 @@ class Pages(MigrateMixin):
attachment_content_type = models.CharField(max_length=255, blank=True, null=True)
attachment_file_size = models.IntegerField(blank=True, null=True)
attachment_updated_at = models.DateTimeField(blank=True, null=True)
attachment_suffix_url = models.TextField(blank=True, null=True)
geometries = models.CharField(max_length=1024, blank=True, null=True)
scheduled_at = models.DateTimeField(blank=True, null=True)
created_at = models.DateTimeField()
@ -766,7 +786,6 @@ class PageTexts(MigrateMixin):
locale = models.CharField(max_length=255, blank=True, null=True)
state = models.CharField(max_length=255, blank=True, null=True)
page = models.ForeignKey(Pages, models.DO_NOTHING, blank=True, null=True)
# page_id = models.IntegerField(blank=True, null=True)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
summary = models.TextField(blank=True, null=True)
@ -795,7 +814,7 @@ class PageMetadata(MigrateMixin):
key = models.CharField(max_length=255, blank=True, null=True)
value = models.CharField(max_length=255, blank=True, null=True)
page_id = models.IntegerField(blank=True, null=True)
page = models.ForeignKey('Pages', models.DO_NOTHING, blank=True, null=True, related_name='tags')
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
@ -822,3 +841,100 @@ class Ads(MigrateMixin):
class Meta:
managed = False
db_table = 'ads'
class KeyValueMetadata(MigrateMixin):
using = 'legacy'
key_name = models.CharField(max_length=255, blank=True, null=True)
value_type = models.CharField(max_length=255, blank=True, null=True)
value_list = models.TextField(blank=True, null=True)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
public = models.IntegerField(blank=True, null=True)
site_id = models.IntegerField(blank=True, null=True)
class Meta:
managed = False
db_table = 'key_value_metadata'
class Metadata(MigrateMixin):
using = 'legacy'
key = models.CharField(max_length=255, blank=True, null=True)
value = models.CharField(max_length=255, blank=True, null=True)
establishment = models.ForeignKey('transfer.Establishments', models.DO_NOTHING, blank=True, null=True)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
key_value_metadatum = models.ForeignKey('transfer.KeyValueMetadata', models.DO_NOTHING, blank=True, null=True)
class Meta:
managed = False
db_table = 'metadata'
class KeyValueMetadatumEstablishments(MigrateMixin):
using = 'legacy'
name = models.CharField(max_length=255, blank=True, null=True)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
class Meta:
managed = False
db_table = 'key_value_metadatum_establishments'
class KeyValueMetadatumKeyValueMetadatumEstablishments(MigrateMixin):
using = 'legacy'
key_value_metadatum_id = models.IntegerField(blank=True, null=True)
key_value_metadatum_establishment_id = models.IntegerField(blank=True, null=True)
class Meta:
managed = False
db_table = 'key_value_metadatum_key_value_metadatum_establishments'
# class Products(models.Model):
# establishment = models.ForeignKey('Establishments', models.DO_NOTHING, blank=True, null=True)
# brand = models.CharField(max_length=255, blank=True, null=True)
# name = models.CharField(max_length=255, blank=True, null=True)
# vintage = models.CharField(max_length=255, blank=True, null=True)
# type = models.CharField(max_length=255, blank=True, null=True)
# unique_key = models.CharField(max_length=255, blank=True, null=True)
# price = models.FloatField(blank=True, null=True)
# average_price_in_shops = models.FloatField(blank=True, null=True)
# fra_encima_id = models.IntegerField(blank=True, null=True)
# wine_sub_region_id = models.IntegerField(blank=True, null=True)
# classification = models.ForeignKey('WineClassifications', models.DO_NOTHING, blank=True, null=True)
# wine_region = models.ForeignKey('WineLocations', models.DO_NOTHING, blank=True, null=True)
# wine_type = models.ForeignKey('WineTypes', models.DO_NOTHING, blank=True, null=True)
# wine_color = models.ForeignKey('WineColors', models.DO_NOTHING, blank=True, null=True)
# appellation = models.ForeignKey('WineClassifications', models.DO_NOTHING, blank=True, null=True)
# created_at = models.DateTimeField()
# updated_at = models.DateTimeField()
# state = models.CharField(max_length=255, blank=True, null=True)
# wine_style = models.ForeignKey('WineStyles', models.DO_NOTHING, blank=True, null=True)
# village = models.ForeignKey('WineLocations', models.DO_NOTHING, blank=True, null=True)
# vineyard = models.ForeignKey('WineLocations', models.DO_NOTHING, blank=True, null=True)
# yard_classification = models.ForeignKey('WineClassifications', models.DO_NOTHING, blank=True, null=True)
# wine_quality = models.ForeignKey('WineClassifications', models.DO_NOTHING, blank=True, null=True)
# bottles_produced = models.CharField(max_length=3000, blank=True, null=True)
# deu_import_id = models.IntegerField(blank=True, null=True)
#
# class Meta:
# managed = False
# db_table = 'products'
#
#
# class WineTypes(models.Model):
# name = models.CharField(max_length=255, blank=True, null=True)
# fra_encima_id = models.IntegerField(blank=True, null=True)
# created_at = models.DateTimeField()
# updated_at = models.DateTimeField()
#
# class Meta:
# managed = False
# db_table = 'wine_types'

View File

@ -24,6 +24,7 @@ class EstablishmentSerializer(serializers.ModelSerializer):
facebook = serializers.CharField(allow_null=True, allow_blank=True)
twitter = serializers.CharField(allow_null=True, allow_blank=True)
booking = serializers.CharField(allow_null=True, allow_blank=True)
state = serializers.CharField(allow_null=True)
tz = serializers.CharField()
created = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S')
@ -47,6 +48,7 @@ class EstablishmentSerializer(serializers.ModelSerializer):
'location', # + получить новые объекты Address по old_id
'email', # + создать объект для ContactEmail
'phone', # + создать объект для ContactPhone
'state', # + создать объект для ContactPhone
)
def validate(self, data):
@ -54,9 +56,11 @@ class EstablishmentSerializer(serializers.ModelSerializer):
'slug': generate_unique_slug(Establishment, data['slug'] if data['slug'] else data['name']),
'address_id': self.get_address(data['location']),
'establishment_type_id': self.get_type(data),
'is_publish': data.get('state') == 'published',
})
data.pop('location')
data.pop('type')
data.pop('state')
return data
@transaction.atomic

View File

@ -1,54 +1,96 @@
from rest_framework import serializers
from news.models import News
from gallery.models import Image
from location.models import Country
from news.models import News, NewsGallery
from tag.models import Tag
from transfer.models import PageMetadata
from utils.legacy_parser import parse_legacy_news_content
from utils.slug_generator import generate_unique_slug
class NewsSerializer(serializers.ModelSerializer):
locale = serializers.CharField()
slug = serializers.CharField()
body = serializers.CharField(allow_null=True)
class NewsSerializer(serializers.Serializer):
id = serializers.IntegerField()
tag_cat_id = serializers.IntegerField()
news_type_id = serializers.IntegerField()
news_title = serializers.CharField()
title = serializers.CharField()
template = serializers.CharField()
summary = serializers.CharField(allow_null=True)
body = serializers.CharField(allow_null=True)
created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S')
slug = serializers.CharField()
state = serializers.CharField()
created_at = serializers.DateTimeField(source='start', format='%m-%d-%Y %H:%M:%S')
class Meta:
model = News
fields = (
'created_at',
'state',
'template',
'title',
'body',
'slug',
'news_type',
'locale',
)
def validate(self, data):
data.update({
'slug': generate_unique_slug(News, data['slug']),
'state': self.get_state(data),
'template': self.get_template(data),
'title': self.get_title(data),
'description': self.get_description(data),
})
data.pop('body')
data.pop('locale')
return data
template = serializers.CharField()
country_code = serializers.CharField(allow_null=True)
locale = serializers.CharField()
image = serializers.CharField()
tags = serializers.CharField(allow_null=True)
def create(self, validated_data):
return News.objects.create(**validated_data)
payload = {
'old_id': validated_data['id'],
'news_type_id': validated_data['news_type_id'],
'title': {validated_data['locale']: validated_data['news_title']},
'subtitle': self.get_subtitle(validated_data),
'description': self.get_description(validated_data),
'start': validated_data['created_at'],
'slug': generate_unique_slug(News, validated_data['slug']),
'state': self.get_state(validated_data),
'template': self.get_template(validated_data),
'country': self.get_country(validated_data),
}
obj = News.objects.create(**payload)
tags = self.get_tags(validated_data)
for tag in tags:
obj.tags.add(tag)
obj.save()
self.make_gallery(validated_data, obj)
return obj
@staticmethod
def get_template(data):
templates = {
'main': News.MAIN,
'main.pdf.erb': News.MAIN_PDF_ERB,
}
return templates.get(data['template'], News.MAIN)
def make_gallery(data, obj):
if not data['image'] or data['image'] == 'default/missing.png':
return
img = Image.objects.create(
image=data['image'],
title=data['news_title'],
)
NewsGallery.objects.create(
news=obj,
image=img,
is_main=True,
)
@staticmethod
def get_tags(data):
results = []
if not data['tags']:
return results
meta_ids = (int(_id) for _id in data['tags'].split(','))
tags = PageMetadata.objects.filter(
id__in=meta_ids,
key='tag',
value__isnull=False,
)
for old_tag in tags:
tag, _ = Tag.objects.get_or_create(
category_id=data['tag_cat_id'],
label={data['locale']: old_tag.value},
)
results.append(tag)
return results
@staticmethod
def get_description(data):
if data['body']:
content = parse_legacy_news_content(data['body'])
return {data['locale']: content}
return None
@staticmethod
def get_state(data):
@ -61,13 +103,26 @@ class NewsSerializer(serializers.ModelSerializer):
}
return states.get(data['state'], News.WAITING)
@staticmethod
def get_template(data):
templates = {
'main': News.MAIN,
'main.pdf.erb': News.MAIN_PDF_ERB,
}
return templates.get(data['template'], News.MAIN)
@staticmethod
def get_country(data):
return Country.objects.filter(code__iexact=data['country_code']).first()
@staticmethod
def get_title(data):
return {data['locale']: data['title']}
@staticmethod
def get_description(data):
content = None
if data['body']:
content = parse_legacy_news_content(data['body'])
return {data['locale']: content}
def get_subtitle(data):
if 'summary' in data:
content = data.pop('summary')
if not content:
content = {data['locale']: data['title']}
return {data['locale']: content}

View File

@ -0,0 +1,55 @@
from rest_framework import serializers
from establishment.models import Menu, Plate, Establishment
from main.models import Currency
from utils.constants import CODE_LOCALES
class PlateSerializer(serializers.Serializer):
name = serializers.CharField()
price = serializers.DecimalField(decimal_places=2, max_digits=10)
currency = serializers.CharField()
dish_type = serializers.CharField()
country_code = serializers.CharField()
establishment_id = serializers.IntegerField()
signature = serializers.IntegerField(allow_null=True)
def create(self, validated_data):
establishment = Establishment.objects.filter(old_id=validated_data['establishment_id']).first()
if not establishment:
return
return Plate.objects.create(**self.get_plate_data(validated_data, establishment.id))
def get_plate_data(self, validated_data, est_id):
locale = CODE_LOCALES.get(validated_data['country_code'], 'en-GB')
payload = {
'name': {locale: validated_data['name']},
'price': validated_data['price'],
'currency_id': self.get_currency(validated_data),
'menu_id': self.get_menu(validated_data, est_id),
'is_signature_plate': bool(validated_data['signature']),
}
return payload
@staticmethod
def get_menu(validated_data, est_id):
payload = {
'establishment_id': est_id,
'category': {'en-GB': validated_data['dish_type']},
}
menu, _ = Menu.objects.get_or_create(**payload)
return menu.id
@staticmethod
def get_currency(validated_data):
try:
currency = Currency.objects.get(
slug=validated_data['currency'],
sign='$',
)
except Currency.DoesNotExist:
currency = Currency.objects.create(
slug=validated_data['currency'],
sign='$',
)
return currency.id

26
apps/utils/constants.py Normal file
View File

@ -0,0 +1,26 @@
CODE_LOCALES = {
'AAA': 'en-GB',
'AUS': 'en-AU', # Австралия
'AUT': 'de-AT', # Австрия
'BEL': 'de-BE', # Бельгия
'BRA': 'pt-BR', # Бразилия
'CAN': 'fr-CA', # Канада
'DEU': 'da-DE', # Германия
'FRA': 'fr-FR', # Франция
'GEO': 'ka', # Грузия
'GRC': 'el-GR', # Греция
'HRV': 'hr-HR', # Хорватия
'HUN': 'hu-HU', # Венгрия
'ISR': 'en-IL', # Израиль
'ITA': 'it-IT', # Италия
'JPN': 'ja', # Япония
'LUX': 'fr-LU', # Люксембург
'MAR': 'ar-MA', # Марокко
'MDV': 'dv', # Мальдивы
'NLD': 'nl-NL', # Нидерланды
'POL': 'pl', # Польша
'ROU': 'ro', # Румыния
'RUS': 'ru-RU', # Россия
'SVN': 'hu-SI', # Словения
'USA': 'en-US', # США
}

View File

@ -44,18 +44,19 @@ class TJSONField(JSONField):
def to_locale(language):
"""Turn a language name (en-us) into a locale name (en_US)."""
language, _, country = language.lower().partition('-')
if not country:
return language
# A language with > 2 characters after the dash only has its first
# character after the dash capitalized; e.g. sr-latn becomes sr-Latn.
# A language with 2 characters after the dash has both characters
# capitalized; e.g. en-us becomes en-US.
country, _, tail = country.partition('-')
country = country.title() if len(country) > 2 else country.upper()
if tail:
country += '-' + tail
return language + '-' + country
if language:
language, _, country = language.lower().partition('-')
if not country:
return language
# A language with > 2 characters after the dash only has its first
# character after the dash capitalized; e.g. sr-latn becomes sr-Latn.
# A language with 2 characters after the dash has both characters
# capitalized; e.g. en-us becomes en-US.
country, _, tail = country.partition('-')
country = country.title() if len(country) > 2 else country.upper()
if tail:
country += '-' + tail
return language + '-' + country
def translate_field(self, field_name):

View File

@ -20,8 +20,8 @@ class IsAuthenticatedAndTokenIsValid(permissions.BasePermission):
access_token = request.COOKIES.get('access_token')
if user.is_authenticated and access_token:
access_token = AccessToken(access_token)
valid_tokens = user.access_tokens.valid()\
.by_jti(jti=access_token.payload.get('jti'))
valid_tokens = user.access_tokens.valid() \
.by_jti(jti=access_token.payload.get('jti'))
return valid_tokens.exists()
else:
return False
@ -31,13 +31,14 @@ class IsRefreshTokenValid(permissions.BasePermission):
"""
Check if user has a valid refresh token and authenticated
"""
def has_permission(self, request, view):
"""Check permissions by refresh token and default REST permission IsAuthenticated"""
refresh_token = request.COOKIES.get('refresh_token')
if refresh_token:
refresh_token = GMRefreshToken(refresh_token)
refresh_token_qs = JWTRefreshToken.objects.valid()\
.by_jti(jti=refresh_token.payload.get('jti'))
refresh_token_qs = JWTRefreshToken.objects.valid() \
.by_jti(jti=refresh_token.payload.get('jti'))
return refresh_token_qs.exists()
else:
return False
@ -55,11 +56,15 @@ class IsGuest(permissions.IsAuthenticatedOrReadOnly):
"""
Object-level permission to only allow owners of an object to edit it.
"""
def has_permission(self, request, view):
return request.user.is_authenticated
rules = [
request.user.is_superuser,
request.method in permissions.SAFE_METHODS
]
return any(rules)
def has_object_permission(self, request, view, obj):
rules = [
request.user.is_superuser,
request.method in permissions.SAFE_METHODS
@ -72,6 +77,21 @@ class IsStandardUser(IsGuest):
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute.
"""
def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request, 'user'):
rules = [
request.user.is_authenticated,
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request
rules = [
@ -92,11 +112,29 @@ class IsContentPageManager(IsStandardUser):
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute.
"""
def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request, 'user'):
role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER,
country_id=request.country_id) \
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
# and obj.user != request.user,
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request.
role = Role.objects.filter(role=Role.CONTENT_PAGE_MANAGER,
country_id=obj.country_id)\
country_id=obj.country_id) \
.first() # 'Comments moderator'
rules = [
@ -112,6 +150,26 @@ class IsCountryAdmin(IsStandardUser):
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute.
"""
def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'):
# Read permissions are allowed to any request.
role = Role.objects.filter(role=Role.COUNTRY_ADMIN,
country_id=request.data.country_id) \
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request.
role = Role.objects.filter(role=Role.COUNTRY_ADMIN,
@ -119,9 +177,20 @@ class IsCountryAdmin(IsStandardUser):
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
super().has_object_permission(request, view, obj),
super().has_object_permission(request, view, obj)
]
# and request.user.email_confirmed,
if hasattr(request, 'user') and request.user.is_authenticated:
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
super().has_object_permission(request, view, obj),
]
if hasattr(request.data, 'user'):
rules = [
UserRole.objects.filter(user=request.data.user, role=role).exists(),
super().has_object_permission(request, view, obj),
]
return any(rules)
@ -131,10 +200,31 @@ class IsCommentModerator(IsStandardUser):
Object-level permission to only allow owners of an object to edit it.
Assumes the model instance has an `owner` attribute.
"""
def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'):
# Read permissions are allowed to any request.
role = Role.objects.filter(role=Role.COMMENTS_MODERATOR,
country_id=request.data.country_id) \
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request.
role = Role.objects.filter(role=Role.COMMENTS_MODERATOR,
country_id=obj.country_id)\
country_id=obj.country_id) \
.first() # 'Comments moderator'
rules = [
@ -147,10 +237,28 @@ class IsCommentModerator(IsStandardUser):
class IsEstablishmentManager(IsStandardUser):
def has_object_permission(self, request, view, obj):
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER)\
def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'establishment_id'):
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role,
establishment_id=request.data.establishment_id
).exists(),
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj):
role = Role.objects.filter(role=Role.ESTABLISHMENT_MANAGER) \
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role,
establishment_id=obj.establishment_id
@ -163,11 +271,28 @@ class IsEstablishmentManager(IsStandardUser):
class IsReviewerManager(IsStandardUser):
def has_object_permission(self, request, view, obj):
def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'country_id'):
role = Role.objects.filter(role=Role.REVIEWER_MANGER) \
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role,
establishment_id=request.data.country_id
).exists(),
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj):
role = Role.objects.filter(role=Role.REVIEWER_MANGER,
country_id=obj.country_id)\
.first()
country_id=obj.country_id) \
.first()
rules = [
UserRole.objects.filter(user=request.user, role=role).exists(),
@ -179,8 +304,25 @@ class IsReviewerManager(IsStandardUser):
class IsRestaurantReviewer(IsStandardUser):
def has_object_permission(self, request, view, obj):
def has_permission(self, request, view):
rules = [
super().has_permission(request, view)
]
# and request.user.email_confirmed,
if hasattr(request.data, 'user') and hasattr(request.data, 'object_id'):
role = Role.objects.filter(role=Role.RESTAURANT_REVIEWER) \
.first() # 'Comments moderator'
rules = [
UserRole.objects.filter(user=request.user, role=role,
establishment_id=request.data.object_id
).exists(),
super().has_permission(request, view)
]
return any(rules)
def has_object_permission(self, request, view, obj):
content_type = ContentType.objects.get(app_lable='establishment',
model='establishment')

View File

@ -4,6 +4,7 @@ from django.core import exceptions
from rest_framework import serializers
from utils import models
from translation.models import Language
from favorites.models import Favorites
class EmptySerializer(serializers.Serializer):
@ -72,3 +73,28 @@ class ProjectModelSerializer(serializers.ModelSerializer):
"""Overrided ModelSerializer."""
serializers.ModelSerializer.serializer_field_mapping[models.TJSONField] = TJSONField
class FavoritesCreateSerializer(serializers.ModelSerializer):
"""Serializer to favorite object."""
class Meta:
"""Serializer for model Comment."""
model = Favorites
fields = [
'id',
'created',
]
@property
def request(self):
return self.context.get('request')
@property
def user(self):
"""Get user from request"""
return self.request.user
@property
def slug(self):
return self.request.parser_context.get('kwargs').get('slug')

View File

@ -9,10 +9,11 @@ class BasePermissionTests(APITestCase):
title='Russia',
locale='ru-RU'
)
self.lang.save()
self.country_ru = Country.objects.get(
name={"en-GB": "Russian"}
)
self.country_ru.save()

View File

@ -9,6 +9,7 @@ from account.models import User
from news.models import News, NewsType
from establishment.models import Establishment, EstablishmentType, Employee
from location.models import Country
class BaseTestCase(APITestCase):
@ -39,7 +40,13 @@ class TranslateFieldTests(BaseTestCase):
self.news_type = NewsType.objects.create(name="Test news type")
self.news_type.save()
self.country_ru = Country.objects.get(
name={"en-GB": "Russian"}
)
self.news_item = News.objects.create(
id=8,
created_by=self.user,
modified_by=self.user,
title={
@ -52,6 +59,7 @@ class TranslateFieldTests(BaseTestCase):
news_type=self.news_type,
slug='test',
state=News.PUBLISHED,
country=self.country_ru,
)
self.news_item.save()
@ -62,7 +70,6 @@ class TranslateFieldTests(BaseTestCase):
response = self.client.get(f"/api/web/news/slug/{self.news_item.slug}/", format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
news_data = response.json()
print(news_data)
self.assertIn("title_translated", news_data)
self.assertIn("Test news item", news_data['title_translated'])

View File

@ -51,18 +51,10 @@ services:
# Redis
redis:
image: redis:2.8.23
ports:
- "6379:6379"
image: redis:alpine
networks:
- redis_network
# RabbitMQ
#rabbitmq:
# image: rabbitmq:latest
# ports:
# - "5672:5672"
# Celery
worker:
build: .
@ -78,7 +70,6 @@ services:
- .:/code
links:
- db
# - rabbitmq
- redis
networks:
- redis_network
@ -97,7 +88,6 @@ services:
- .:/code
links:
- db
# - rabbitmq
- redis
networks:
- redis_network
@ -116,7 +106,6 @@ services:
depends_on:
- mysql_db
- db
# - rabbitmq
- redis
- worker
- worker_beat
@ -135,13 +124,10 @@ services:
volumes:
gm-mysql_db:
name: gm-mysql_db
gm-db:
name: gm-db
gm-media:
name: gm-media
gm-esdata:
networks:

14
fabfile.py vendored
View File

@ -53,12 +53,14 @@ def collectstatic():
def deploy(branch=None):
fetch()
install_requirements()
migrate()
collectstatic()
touch()
kill_celery()
role = env.roles[0]
if env.roledefs[role]['branch'] != 'develop':
fetch()
install_requirements()
migrate()
collectstatic()
touch()
kill_celery()
def rev():

View File

@ -75,8 +75,8 @@ PROJECT_APPS = [
'favorites.apps.FavoritesConfig',
'rating.apps.RatingConfig',
'transfer.apps.TransferConfig',
'tag.apps.TagConfig'
'tag.apps.TagConfig',
'product.apps.ProductConfig',
]
EXTERNAL_APPS = [
@ -100,7 +100,7 @@ EXTERNAL_APPS = [
'timezone_field',
'storages',
'sorl.thumbnail',
'timezonefinder'
'timezonefinder',
]
@ -329,7 +329,8 @@ REDOC_SETTINGS = {
# RabbitMQ
# BROKER_URL = 'amqp://rabbitmq:5672'
# Redis
BROKER_URL = 'redis://localhost:6379/1'
# BROKER_URL = 'redis://localhost:6379/1'
BROKER_URL = 'redis://redis:6379/1'
CELERY_RESULT_BACKEND = BROKER_URL
CELERY_BROKER_URL = BROKER_URL
CELERY_ACCEPT_CONTENT = ['application/json']
@ -372,7 +373,7 @@ THUMBNAIL_DEFAULT_OPTIONS = {
THUMBNAIL_QUALITY = 85
THUMBNAIL_DEBUG = False
SORL_THUMBNAIL_ALIASES = {
'news_preview': {'geometry_string': '100x100', 'crop': 'center'},
'news_preview': {'geometry_string': '300x260', 'crop': 'center'},
'news_promo_horizontal_web': {'geometry_string': '1900x600', 'crop': 'center'},
'news_promo_horizontal_mobile': {'geometry_string': '375x260', 'crop': 'center'},
'news_tile_horizontal_web': {'geometry_string': '300x275', 'crop': 'center'},
@ -484,3 +485,6 @@ STATICFILES_DIRS = (
# MEDIA
MEDIA_LOCATION = 'media'
PHONENUMBER_DB_FORMAT = 'NATIONAL'
PHONENUMBER_DEFAULT_REGION = "FR"

View File

@ -26,8 +26,8 @@ ELASTICSEARCH_DSL = {
ELASTICSEARCH_INDEX_NAMES = {
# 'search_indexes.documents.news': 'development_news', # temporarily disabled
# 'search_indexes.documents.establishment': 'development_establishment',
'search_indexes.documents.news': 'development_news', # temporarily disabled
'search_indexes.documents.establishment': 'development_establishment',
}

View File

@ -35,4 +35,5 @@ urlpatterns = [
path('comments/', include('comment.urls.web')),
path('favorites/', include('favorites.urls')),
path('timetables/', include('timetable.urls.web')),
path('products/', include('product.urls.web')),
]