Merge branch 'develop' into feature/fix-country-region-city-transfer

# Conflicts:
#	apps/location/models.py
#	apps/location/transfer_data.py
#	apps/location/views/common.py
#	apps/transfer/management/commands/transfer.py
#	project/settings/local.py
This commit is contained in:
littlewolf 2019-11-21 14:43:30 +03:00
commit 470bd096b0
121 changed files with 3240 additions and 660 deletions

View File

@ -1,6 +1,7 @@
"""Back account serializers"""
from rest_framework import serializers
from account import models
from account.models import User
class RoleSerializer(serializers.ModelSerializer):
@ -18,4 +19,33 @@ class UserRoleSerializer(serializers.ModelSerializer):
fields = [
'user',
'role'
]
]
class BackUserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = '__all__'
extra_kwargs = {
'password': {'write_only': True}
}
read_only_fields = ('old_password', 'last_login', 'date_joined')
def create(self, validated_data):
user = super().create(validated_data)
user.set_password(validated_data['password'])
user.save()
return user
class BackDetailUserSerializer(BackUserSerializer):
class Meta:
model = User
exclude = ('password',)
read_only_fields = ('old_password', 'last_login', 'date_joined')
def create(self, validated_data):
user = super().create(validated_data)
user.set_password(validated_data['password'])
user.save()
return user

View File

@ -99,6 +99,18 @@ class UserBaseSerializer(serializers.ModelSerializer):
read_only_fields = fields
class UserShortSerializer(UserSerializer):
"""Compact serializer for model User."""
class Meta(UserSerializer.Meta):
"""Meta class."""
fields = [
'id',
'fullname',
'email',
]
class ChangePasswordSerializer(serializers.ModelSerializer):
"""Serializer for model User."""

View File

@ -84,3 +84,52 @@ class UserRoleTests(APITestCase):
response = self.client.post(url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
class UserTestCase(APITestCase):
def setUp(self):
self.user_1 = User.objects.create_user(
username='alex',
email='alex@mail.com',
password='alex_password',
is_staff=True,
)
self.user_2 = User.objects.create_user(
username='boris',
email='boris@mail.com',
password='boris_password',
)
# get tokens
tokens = User.create_jwt_tokens(self.user_1)
self.client.cookies = SimpleCookie(
{'access_token': tokens.get('access_token'),
'refresh_token': tokens.get('refresh_token')})
def test_user_CRUD(self):
response = self.client.get('/api/back/account/user/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = {
'username': 'roman',
'email': 'roman@mail.com',
'password': 'roman_password',
}
response = self.client.post('/api/back/account/user/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get(f'/api/back/account/user/{self.user_2.id}/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'first_name': 'Boris'
}
response = self.client.patch(f'/api/back/account/user/{self.user_2.id}/', data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete(f'/api/back/account/user/{self.user_2.id}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

View File

@ -8,5 +8,6 @@ app_name = 'account'
urlpatterns = [
path('role/', views.RoleLstView.as_view(), name='role-list-create'),
path('user-role/', views.UserRoleLstView.as_view(), name='user-role-list-create'),
path('user/', views.UserLstView.as_view(), name='user-list-create'),
path('user/<int:id>/', views.UserRUDView.as_view(), name='user-rud'),
]

View File

@ -1,6 +1,9 @@
from rest_framework import generics
from account.serializers import back as serializers
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions
from account import models
from account.models import User
from account.serializers import back as serializers
class RoleLstView(generics.ListCreateAPIView):
@ -10,4 +13,27 @@ class RoleLstView(generics.ListCreateAPIView):
class UserRoleLstView(generics.ListCreateAPIView):
serializer_class = serializers.UserRoleSerializer
queryset = models.Role.objects.all()
queryset = models.Role.objects.all()
class UserLstView(generics.ListCreateAPIView):
"""User list create view."""
queryset = User.objects.all()
serializer_class = serializers.BackUserSerializer
permission_classes = (permissions.IsAdminUser,)
filter_backends = (DjangoFilterBackend,)
filterset_fields = (
'email_confirmed',
'is_staff',
'is_active',
'is_superuser',
'roles',
)
class UserRUDView(generics.RetrieveUpdateDestroyAPIView):
"""User RUD view."""
queryset = User.objects.all()
serializer_class = serializers.BackDetailUserSerializer
permission_classes = (permissions.IsAdminUser,)
lookup_field = 'id'

View File

@ -63,7 +63,7 @@ class SendConfirmationEmailView(generics.GenericAPIView):
return Response(status=status.HTTP_200_OK)
class ConfirmEmailView(JWTGenericViewMixin):
class ConfirmEmailView(JWTGenericViewMixin, generics.GenericAPIView):
"""View for confirm changing email"""
permission_classes = (permissions.AllowAny,)

View File

@ -33,7 +33,7 @@ class PasswordResetView(generics.GenericAPIView):
return Response(status=status.HTTP_200_OK)
class PasswordResetConfirmView(JWTGenericViewMixin):
class PasswordResetConfirmView(JWTGenericViewMixin, generics.GenericAPIView):
"""View for confirmation new password"""
serializer_class = serializers.PasswordResetConfirmSerializer
permission_classes = (permissions.AllowAny,)

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.7 on 2019-11-16 11:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('advertisement', '0007_auto_20191115_0750'),
]
operations = [
migrations.AlterModelOptions(
name='advertisement',
options={'verbose_name': 'Advertisement', 'verbose_name_plural': 'Advertisements'},
),
]

View File

@ -2,11 +2,12 @@
import uuid
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from main.models import Page
from translation.models import Language
from utils.models import ProjectBaseMixin, ImageMixin, PlatformMixin, URLImageMixin
from main.models import Page
class AdvertisementQuerySet(models.QuerySet):
@ -25,6 +26,10 @@ class AdvertisementQuerySet(models.QuerySet):
"""Filter by locale."""
return self.filter(target_languages__locale=locale)
def valid(self):
"""Return only valid advertisements."""
return self.filter(end__lte=timezone.now())
class Advertisement(ProjectBaseMixin):
"""Advertisement model."""
@ -50,11 +55,17 @@ class Advertisement(ProjectBaseMixin):
class Meta:
verbose_name = _('Advertisement')
verbose_name_plural = _('Advertisement')
verbose_name_plural = _('Advertisements')
def __str__(self):
return str(self.url)
def delete(self, using=None, keep_parents=False):
"""Overridden delete method."""
# Delete all related pages.
self.pages.all().delete()
return super().delete(using, keep_parents)
@property
def mobile_page(self):
"""Return mobile page"""

View File

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

View File

@ -0,0 +1,26 @@
"""Serializers for back office app advertisements"""
from main.serializers import PageBaseSerializer
class AdvertisementPageBaseSerializer(PageBaseSerializer):
"""Base serializer for linking page w/ advertisement."""
class Meta(PageBaseSerializer.Meta):
"""Meta class."""
PageBaseSerializer.Meta.extra_kwargs.update({
'advertisement': {'write_only': True},
'image_url': {'required': True},
'width': {'required': True},
'height': {'required': True},
})
class AdvertisementPageListCreateSerializer(AdvertisementPageBaseSerializer):
"""Serializer for linking page w/ advertisement."""
def create(self, validated_data):
"""Overridden create method."""
validated_data['advertisement'] = self.context.get('view').get_object()
return super().create(validated_data)

View File

@ -3,15 +3,28 @@ from rest_framework import serializers
from advertisement import models
from translation.serializers import LanguageSerializer
from main.serializers import SiteShortSerializer
from main.serializers import PageBaseSerializer
from main.serializers import SiteShortSerializer, PageBaseSerializer
from translation.models import Language
from main.models import SiteSettings
class AdvertisementBaseSerializer(serializers.ModelSerializer):
"""Base serializer for model Advertisement."""
languages = LanguageSerializer(many=True, read_only=True)
languages = LanguageSerializer(many=True, read_only=True,
source='target_languages')
target_languages = serializers.PrimaryKeyRelatedField(
queryset=Language.objects.all(),
many=True,
write_only=True
)
sites = SiteShortSerializer(many=True, read_only=True)
target_sites = serializers.PrimaryKeyRelatedField(
queryset=SiteSettings.objects.all(),
many=True,
write_only=True,
source='sites'
)
class Meta:
model = models.Advertisement
@ -21,7 +34,9 @@ class AdvertisementBaseSerializer(serializers.ModelSerializer):
'url',
'block_level',
'languages',
'target_languages',
'sites',
'target_sites',
'start',
'end',
]

View File

@ -1,20 +1,42 @@
from pprint import pprint
from transfer.models import Ads
from transfer.serializers.advertisement import AdvertisementSerializer
from transfer.serializers.advertisement import AdvertisementSerializer, AdvertisementImageSerializer
def transfer_advertisement():
queryset = Ads.objects.filter(href__isnull=False).values_list('id', 'href', 'attachment_suffix_url')
errors = []
queryset = Ads.objects.exclude(href__isnull=True) \
.exclude(attachment_suffix_url__isnull=True) \
.exclude(site_id__isnull=True)
serialized_data = AdvertisementSerializer(data=list(queryset.values()), many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
pprint(f"News serializer errors: {serialized_data.errors}")
for d in serialized_data.errors: errors.append(d) if d else None
pprint(f"transfer_product errors: {errors}")
def transfer_page():
errors = []
queryset = Ads.objects.exclude(href__isnull=True) \
.exclude(attachment_suffix_url__isnull=True) \
.exclude(site_id__isnull=True)
serialized_data = AdvertisementImageSerializer(data=list(queryset.values()), many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
for d in serialized_data.errors: errors.append(d) if d else None
pprint(f"transfer_page errors: {errors}")
data_types = {
"commercial": [transfer_advertisement]
"commercial": [
transfer_advertisement,
transfer_page
]
}

View File

@ -0,0 +1,18 @@
"""Advertisement back office urlpaths."""
from django.urls import path
from advertisement import views
from advertisement.urls.common import common_urlpatterns
app_name = 'advertisements'
urlpatterns = [
path('', views.AdvertisementListCreateView.as_view(), name='list-create'),
path('<int:pk>/', views.AdvertisementRUDView.as_view(), name='rud'),
path('<int:pk>/pages/', views.AdvertisementPageListCreateView.as_view(),
name='page-list-create'),
path('<int:ad_pk>/pages/<int:page_pk>/', views.AdvertisementPageRUDView.as_view(),
name='page-rud')
]
urlpatterns += common_urlpatterns

View File

@ -1,5 +1,6 @@
"""Advertisement common urlpaths."""
from django.urls import path
from advertisement import views
app_name = 'advertisements'

View File

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

View File

@ -0,0 +1,77 @@
"""Back office views for app advertisement"""
from rest_framework import generics
from rest_framework import permissions
from django.shortcuts import get_object_or_404
from advertisement.models import Advertisement
from rest_framework.response import Response
from rest_framework import status
from advertisement.serializers import (AdvertisementBaseSerializer,
AdvertisementPageBaseSerializer,
AdvertisementPageListCreateSerializer)
class AdvertisementBackOfficeViewMixin(generics.GenericAPIView):
"""Base back office advertisement view."""
permission_classes = (permissions.IsAuthenticated, )
def get_queryset(self):
"""Overridden get queryset method."""
return Advertisement.objects.with_base_related()
class AdvertisementListCreateView(AdvertisementBackOfficeViewMixin, generics.ListCreateAPIView):
"""List|Create advertisement view."""
serializer_class = AdvertisementBaseSerializer
class AdvertisementRUDView(AdvertisementBackOfficeViewMixin,
generics.RetrieveUpdateDestroyAPIView):
"""Retrieve|Update|Destroy advertisement page view."""
serializer_class = AdvertisementBaseSerializer
class AdvertisementPageListCreateView(AdvertisementBackOfficeViewMixin,
generics.ListCreateAPIView):
"""Retrieve|Update|Destroy advertisement page view."""
serializer_class = AdvertisementPageListCreateSerializer
def get_object(self):
"""Returns the object the view is displaying."""
ad_qs = Advertisement.objects.all()
filtered_ad_qs = self.filter_queryset(ad_qs)
ad = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk'))
# May raise a permission denied
self.check_object_permissions(self.request, ad)
return ad
def get_queryset(self):
"""Overridden get_queryset method."""
return self.get_object().pages.all()
class AdvertisementPageRUDView(AdvertisementBackOfficeViewMixin,
generics.RetrieveUpdateDestroyAPIView):
"""Create|Retrieve|Update|Destroy advertisement page view."""
serializer_class = AdvertisementPageBaseSerializer
def get_object(self):
"""Returns the object the view is displaying."""
ad_qs = Advertisement.objects.all()
filtered_ad_qs = self.filter_queryset(ad_qs)
ad = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('ad_pk'))
page = get_object_or_404(ad.pages.all(), pk=self.kwargs.get('page_pk'))
# May raise a permission denied
self.check_object_permissions(self.request, page)
return page

View File

@ -71,7 +71,7 @@ class OAuth2ViewMixin(CsrfExemptMixin, OAuthLibMixin, BaseOAuth2ViewMixin):
# Sign in via Facebook
class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin):
class OAuth2SignUpView(OAuth2ViewMixin, JWTGenericViewMixin, generics.GenericAPIView):
"""
Implements an endpoint to convert a provider token to an access token
@ -142,7 +142,7 @@ class SignUpView(generics.GenericAPIView):
return Response(status=status.HTTP_201_CREATED)
class ConfirmationEmailView(JWTGenericViewMixin):
class ConfirmationEmailView(JWTGenericViewMixin, generics.GenericAPIView):
"""View for confirmation email"""
permission_classes = (permissions.AllowAny, )
@ -174,7 +174,7 @@ class ConfirmationEmailView(JWTGenericViewMixin):
# Login by username|email + password
class LoginByUsernameOrEmailView(JWTGenericViewMixin):
class LoginByUsernameOrEmailView(JWTGenericViewMixin, generics.GenericAPIView):
"""Login by email and password"""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.LoginByUsernameOrEmailSerializer
@ -197,7 +197,7 @@ class LoginByUsernameOrEmailView(JWTGenericViewMixin):
# Logout
class LogoutView(JWTGenericViewMixin):
class LogoutView(JWTGenericViewMixin, generics.GenericAPIView):
"""Logout user"""
permission_classes = (IsAuthenticatedAndTokenIsValid, )
@ -215,7 +215,7 @@ class LogoutView(JWTGenericViewMixin):
# Refresh token
class RefreshTokenView(JWTGenericViewMixin):
class RefreshTokenView(JWTGenericViewMixin, generics.GenericAPIView):
"""Refresh access_token"""
permission_classes = (permissions.AllowAny, )
serializer_class = serializers.RefreshTokenSerializer

View File

@ -13,10 +13,50 @@ from utils.methods import get_user_ip
class CheckWhetherBookingAvailable(generics.GenericAPIView):
""" Checks which service to use if establishmend is managed by any """
_VALID_GUESTONLINE_PERIODS = {'lunch', 'dinner', 'afternoon', 'breakfast'}
_GUESTONLINE_PERIODS_TO_PRIOR = {
'breakfast': 1,
'lunch': 2,
'afternoon': 3,
'dinner': 4,
}
permission_classes = (permissions.AllowAny,)
serializer_class = CheckBookingSerializer
pagination_class = None
def _fill_period_template(self, period_template, period_name):
period_template_copy = period_template.copy()
period_template_copy['period'] = period_name
return period_template_copy
def _preprocess_guestonline_response(self, response):
periods = response['periods']
periods_by_name = {period['period']: period for period in periods if 'period' in period}
if not periods_by_name:
raise ValueError('Empty guestonline response')
period_template = iter(periods_by_name.values()).__next__().copy()
period_template.pop('total_left_seats')
period_template['hours'] = []
period_template.pop('period')
processed_periods = [
periods_by_name[period_name]
if period_name in periods_by_name
else self._fill_period_template(period_template, period_name)
for period_name in CheckWhetherBookingAvailable._VALID_GUESTONLINE_PERIODS
]
unnamed_periods = filter(lambda period: 'period' not in period, periods)
for unnamed_period in unnamed_periods:
processed_periods.append(unnamed_period)
response['periods'] = sorted(processed_periods,
key=lambda x: self._GUESTONLINE_PERIODS_TO_PRIOR[x.get('period', 'lunch')])
return response
def get(self, request, *args, **kwargs):
is_booking_available = False
establishment = get_object_or_404(Establishment, pk=kwargs['establishment_id'])
@ -24,12 +64,12 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView):
date = request.query_params.get('date')
g_service = GuestonlineService()
l_service = LastableService()
if (not establishment.lastable_id is None) and l_service \
if establishment.lastable_id is not None and l_service \
.check_whether_booking_available(establishment.lastable_id, date):
is_booking_available = True
service = l_service
service.service_id = establishment.lastable_id
elif (not establishment.guestonline_id is None) and g_service \
elif establishment.guestonline_id is not None and g_service \
.check_whether_booking_available(establishment.guestonline_id,
**g_service.get_certain_keys(request.query_params,
{'date', 'persons'})):
@ -41,7 +81,11 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView):
'available': is_booking_available,
'type': service.service if service else None,
}
response.update({'details': service.response} if service and service.response else {})
service_response = self._preprocess_guestonline_response(service.response) \
if establishment.guestonline_id is not None \
else service.response
response.update({'details': service_response} if service and service.response else {})
return Response(data=response, status=200)
@ -97,8 +141,9 @@ class UpdatePendingBooking(generics.UpdateAPIView):
r = service.update_booking(service.get_certain_keys(data, {
'email', 'phone', 'last_name', 'first_name', 'country_code', 'pending_booking_id', 'note',
}, {
'email', 'phone', 'last_name', 'first_name', 'country_code', 'pending_booking_id',
}))
'email', 'phone', 'last_name', 'first_name',
'country_code', 'pending_booking_id',
}))
if isinstance(r, Response):
return r
if data.get('newsletter'):

View File

@ -1,8 +1,14 @@
from rest_framework import serializers
from collection import models
from collection.serializers.common import CollectionBaseSerializer
from establishment.models import Establishment
from location.models import Country
from location.serializers import CountrySimpleSerializer
from collection.serializers.common import CollectionBaseSerializer
from collection import models
from product.models import Product
from utils.exceptions import (
BindingObjectNotFound, RemovedBindingObjectNotFound, ObjectAlreadyAdded
)
class CollectionBackOfficeSerializer(CollectionBaseSerializer):
@ -31,3 +37,54 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer):
'start',
'end',
]
class CollectionBindObjectSerializer(serializers.Serializer):
"""Serializer for binding collection and objects"""
ESTABLISHMENT = 'establishment'
PRODUCT = 'product'
TYPE_CHOICES = (
(ESTABLISHMENT, 'Establishment'),
(PRODUCT, 'Product'),
)
type = serializers.ChoiceField(TYPE_CHOICES)
object_id = serializers.IntegerField()
def validate(self, attrs):
view = self.context.get('view')
request = self.context.get('request')
obj_type = attrs.get('type')
obj_id = attrs.get('object_id')
collection = view.get_object()
attrs['collection'] = collection
if obj_type == self.ESTABLISHMENT:
establishment = Establishment.objects.filter(pk=obj_id).\
first()
if not establishment:
raise BindingObjectNotFound()
if request.method == 'POST' and collection.establishments.\
filter(pk=establishment.pk).exists():
raise ObjectAlreadyAdded()
if request.method == 'DELETE' and not collection.\
establishments.filter(pk=establishment.pk).\
exists():
raise RemovedBindingObjectNotFound()
attrs['related_object'] = establishment
elif obj_type == self.PRODUCT:
product = Product.objects.filter(pk=obj_id).first()
if not product:
raise BindingObjectNotFound()
if request.method == 'POST' and collection.products.\
filter(pk=product.pk).exists():
raise ObjectAlreadyAdded()
if request.method == 'DELETE' and not collection.products.\
filter(pk=product.pk).exists():
raise RemovedBindingObjectNotFound()
attrs['related_object'] = product
return attrs

View File

@ -1,11 +1,10 @@
"""Collection common urlpaths."""
from django.urls import path
from rest_framework.routers import SimpleRouter
from collection.views import back as views
app_name = 'collection'
router = SimpleRouter()
router.register(r'', views.CollectionBackOfficeViewSet)
urlpatterns = [
path('', views.CollectionListCreateView.as_view(), name='list-create'),
path('<int:pk>/', views.CollectionRUDView.as_view(), name='rud-collection'),
]
urlpatterns = router.urls

View File

@ -1,19 +1,49 @@
from rest_framework import generics, permissions
from rest_framework import permissions
from rest_framework import viewsets, mixins
from collection import models
from collection.serializers import back
from collection.serializers import back as serializers
from utils.views import BindObjectMixin
class CollectionListCreateView(generics.ListCreateAPIView):
"""Collection list-create view."""
class CollectionViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
"""ViewSet for Collection model."""
pagination_class = None
permission_classes = (permissions.AllowAny,)
queryset = models.Collection.objects.all()
serializer_class = back.CollectionBackOfficeSerializer
# todo: conf. permissions by TT
permission_classes = (permissions.IsAuthenticated, )
serializer_class = serializers.CollectionBackOfficeSerializer
class CollectionRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Collection list-create view."""
class CollectionBackOfficeViewSet(mixins.CreateModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.RetrieveModelMixin,
BindObjectMixin,
CollectionViewSet):
"""ViewSet for Collection model for BackOffice users."""
permission_classes = (permissions.IsAuthenticated,)
queryset = models.Collection.objects.all()
serializer_class = back.CollectionBackOfficeSerializer
# todo: conf. permissions by TT
permission_classes = (permissions.IsAuthenticated, )
serializer_class = serializers.CollectionBackOfficeSerializer
bind_object_serializer_class = serializers.CollectionBindObjectSerializer
def perform_binding(self, serializer):
data = serializer.validated_data
collection = data.pop('collection')
obj_type = data.get('type')
related_object = data.get('related_object')
if obj_type == self.bind_object_serializer_class.ESTABLISHMENT:
collection.establishments.add(related_object)
elif obj_type == self.bind_object_serializer_class.PRODUCT:
collection.products.add(related_object)
def perform_unbinding(self, serializer):
data = serializer.validated_data
collection = data.pop('collection')
obj_type = data.get('type')
related_object = data.get('related_object')
if obj_type == self.bind_object_serializer_class.ESTABLISHMENT:
collection.establishments.remove(related_object)
elif obj_type == self.bind_object_serializer_class.PRODUCT:
collection.products.remove(related_object)

View File

@ -1,11 +1,10 @@
from rest_framework import generics
from rest_framework import permissions
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions, pagination
from collection import models
from utils.pagination import ProjectPageNumberPagination
from django.shortcuts import get_object_or_404
from establishment.serializers import EstablishmentBaseSerializer
from collection.serializers import common as serializers
from establishment.serializers import EstablishmentSimilarSerializer
from utils.pagination import ProjectPageNumberPagination, ProjectMobilePagination
# Mixins
@ -18,8 +17,8 @@ class CollectionViewMixin(generics.GenericAPIView):
def get_queryset(self):
"""Override get_queryset method."""
return models.Collection.objects.published() \
.by_country_code(code=self.request.country_code) \
.order_by('-on_top', '-modified')
.by_country_code(code=self.request.country_code) \
.order_by('-on_top', '-modified')
class GuideViewMixin(generics.GenericAPIView):
@ -40,7 +39,7 @@ class CollectionHomePageView(CollectionListView):
def get_queryset(self):
"""Override get_queryset."""
return super(CollectionHomePageView, self).get_queryset() \
.filter_all_related_gt(3)
.filter_all_related_gt(3)
class CollectionDetailView(CollectionViewMixin, generics.RetrieveAPIView):
@ -52,8 +51,8 @@ class CollectionDetailView(CollectionViewMixin, generics.RetrieveAPIView):
class CollectionEstablishmentListView(CollectionListView):
"""Retrieve list of establishment for collection."""
lookup_field = 'slug'
pagination_class = ProjectPageNumberPagination
serializer_class = EstablishmentBaseSerializer
pagination_class = ProjectMobilePagination
serializer_class = EstablishmentSimilarSerializer
def get_queryset(self):
"""
@ -66,7 +65,7 @@ class CollectionEstablishmentListView(CollectionListView):
# May raise a permission denied
self.check_object_permissions(self.request, collection)
return collection.establishments.all()
return collection.establishments.all().annotate_in_favorites(self.request.user)
# Guide

View File

@ -7,7 +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 product.models import Product, PurchasedProduct
from review import models as review_models
@ -32,6 +32,12 @@ class ContactPhoneInline(admin.TabularInline):
extra = 0
class GalleryImageInline(admin.TabularInline):
"""Gallery image inline admin."""
model = models.EstablishmentGallery
extra = 0
class ContactEmailInline(admin.TabularInline):
"""Contact email inline admin."""
model = models.ContactEmail
@ -53,12 +59,29 @@ class ProductInline(admin.TabularInline):
extra = 0
class CompanyInline(admin.TabularInline):
model = models.Company
extra = 0
class EstablishmentNote(admin.TabularInline):
model = models.EstablishmentNote
extra = 0
class PurchasedProduct(admin.TabularInline):
model = PurchasedProduct
extra = 0
@admin.register(models.Establishment)
class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""Establishment admin."""
list_display = ['id', '__str__', 'image_tag', ]
search_fields = ['id', 'name', 'index_name', 'slug']
list_filter = ['public_mark', 'toque_number']
inlines = [GalleryImageInline, CompanyInline, EstablishmentNote,
PurchasedProduct]
# inlines = [
# AwardInline, ContactPhoneInline, ContactEmailInline,
@ -107,3 +130,9 @@ class SocialChoiceAdmin(BaseModelAdminMixin, admin.ModelAdmin):
class SocialNetworkAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""Admin conf for SocialNetwork model."""
raw_id_fields = ('establishment',)
@admin.register(models.Company)
class CompanyAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""Admin conf for Company model."""
raw_id_fields = ['establishment', 'address', ]

View File

@ -0,0 +1,41 @@
from django.core.management.base import BaseCommand
from establishment.models import Establishment, EstablishmentGallery
from gallery.models import Image
import requests
from rest_framework import status
class Command(BaseCommand):
help = 'Fill establishment gallery from existing images'
def handle(self, *args, **kwargs):
count = 0
not_valid_link_counter = 0
not_valid_urls = []
cdn_prefix = 'https://1dc3f33f6d-3.optimicdn.com/gaultmillau.com/'
establishments = Establishment.objects.exclude(image_url__isnull=True) \
.exclude(preview_image_url__isnull=True)
for establishment in establishments:
image_url = establishment.image_url.rstrip()
relative_image_path = image_url[len(cdn_prefix):]
#response = requests.head(image_url, allow_redirects=True)
#if response.status_code != status.HTTP_200_OK:
# not_valid_link_counter += 1
# not_valid_urls.append(image_url)
image, image_created = Image.objects.get_or_create(
orientation=Image.HORIZONTAL,
title=f'{establishment.name} - {relative_image_path}',
image=relative_image_path)
gallery, _ = EstablishmentGallery.objects.get_or_create(establishment=establishment,
image=image,
is_main=True)
if image_created:
count += 1
self.stdout.write(self.style.WARNING(f'Created/updated {count} objects.\n'
f'Not valid link counter: {not_valid_link_counter}\n'
f'List of non valid image url: {not_valid_urls}'))

View File

@ -0,0 +1,38 @@
# Generated by Django 2.2.7 on 2019-11-17 11:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('gallery', '0006_merge_20191027_1758'),
('establishment', '0061_auto_20191114_0550'),
]
operations = [
migrations.AlterModelOptions(
name='establishmentnote',
options={'verbose_name': 'establishment note', 'verbose_name_plural': 'establishment notes'},
),
migrations.CreateModel(
name='EstablishmentGallery',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_main', models.BooleanField(default=False, verbose_name='Is the main image')),
('establishment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='establishment_gallery', to='establishment.Establishment', verbose_name='establishment')),
('image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='establishment_gallery', to='gallery.Image', verbose_name='image')),
],
options={
'verbose_name': 'establishment gallery',
'verbose_name_plural': 'establishment galleries',
'unique_together': {('establishment', 'is_main'), ('establishment', 'image')},
},
),
migrations.AddField(
model_name='establishment',
name='gallery',
field=models.ManyToManyField(through='establishment.EstablishmentGallery', to='gallery.Image'),
),
]

View File

@ -0,0 +1,40 @@
# Generated by Django 2.2.7 on 2019-11-18 14:19
import django.contrib.postgres.fields
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import phonenumber_field.modelfields
class Migration(migrations.Migration):
dependencies = [
('location', '0027_auto_20191118_1313'),
('establishment', '0062_auto_20191117_1117'),
]
operations = [
migrations.CreateModel(
name='Company',
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', models.CharField(max_length=255, verbose_name='name')),
('phones', django.contrib.postgres.fields.ArrayField(base_field=phonenumber_field.modelfields.PhoneNumberField(max_length=128), blank=True, default=None, null=True, size=None, verbose_name='contact phones')),
('faxes', django.contrib.postgres.fields.ArrayField(base_field=phonenumber_field.modelfields.PhoneNumberField(max_length=128), blank=True, default=None, null=True, size=None, verbose_name='fax numbers')),
('legal_entity', models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='legal entity')),
('registry_number', models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='registry number')),
('vat_number', models.CharField(blank=True, default=None, max_length=30, null=True, verbose_name='VAT identification number')),
('sic_code', models.IntegerField(blank=True, default=True, null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(9999)], verbose_name='sic code')),
('address', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='companies', to='location.Address', verbose_name='address')),
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='companies', to='establishment.Establishment', verbose_name='establishment')),
],
options={
'verbose_name': 'company',
'verbose_name_plural': 'companies',
},
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.7 on 2019-11-19 15:46
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('establishment', '0063_company'),
]
operations = [
migrations.AlterField(
model_name='establishmentnote',
name='establishment',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='notes', to='establishment.Establishment', verbose_name='establishment'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.7 on 2019-11-20 12:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0018_purchasedproduct'),
('establishment', '0064_auto_20191119_1546'),
]
operations = [
migrations.AddField(
model_name='establishment',
name='purchased_products',
field=models.ManyToManyField(blank=True, help_text='Attribute from legacy db.\nMust be deleted after the implementation of the market.', related_name='establishments', through='product.PurchasedProduct', to='product.Product', verbose_name='purchased plaques'),
),
]

View File

@ -9,7 +9,9 @@ from django.contrib.contenttypes import fields as generic
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import Distance as DistanceMeasure
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q
from django.utils import timezone
@ -22,7 +24,8 @@ from location.models import Address
from main.models import Award, Currency
from review.models import Review
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes)
TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
IntermediateGalleryModelMixin, HasTagsMixin)
# todo: establishment type&subtypes check
@ -316,9 +319,11 @@ class EstablishmentQuerySet(models.QuerySet):
return self.exclude(address__city__country__in=countries)
class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin, HasTagsMixin):
"""Establishment model."""
# todo: delete image URL fields after moving on gallery
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
name = models.CharField(_('name'), max_length=255, default='')
transliterated_name = models.CharField(default='', max_length=255,
@ -376,6 +381,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
related_name='establishments',
blank=True, default=None,
verbose_name=_('Collections'))
gallery = models.ManyToManyField('gallery.Image', through='EstablishmentGallery')
preview_image_url = models.URLField(verbose_name=_('Preview image URL path'), max_length=255,
blank=True, null=True, default=None)
slug = models.SlugField(unique=True, max_length=255, null=True,
@ -391,6 +397,13 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
currency = models.ForeignKey(Currency, blank=True, null=True, default=None,
on_delete=models.PROTECT,
verbose_name=_('currency'))
purchased_products = models.ManyToManyField('product.Product', blank=True,
through='product.PurchasedProduct',
related_name='establishments',
verbose_name=_('purchased plaques'),
help_text=_('Attribute from legacy db.\n'
'Must be deleted after the '
'implementation of the market.'))
objects = EstablishmentQuerySet.as_manager()
@ -403,6 +416,26 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
def __str__(self):
return f'id:{self.id}-{self.name}'
def clean_fields(self, exclude=None):
super().clean_fields(exclude)
if self.purchased_products.filter(product_type__index_name='souvenir').exists():
raise ValidationError(
_('Only souvenirs.'))
def delete(self, using=None, keep_parents=False):
"""Overridden delete method"""
# Delete all related companies
self.companies.all().delete()
# Delete all related notes
self.notes.all().delete()
return super().delete(using, keep_parents)
@property
def visible_tags(self):
return super().visible_tags\
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
'business_tag', 'business_tags_de'])\
# todo: recalculate toque_number
def recalculate_toque_number(self):
toque_number = 0
@ -544,6 +577,12 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
"""Return list products with type wine"""
return self.products.wines()
@property
def main_image(self):
qs = self.establishment_gallery.main_image()
if qs.exists():
return qs.first().image
class EstablishmentNoteQuerySet(models.QuerySet):
"""QuerySet for model EstablishmentNote."""
@ -554,7 +593,7 @@ class EstablishmentNote(ProjectBaseMixin):
old_id = models.PositiveIntegerField(null=True, blank=True)
text = models.TextField(verbose_name=_('text'))
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT,
related_name='establishment_notes',
related_name='notes',
verbose_name=_('establishment'))
user = models.ForeignKey('account.User', on_delete=models.PROTECT,
null=True,
@ -565,8 +604,26 @@ class EstablishmentNote(ProjectBaseMixin):
class Meta:
"""Meta class."""
verbose_name_plural = _('product note')
verbose_name = _('product notes')
verbose_name_plural = _('establishment notes')
verbose_name = _('establishment note')
class EstablishmentGallery(IntermediateGalleryModelMixin):
establishment = models.ForeignKey(Establishment, null=True,
related_name='establishment_gallery',
on_delete=models.CASCADE,
verbose_name=_('establishment'))
image = models.ForeignKey('gallery.Image', null=True,
related_name='establishment_gallery',
on_delete=models.CASCADE,
verbose_name=_('image'))
class Meta:
"""Meta class."""
verbose_name = _('establishment gallery')
verbose_name_plural = _('establishment galleries')
unique_together = (('establishment', 'is_main'), ('establishment', 'image'))
class Position(BaseAttributes, TranslatedFieldsMixin):
@ -837,3 +894,46 @@ class RatingStrategy(ProjectBaseMixin):
return f'{self.country.code if self.country else "Other country"}. ' \
f'"{self.toque_number}": {self.public_mark_min_value}-' \
f'{self.public_mark_max_value}'
class CompanyQuerySet(models.QuerySet):
"""QuerySet for model Company."""
class Company(ProjectBaseMixin):
"""Establishment company model."""
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT,
related_name='companies',
verbose_name=_('establishment'))
name = models.CharField(max_length=255, verbose_name=_('name'))
phones = ArrayField(PhoneNumberField(max_length=128),
blank=True, null=True, default=None,
verbose_name=_('contact phones'))
faxes = ArrayField(PhoneNumberField(max_length=128),
blank=True, null=True, default=None,
verbose_name=_('fax numbers'))
legal_entity = models.CharField(max_length=255,
blank=True, null=True, default=None,
verbose_name=_('legal entity'))
registry_number = models.CharField(max_length=255,
blank=True, null=True, default=None,
verbose_name=_('registry number'))
vat_number = models.CharField(max_length=30,
blank=True, null=True, default=None,
verbose_name=_('VAT identification number'))
sic_code = models.IntegerField(validators=[MinValueValidator(1),
MaxValueValidator(9999)],
blank=True, null=True, default=True,
verbose_name=_('sic code'))
address = models.ForeignKey(Address, on_delete=models.PROTECT,
blank=True, null=True, default=None,
related_name='companies',
verbose_name=_('address'))
objects = CompanyQuerySet.as_manager()
class Meta:
"""Meta class."""
verbose_name = _('company')
verbose_name_plural = _('companies')

View File

@ -1,27 +1,32 @@
from rest_framework import serializers
from establishment import models
from establishment.serializers import (
EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer,
ContactPhonesSerializer, SocialNetworkRelatedSerializers,
EstablishmentTypeBaseSerializer)
from establishment import serializers as model_serializers
from location.serializers import AddressDetailSerializer
from main.models import Currency
from utils.decorators import with_base_attributes
from utils.serializers import TimeZoneChoiceField
from gallery.models import Image
from django.utils.translation import gettext_lazy as _
from account.serializers.common import UserShortSerializer
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSerializer):
"""Establishment create serializer"""
type_id = serializers.PrimaryKeyRelatedField(
source='establishment_type',
queryset=models.EstablishmentType.objects.all(), write_only=True
queryset=models.EstablishmentType.objects.all(),
write_only=True
)
phones = ContactPhonesSerializer(read_only=True, many=True, )
emails = ContactEmailsSerializer(read_only=True, many=True, )
socials = SocialNetworkRelatedSerializers(read_only=True, many=True, )
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
phones = model_serializers.ContactPhonesSerializer(read_only=True,
many=True, )
emails = model_serializers.ContactEmailsSerializer(read_only=True,
many=True, )
socials = model_serializers.SocialNetworkRelatedSerializers(read_only=True,
many=True, )
type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type',
read_only=True)
tz = TimeZoneChoiceField()
class Meta:
@ -50,7 +55,7 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
]
class EstablishmentRUDSerializer(EstablishmentBaseSerializer):
class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
"""Establishment create serializer"""
type_id = serializers.PrimaryKeyRelatedField(
@ -58,10 +63,13 @@ class EstablishmentRUDSerializer(EstablishmentBaseSerializer):
queryset=models.EstablishmentType.objects.all(), write_only=True
)
address = AddressDetailSerializer()
phones = ContactPhonesSerializer(read_only=False, many=True, )
emails = ContactEmailsSerializer(read_only=False, many=True, )
socials = SocialNetworkRelatedSerializers(read_only=False, many=True, )
type = EstablishmentTypeBaseSerializer(source='establishment_type')
phones = model_serializers.ContactPhonesSerializer(read_only=False,
many=True, )
emails = model_serializers.ContactEmailsSerializer(read_only=False,
many=True, )
socials = model_serializers.SocialNetworkRelatedSerializers(read_only=False,
many=True, )
type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type')
class Meta:
model = models.Establishment
@ -105,7 +113,7 @@ class SocialNetworkSerializers(serializers.ModelSerializer):
]
class PlatesSerializers(PlateSerializer):
class PlatesSerializers(model_serializers.PlateSerializer):
"""Plates serializers."""
currency_id = serializers.PrimaryKeyRelatedField(
@ -117,14 +125,14 @@ class PlatesSerializers(PlateSerializer):
"""Meta class."""
model = models.Plate
fields = PlateSerializer.Meta.fields + [
fields = model_serializers.PlateSerializer.Meta.fields + [
'name',
'currency_id',
'menu'
]
class ContactPhoneBackSerializers(PlateSerializer):
class ContactPhoneBackSerializers(model_serializers.PlateSerializer):
"""ContactPhone serializers."""
class Meta:
@ -136,7 +144,7 @@ class ContactPhoneBackSerializers(PlateSerializer):
]
class ContactEmailBackSerializers(PlateSerializer):
class ContactEmailBackSerializers(model_serializers.PlateSerializer):
"""ContactEmail serializers."""
class Meta:
@ -160,3 +168,112 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
'user',
'name'
]
class EstablishmentBackOfficeGallerySerializer(serializers.ModelSerializer):
"""Serializer class for model EstablishmentGallery."""
class Meta:
"""Meta class"""
model = models.EstablishmentGallery
fields = [
'id',
'is_main',
]
def get_request_kwargs(self):
"""Get url kwargs from request."""
return self.context.get('request').parser_context.get('kwargs')
def validate(self, attrs):
"""Override validate method."""
establishment_pk = self.get_request_kwargs().get('pk')
image_id = self.get_request_kwargs().get('image_id')
establishment_qs = models.Establishment.objects.filter(pk=establishment_pk)
image_qs = Image.objects.filter(id=image_id)
if not establishment_qs.exists():
raise serializers.ValidationError({'detail': _('Establishment not found')})
if not image_qs.exists():
raise serializers.ValidationError({'detail': _('Image not found')})
establishment = establishment_qs.first()
image = image_qs.first()
if image in establishment.gallery.all():
raise serializers.ValidationError({'detail': _('Image is already added.')})
attrs['establishment'] = establishment
attrs['image'] = image
return attrs
class EstablishmentCompanyListCreateSerializer(model_serializers.CompanyBaseSerializer):
"""Serializer for linking page w/ advertisement."""
class Meta(model_serializers.CompanyBaseSerializer.Meta):
"""Meta class."""
model_serializers.CompanyBaseSerializer.Meta.extra_kwargs.update({
'establishment': {'required': False}
})
def create(self, validated_data):
"""Overridden create method."""
validated_data['establishment'] = self.context.get('view').get_object()
return super().create(validated_data)
class EstablishmentNoteBaseSerializer(serializers.ModelSerializer):
"""Serializer for model EstablishmentNote."""
user_detail = UserShortSerializer(read_only=True, source='user')
class Meta:
"""Meta class."""
model = models.EstablishmentNote
fields = [
'id',
'created',
'modified',
'text',
'user',
'user_detail',
'establishment',
]
extra_kwargs = {
'created': {'read_only': True},
'modified': {'read_only': True},
'establishment': {'required': False, 'write_only': True},
'user': {'required': False, 'write_only': True},
}
@property
def serializer_view(self):
"""Return view instance."""
return self.context.get('view')
class EstablishmentNoteListCreateSerializer(EstablishmentNoteBaseSerializer):
"""Serializer for List|Create action for model EstablishmentNote."""
def create(self, validated_data):
"""Overridden create method."""
validated_data['user'] = self.user
validated_data['establishment'] = self.establishment
return super().create(validated_data)
@property
def user(self):
"""Return user instance from view."""
if self.serializer_view:
return self.serializer_view.request.user
@property
def establishment(self):
"""Return establishment instance from view."""
if self.serializer_view:
return self.serializer_view.get_object()

View File

@ -1,22 +1,26 @@
"""Establishment serializers."""
from django.utils.translation import ugettext_lazy as _
from phonenumber_field.phonenumber import to_python as str_to_phonenumber
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 location.serializers import AddressBaseSerializer, CitySerializer, AddressDetailSerializer
from location.serializers import AddressBaseSerializer, CitySerializer, AddressDetailSerializer, \
CityShortSerializer
from main.serializers import AwardSerializer, CurrencySerializer
from review.serializers import ReviewShortSerializer
from tag.serializers import TagBaseSerializer
from timetable.serialziers import ScheduleRUDSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import ImageBaseSerializer
from utils.serializers import (ProjectModelSerializer, TranslatedField,
FavoritesCreateSerializer)
from review.serializers import ReviewShortSerializer
class ContactPhonesSerializer(serializers.ModelSerializer):
"""Contact phone serializer"""
class Meta:
model = models.ContactPhone
fields = [
@ -26,6 +30,7 @@ class ContactPhonesSerializer(serializers.ModelSerializer):
class ContactEmailsSerializer(serializers.ModelSerializer):
"""Contact email serializer"""
class Meta:
model = models.ContactEmail
fields = [
@ -35,6 +40,7 @@ class ContactEmailsSerializer(serializers.ModelSerializer):
class SocialNetworkRelatedSerializers(serializers.ModelSerializer):
"""Social network serializers."""
class Meta:
model = models.SocialNetwork
fields = [
@ -45,7 +51,6 @@ class SocialNetworkRelatedSerializers(serializers.ModelSerializer):
class PlateSerializer(ProjectModelSerializer):
name_translated = TranslatedField()
currency = CurrencySerializer(read_only=True)
@ -176,6 +181,7 @@ class EstablishmentShortSerializer(serializers.ModelSerializer):
city = CitySerializer(source='address.city', allow_null=True)
establishment_type = EstablishmentTypeGeoSerializer()
establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True)
currency = CurrencySerializer(read_only=True)
class Meta:
"""Meta class."""
@ -188,6 +194,31 @@ class EstablishmentShortSerializer(serializers.ModelSerializer):
'city',
'establishment_type',
'establishment_subtypes',
'currency',
]
class EstablishmentProductShortSerializer(serializers.ModelSerializer):
"""SHORT Serializer for displaying info about an establishment on product page."""
establishment_type = EstablishmentTypeGeoSerializer()
establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True)
address = AddressBaseSerializer()
city = CityShortSerializer(source='address.city', allow_null=True)
currency_detail = CurrencySerializer(source='currency', read_only=True)
class Meta:
"""Meta class."""
model = models.Establishment
fields = [
'id',
'name',
'index_name',
'slug',
'city',
'establishment_type',
'establishment_subtypes',
'address',
'currency_detail',
]
@ -206,13 +237,18 @@ class EstablishmentProductSerializer(EstablishmentShortSerializer):
class EstablishmentBaseSerializer(ProjectModelSerializer):
"""Base serializer for Establishment model."""
preview_image = serializers.URLField(source='preview_image_url')
address = AddressBaseSerializer()
in_favorites = serializers.BooleanField(allow_null=True)
tags = TagBaseSerializer(read_only=True, many=True)
tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags')
currency = CurrencySerializer()
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes')
image = serializers.URLField(source='image_url', read_only=True)
preview_image = serializers.URLField(source='preview_image_url',
allow_null=True,
read_only=True)
new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True)
class Meta:
"""Meta class."""
@ -227,13 +263,15 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
'toque_number',
'public_mark',
'slug',
'preview_image',
'in_favorites',
'address',
'tags',
'currency',
'type',
'subtypes',
'image',
'preview_image',
'new_image',
]
@ -272,7 +310,6 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
"""Serializer for Establishment model."""
description_translated = TranslatedField()
image = serializers.URLField(source='image_url')
awards = AwardSerializer(many=True)
schedule = ScheduleRUDSerializer(many=True, allow_null=True)
phones = ContactPhonesSerializer(read_only=True, many=True)
@ -288,6 +325,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
range_price_menu = RangePriceSerializer(read_only=True)
range_price_carte = RangePriceSerializer(read_only=True)
vintage_year = serializers.ReadOnlyField()
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
class Meta(EstablishmentBaseSerializer.Meta):
"""Meta class."""
@ -313,9 +351,16 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
'range_price_carte',
'transportation',
'vintage_year',
'gallery',
]
class EstablishmentSimilarSerializer(EstablishmentBaseSerializer):
"""Serializer for Establishment model."""
address = AddressDetailSerializer(read_only=True)
class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer):
"""Create comment serializer"""
mark = serializers.IntegerField()
@ -380,3 +425,56 @@ class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
})
return super().create(validated_data)
class CompanyBaseSerializer(serializers.ModelSerializer):
"""Company base serializer"""
phone_list = serializers.SerializerMethodField(source='phones', read_only=True)
fax_list = serializers.SerializerMethodField(source='faxes', read_only=True)
address_detail = AddressDetailSerializer(source='address', read_only=True)
class Meta:
"""Meta class."""
model = models.Company
fields = [
'id',
'establishment',
'name',
'phones',
'faxes',
'legal_entity',
'registry_number',
'vat_number',
'sic_code',
'address',
'phone_list',
'fax_list',
'address_detail',
]
extra_kwargs = {
'establishment': {'write_only': True},
'phones': {'write_only': True},
'faxes': {'write_only': True},
'address': {'write_only': True}
}
def get_phone_list(self, instance):
"""Return list of phone numbers."""
return instance.phones
def get_fax_list(self, instance):
"""Return list of fax numbers."""
return instance.faxes
def validate(self, attrs):
"""Overridden validate method"""
phones = [str_to_phonenumber(phone).as_national for phone in attrs.get('phones')]
faxes = [str_to_phonenumber(fax).as_national for fax in attrs.get('faxes')]
if faxes:
if models.Company.objects.filter(faxes__overlap=faxes).exists():
raise serializers.ValidationError({'detail': _('Fax is already reserved.')})
if phones:
if models.Company.objects.filter(phones__overlap=phones).exists():
raise serializers.ValidationError({'detail': _('Phones is already reserved.')})
return attrs

View File

@ -4,7 +4,9 @@ from django.db.models import Q, F
from establishment.models import Establishment
from location.models import Address
from transfer.models import Establishments, Dishes, EstablishmentNotes
from product.models import PurchasedProduct, Product
from transfer.models import Establishments, Dishes, EstablishmentNotes, \
EstablishmentMerchandises
from transfer.serializers.establishment import EstablishmentSerializer, \
EstablishmentNoteSerializer
from transfer.serializers.plate import PlateSerializer
@ -140,6 +142,43 @@ def transfer_establishment_note():
pprint(f"transfer_establishment_note errors: {errors}")
def transfer_purchased_plaques():
update_products_counter = 0
already_updated_counter = 0
not_existed_establishment_counter = 0
purchased = EstablishmentMerchandises.objects.values_list(
'establishment_id',
'merchandise__vintage',
'gifted',
'quantity'
)
for old_est_id, vintage, gifted, quantity in purchased:
establishment_qs = Establishment.objects.filter(old_id=old_est_id)
product_qs = Product.objects.filter(name='Plaque restaurants',
vintage=vintage)
if establishment_qs.exists() and product_qs.exists():
product = product_qs.first()
establishment = establishment_qs.first()
purchases, created = PurchasedProduct.objects.get_or_create(
establishment=establishment,
product=product,
is_gifted=gifted,
quantity=quantity
)
if created:
update_products_counter += 1
else:
already_updated_counter += 1
else:
not_existed_establishment_counter += 1
print(f'Updated products: {update_products_counter}\n'
f'Already updated: {already_updated_counter}\n'
f'Not existed establishment: {not_existed_establishment_counter}')
data_types = {
"establishment": [
transfer_establishment,
@ -149,4 +188,7 @@ data_types = {
transfer_establishment_addresses
],
"menu": [transfer_menu],
"purchased_plaques": [
transfer_purchased_plaques
],
}

View File

@ -13,6 +13,19 @@ urlpatterns = [
name='schedule-rud'),
path('<int:pk>/schedule/', views.EstablishmentScheduleCreateView.as_view(),
name='schedule-create'),
path('<int:pk>/gallery/', views.EstablishmentGalleryListView.as_view(),
name='gallery-list'),
path('<int:pk>/gallery/<int:image_id>/',
views.EstablishmentGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'),
path('<int:pk>/companies/', views.EstablishmentCompanyListCreateView.as_view(),
name='company-list-create'),
path('<int:pk>/companies/<int:company_pk>/', views.EstablishmentCompanyRUDView.as_view(),
name='company-rud'),
path('<int:pk>/notes/', views.EstablishmentNoteListCreateView.as_view(),
name='note-list-create'),
path('<int:pk>/notes/<int:note_pk>/', views.EstablishmentNoteRUDView.as_view(),
name='note-rud'),
path('menus/', views.MenuListCreateView.as_view(), name='menu-list'),
path('menus/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'),
path('plates/', views.PlateListCreateView.as_view(), name='plates'),

View File

@ -2,9 +2,13 @@
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
from establishment import filters, models, serializers
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
from utils.views import CreateDestroyGalleryViewMixin
from timetable.models import Timetable
from rest_framework import status
from rest_framework.response import Response
class EstablishmentMixinViews:
@ -33,13 +37,14 @@ class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView):
class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment schedule RUD view"""
serializer_class = ScheduleRUDSerializer
permission_classes = [IsEstablishmentManager]
def get_object(self):
"""
Returns the object the view is displaying.
"""
establishment_pk = self.kwargs['pk']
schedule_id = self.kwargs['schedule_id']
establishment_pk = self.kwargs.get('pk')
schedule_id = self.kwargs.get('schedule_id')
establishment = get_object_or_404(klass=models.Establishment.objects.all(),
pk=establishment_pk)
@ -56,6 +61,8 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
class EstablishmentScheduleCreateView(generics.CreateAPIView):
"""Establishment schedule Create view"""
serializer_class = ScheduleCreateSerializer
queryset = Timetable.objects.all()
permission_classes = [IsEstablishmentManager]
class MenuListCreateView(generics.ListCreateAPIView):
@ -184,3 +191,130 @@ class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment subtype retrieve/update/destroy view."""
serializer_class = serializers.EstablishmentSubTypeBaseSerializer
queryset = models.EstablishmentSubType.objects.all()
class EstablishmentGalleryCreateDestroyView(EstablishmentMixinViews,
CreateDestroyGalleryViewMixin):
"""Resource for a create|destroy gallery for establishment for back-office users."""
serializer_class = serializers.EstablishmentBackOfficeGallerySerializer
def get_object(self):
"""
Returns the object the view is displaying.
"""
establishment_qs = self.filter_queryset(self.get_queryset())
establishment = get_object_or_404(establishment_qs, pk=self.kwargs.get('pk'))
gallery = get_object_or_404(establishment.establishment_gallery,
image_id=self.kwargs.get('image_id'))
# May raise a permission denied
self.check_object_permissions(self.request, gallery)
return gallery
class EstablishmentGalleryListView(EstablishmentMixinViews,
generics.ListAPIView):
"""Resource for returning gallery for establishment for back-office users."""
serializer_class = serializers.ImageBaseSerializer
def get_object(self):
"""Override get_object method."""
qs = super(EstablishmentGalleryListView, self).get_queryset()
establishment = get_object_or_404(qs, pk=self.kwargs.get('pk'))
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
return establishment
def get_queryset(self):
"""Override get_queryset method."""
return self.get_object().crop_gallery
class EstablishmentCompanyListCreateView(EstablishmentMixinViews,
generics.ListCreateAPIView):
"""List|Create establishment company view."""
serializer_class = serializers.EstablishmentCompanyListCreateSerializer
def get_object(self):
"""Returns the object the view is displaying."""
establishment_qs = models.Establishment.objects.all()
filtered_ad_qs = self.filter_queryset(establishment_qs)
establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk'))
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
return establishment
def get_queryset(self):
"""Overridden get_queryset method."""
return self.get_object().companies.all()
class EstablishmentCompanyRUDView(EstablishmentMixinViews,
generics.RetrieveUpdateDestroyAPIView):
"""Create|Retrieve|Update|Destroy establishment company view."""
serializer_class = serializers.CompanyBaseSerializer
def get_object(self):
"""Returns the object the view is displaying."""
establishment_qs = models.Establishment.objects.all()
filtered_ad_qs = self.filter_queryset(establishment_qs)
establishment = get_object_or_404(filtered_ad_qs, pk=self.kwargs.get('pk'))
company = get_object_or_404(establishment.companies.all(), pk=self.kwargs.get('company_pk'))
# May raise a permission denied
self.check_object_permissions(self.request, company)
return company
class EstablishmentNoteListCreateView(EstablishmentMixinViews,
generics.ListCreateAPIView):
"""Retrieve|Update|Destroy establishment note view."""
serializer_class = serializers.EstablishmentNoteListCreateSerializer
def get_object(self):
"""Returns the object the view is displaying."""
establishment_qs = models.Establishment.objects.all()
filtered_establishment_qs = self.filter_queryset(establishment_qs)
establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs.get('pk'))
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
return establishment
def get_queryset(self):
"""Overridden get_queryset method."""
return self.get_object().notes.all()
class EstablishmentNoteRUDView(EstablishmentMixinViews,
generics.RetrieveUpdateDestroyAPIView):
"""Create|Retrieve|Update|Destroy establishment note view."""
serializer_class = serializers.EstablishmentNoteBaseSerializer
def get_object(self):
"""Returns the object the view is displaying."""
establishment_qs = models.Establishment.objects.all()
filtered_establishment_qs = self.filter_queryset(establishment_qs)
establishment = get_object_or_404(filtered_establishment_qs, pk=self.kwargs.get('pk'))
note = get_object_or_404(establishment.notes.all(), pk=self.kwargs['note_pk'])
# May raise a permission denied
self.check_object_permissions(self.request, note)
return note

View File

@ -72,7 +72,7 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
class EstablishmentSimilarListView(EstablishmentListView):
"""Resource for getting a list of establishments."""
serializer_class = serializers.EstablishmentBaseSerializer
serializer_class = serializers.EstablishmentSimilarSerializer
pagination_class = EstablishmentPortionPagination
def get_queryset(self):

View File

@ -2,7 +2,7 @@
from rest_framework import generics
from establishment.models import Establishment
from establishment.filters import EstablishmentFilter
from establishment.serializers import EstablishmentBaseSerializer
from establishment.serializers import EstablishmentBaseSerializer, EstablishmentSimilarSerializer
from news.filters import NewsListFilterSet
from news.models import News
from news.serializers import NewsBaseSerializer, NewsListSerializer
@ -23,7 +23,7 @@ class FavoritesBaseView(generics.GenericAPIView):
class FavoritesEstablishmentListView(generics.ListAPIView):
"""List views for establishments in favorites."""
serializer_class = EstablishmentBaseSerializer
serializer_class = EstablishmentSimilarSerializer
filter_class = EstablishmentFilter
def get_queryset(self):

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.4 on 2019-11-18 10:11
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('location', '0026_country_is_active'),
]
operations = [
migrations.AlterField(
model_name='winesubregion',
name='wine_region',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='wine_sub_region', to='location.WineRegion', verbose_name='wine sub region'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.7 on 2019-11-18 13:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('location', '0026_country_is_active'),
]
operations = [
migrations.AlterField(
model_name='winesubregion',
name='wine_region',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='wine_sub_region', to='location.WineRegion', verbose_name='wine sub region'),
),
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.7 on 2019-11-18 15:07
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('location', '0027_auto_20191118_1313'),
('location', '0027_auto_20191118_1011'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.7 on 2019-11-19 06:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('location', '0027_auto_20191118_1313'),
('location', '0027_auto_20191118_1011'),
]
operations = [
]

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.7 on 2019-11-19 14:38
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('location', '0028_merge_20191119_0647'),
('location', '0028_merge_20191118_1507'),
]
operations = [
]

View File

@ -0,0 +1,34 @@
# Generated by Django 2.2.7 on 2019-11-20 10:10
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('gallery', '0006_merge_20191027_1758'),
('location', '0029_merge_20191119_1438'),
]
operations = [
migrations.CreateModel(
name='CityGallery',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_main', models.BooleanField(default=False, verbose_name='Is the main image')),
('city', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='city_gallery', to='location.City', verbose_name='city')),
('image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='city_gallery', to='gallery.Image', verbose_name='image')),
],
options={
'verbose_name': 'city gallery',
'verbose_name_plural': 'city galleries',
'unique_together': {('city', 'is_main'), ('city', 'image')},
},
),
migrations.AddField(
model_name='city',
name='gallery',
field=models.ManyToManyField(through='location.CityGallery', to='gallery.Image'),
),
]

View File

@ -10,7 +10,8 @@ from django.contrib.postgres.fields import ArrayField
from translation.models import Language
from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField,
TranslatedFieldsMixin, get_current_locale)
TranslatedFieldsMixin, get_current_locale,
IntermediateGalleryModelMixin, GalleryModelMixin)
class CountryQuerySet(models.QuerySet):
@ -101,9 +102,8 @@ class CityQuerySet(models.QuerySet):
return self.filter(country__code=code)
class City(models.Model):
class City(GalleryModelMixin):
"""Region model."""
name = models.CharField(_('name'), max_length=250)
name_translated = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Translated name'), help_text='{"en-GB":"some text"}')
@ -138,16 +138,8 @@ class City(models.Model):
return self.name
class CityGalleryQuerySet(models.QuerySet):
"""QuerySet for model News"""
def main_image(self):
"""Return objects with flag is_main is True"""
return self.filter(is_main=True)
class CityGallery(models.Model):
old_id = models.IntegerField(blank=True, null=True)
class CityGallery(IntermediateGalleryModelMixin):
"""Gallery for model City."""
city = models.ForeignKey(City, null=True,
related_name='city_gallery',
on_delete=models.CASCADE,
@ -155,14 +147,10 @@ class CityGallery(models.Model):
image = models.ForeignKey('gallery.Image', null=True,
related_name='city_gallery',
on_delete=models.CASCADE,
verbose_name=_('gallery'))
is_main = models.BooleanField(default=False,
verbose_name=_('Is the main image'))
objects = CityGalleryQuerySet.as_manager()
verbose_name=_('image'))
class Meta:
"""NewsGallery meta class."""
"""CityGallery meta class."""
verbose_name = _('city gallery')
verbose_name_plural = _('city galleries')
unique_together = (('city', 'is_main'), ('city', 'image'))
@ -216,6 +204,13 @@ class Address(models.Model):
class WineRegionQuerySet(models.QuerySet):
"""Wine region queryset."""
def with_sub_region_related(self):
return self.prefetch_related('wine_sub_region')
def having_wines(self, value = True):
"""Return qs with regions, which have any wine related to them"""
return self.exclude(wines__isnull=value)
class WineRegion(models.Model, TranslatedFieldsMixin):
"""Wine region model."""
@ -254,6 +249,7 @@ class WineSubRegion(models.Model):
"""Wine sub region model."""
name = models.CharField(_('name'), max_length=255)
wine_region = models.ForeignKey(WineRegion, on_delete=models.PROTECT,
related_name='wine_sub_region',
verbose_name=_('wine sub region'))
old_id = models.PositiveIntegerField(_('old id'), default=None,
blank=True, null=True)

View File

@ -1,5 +1,8 @@
from location import models
from location.serializers import common
from rest_framework import serializers
from gallery.models import Image
from django.utils.translation import gettext_lazy as _
class AddressCreateSerializer(common.AddressDetailSerializer):
@ -18,3 +21,45 @@ class CountryBackSerializer(common.CountrySerializer):
'name',
'country_id'
]
class CityGallerySerializer(serializers.ModelSerializer):
"""Serializer class for model CityGallery."""
class Meta:
"""Meta class"""
model = models.CityGallery
fields = [
'id',
'is_main',
]
def get_request_kwargs(self):
"""Get url kwargs from request."""
return self.context.get('request').parser_context.get('kwargs')
def validate(self, attrs):
"""Override validate method."""
city_pk = self.get_request_kwargs().get('pk')
image_id = self.get_request_kwargs().get('image_id')
city_qs = models.City.objects.filter(pk=city_pk)
image_qs = Image.objects.filter(id=image_id)
if not city_qs.exists():
raise serializers.ValidationError({'detail': _('City not found')})
if not image_qs.exists():
raise serializers.ValidationError({'detail': _('Image not found')})
city = city_qs.first()
image = image_qs.first()
if image in city.gallery.all():
raise serializers.ValidationError({'detail': _('Image is already added.')})
attrs['city'] = city
attrs['image'] = image
return attrs

View File

@ -55,6 +55,20 @@ class RegionSerializer(serializers.ModelSerializer):
'country_id'
]
class CityShortSerializer(serializers.ModelSerializer):
"""Short city serializer"""
country = CountrySerializer(read_only=True)
class Meta:
"""Meta class"""
model = models.City
fields = (
'id',
'name',
'code',
'country',
)
class CropImageSerializer(serializers.Serializer):
"""Serializer for crop images for City object."""
@ -273,3 +287,14 @@ class WineSubRegionBaseSerializer(serializers.ModelSerializer):
'id',
'name',
]
class WineRegionSerializer(WineRegionBaseSerializer):
"""Wine region w/ subregion serializer"""
wine_sub_region = WineSubRegionBaseSerializer(allow_null=True, many=True)
class Meta(WineRegionBaseSerializer.Meta):
fields = WineRegionBaseSerializer.Meta.fields + [
'wine_sub_region'
]

View File

@ -1,9 +1,12 @@
from transfer.serializers import location as location_serializers
from transfer import models as transfer_models
from location.models import Country, Region, City, Address, WineRegion
from location.models import Country, Region, City, Address, WineRegion, CityGallery
from pprint import pprint
import json
from gallery.models import Image
from pprint import pprint
from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned
from collection.models import Collection
@ -494,6 +497,42 @@ def fix_location_models():
fix_chosen_tag()
def transfer_city_gallery():
created_counter = 0
cities_not_exists = {}
gallery_obj_exists_counter = 0
city_gallery = transfer_models.CityPhotos.objects.exclude(city__isnull=True) \
.exclude(city__country_code_2__isnull=True) \
.exclude(city__country_code_2__iexact='') \
.exclude(city__region_code__isnull=True) \
.exclude(city__region_code__iexact='') \
.values_list('city_id', 'attachment_suffix_url')
for old_city_id, image_suffix_url in city_gallery:
city = City.objects.filter(old_id=old_city_id)
if city.exists():
city = city.first()
image, _ = Image.objects.get_or_create(image=image_suffix_url,
defaults={
'image': image_suffix_url,
'orientation': Image.HORIZONTAL,
'title': f'{city.name} - {image_suffix_url}',
})
city_gallery, created = CityGallery.objects.get_or_create(image=image,
city=city,
is_main=True)
if created:
created_counter += 1
else:
gallery_obj_exists_counter += 1
else:
cities_not_exists.update({'city_old_id': old_city_id})
print(f'Created: {created_counter}\n'
f'City not exists: {cities_not_exists}\n'
f'Already added: {gallery_obj_exists_counter}')
data_types = {
"dictionaries": [
transfer_countries,
@ -515,6 +554,8 @@ data_types = {
],
"fix_location": [
fix_location_models
]
],
"fill_city_gallery": [transfer_city_gallery]
}

View File

@ -11,6 +11,11 @@ urlpatterns = [
path('cities/', views.CityListCreateView.as_view(), name='city-list-create'),
path('cities/<int:pk>/', views.CityRUDView.as_view(), name='city-retrieve'),
path('cities/<int:pk>/gallery/', views.CityGalleryListView.as_view(),
name='gallery-list'),
path('cities/<int:pk>/gallery/<int:image_id>/',
views.CityGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'),
path('countries/', views.CountryListCreateView.as_view(), name='country-list-create'),
path('countries/<int:pk>/', views.CountryRUDView.as_view(), name='country-retrieve'),

View File

@ -22,4 +22,6 @@ urlpatterns = [
path('regions/', views.RegionListView.as_view(), name='region-list'),
path('regions/<int:pk>/', views.RegionRetrieveView.as_view(), name='region-retrieve'),
path('wine-regions/', views.WineRegionListView.as_view(), name='wine-region-list'),
]

View File

@ -4,7 +4,11 @@ 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
from utils.views import CreateDestroyGalleryViewMixin
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django.shortcuts import get_object_or_404
from utils.serializers import ImageBaseSerializer
# Address
@ -35,6 +39,48 @@ class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
class CityGalleryCreateDestroyView(common.CityViewMixin,
CreateDestroyGalleryViewMixin):
"""Resource for a create gallery for product for back-office users."""
serializer_class = serializers.CityGallerySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
def get_object(self):
"""
Returns the object the view is displaying.
"""
city_qs = self.filter_queryset(self.get_queryset())
city = get_object_or_404(city_qs, pk=self.kwargs.get('pk'))
gallery = get_object_or_404(city.city_gallery, image_id=self.kwargs.get('image_id'))
# May raise a permission denied
self.check_object_permissions(self.request, gallery)
return gallery
class CityGalleryListView(common.CityViewMixin,
generics.ListAPIView):
"""Resource for returning gallery for product for back-office users."""
serializer_class = ImageBaseSerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
def get_object(self):
"""Override get_object method."""
qs = super(CityGalleryListView, self).get_queryset()
city = get_object_or_404(qs, pk=self.kwargs['pk'])
# May raise a permission denied
self.check_object_permissions(self.request, city)
return city
def get_queryset(self):
"""Override get_queryset method."""
return self.get_object().crop_gallery
# Region
class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region"""

View File

@ -5,8 +5,11 @@ from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions, status
from rest_framework.response import Response
from gallery.tasks import delete_image
from rest_framework import generics
from rest_framework import permissions
from django.db.models.expressions import RawSQL
from location import models, serializers
from utils.models import get_current_locale
# Mixins
@ -41,7 +44,9 @@ class CountryListView(CountryViewMixin, generics.ListAPIView):
"""List view for model Country."""
pagination_class = None
def get_queryset(self):
qs = super().get_queryset().order_by(RawSQL("name->>%s", (get_current_locale(),)))
return qs
class CountryRetrieveView(CountryViewMixin, generics.RetrieveAPIView):
"""Retrieve view for model Country."""
@ -64,6 +69,15 @@ class RegionListView(RegionViewMixin, generics.ListAPIView):
serializer_class = serializers.CountrySerializer
class WineRegionListView(generics.ListAPIView):
"""List view for model WineRegion"""
pagination_class = None
model = models.WineRegion
permission_classes = (permissions.AllowAny,)
queryset = models.WineRegion.objects.with_sub_region_related().having_wines()
serializer_class = serializers.WineRegionSerializer
class RegionDestroyView(RegionViewMixin, generics.DestroyAPIView):
"""Destroy view for model Country"""
serializer_class = serializers.CountrySerializer

View File

@ -40,3 +40,8 @@ class CarouselAdmin(admin.ModelAdmin):
@admin.register(models.PageType)
class PageTypeAdmin(admin.ModelAdmin):
"""PageType admin."""
@admin.register(models.Page)
class PageAdmin(admin.ModelAdmin):
"""Page admin."""

40
apps/main/filters.py Normal file
View File

@ -0,0 +1,40 @@
from django.core.validators import EMPTY_VALUES
from django_filters import rest_framework as filters
from main import models
class AwardFilter(filters.FilterSet):
"""Award filter set."""
establishment_id = filters.NumberFilter(field_name='object_id', )
product_id = filters.NumberFilter(field_name='object_id', )
employee_id = filters.NumberFilter(field_name='object_id', )
class Meta:
"""Meta class."""
model = models.Award
fields = (
'establishment_id',
'product_id',
'employee_id',
'state',
'award_type',
'vintage_year',
)
def by_establishment_id(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_establishment_id(value, content_type='establishment')
return queryset
def by_product_id(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_product_id(value, content_type='product')
return queryset
def by_employee_id(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_employee_id(value, content_type='establishmentemployee')
return queryset

View File

@ -153,7 +153,7 @@ class Award(TranslatedFieldsMixin, URLImageMixin, models.Model):
PUBLISHED = 1
STATE_CHOICES = (
(WAITING,'waiting'),
(WAITING, 'waiting'),
(PUBLISHED, 'published')
)

View File

@ -38,7 +38,7 @@ class SiteFeatureSerializer(serializers.ModelSerializer):
'route',
'source',
'nested',
)
)
class CurrencySerializer(ProjectModelSerializer):
@ -145,6 +145,19 @@ class AwardSerializer(AwardBaseSerializer):
fields = AwardBaseSerializer.Meta.fields + ['award_type', ]
class BackAwardSerializer(AwardBaseSerializer):
"""Award serializer."""
class Meta:
model = models.Award
fields = AwardBaseSerializer.Meta.fields + [
'award_type',
'state',
'content_type',
'object_id',
]
class CarouselListSerializer(serializers.ModelSerializer):
"""Serializer for retrieving list of carousel items."""
@ -186,7 +199,11 @@ class PageBaseSerializer(serializers.ModelSerializer):
'image_url',
'width',
'height',
'advertisement',
]
extra_kwargs = {
'establishment': {'write_only': True}
}
class PageTypeBaseSerializer(serializers.ModelSerializer):
@ -198,4 +215,4 @@ class PageTypeBaseSerializer(serializers.ModelSerializer):
fields = [
'id',
'name',
]
]

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

View File

@ -0,0 +1,73 @@
from http.cookies import SimpleCookie
from django.contrib.contenttypes.models import ContentType
from rest_framework import status
from rest_framework.test import APITestCase
from account.models import User
from location.models import Country
from main.models import Award, AwardType
class AwardTestCase(APITestCase):
def setUp(self):
self.user = User.objects.create_user(
username='alex',
email='alex@mail.com',
password='alex_password',
is_staff=True,
)
# 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')})
self.country_ru = Country.objects.create(
name={'en-GB': 'Russian'},
code='RU',
)
self.content_type = ContentType.objects.get(app_label="establishment", model="establishment")
self.award_type = AwardType.objects.create(
country=self.country_ru,
name="Test award type",
)
self.award = Award.objects.create(
award_type=self.award_type,
vintage_year='2017',
state=Award.PUBLISHED,
object_id=1,
content_type_id=1,
)
def test_award_CRUD(self):
response = self.client.get('/api/back/main/awards/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
data = {
'award_type': self.award_type.pk,
'state': 1,
'object_id': 1,
'content_type': 1,
}
response = self.client.post('/api/back/main/awards/', data=data)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
response = self.client.get(f'/api/back/main/awards/{self.award.id}/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
update_data = {
'vintage_year': '2019'
}
response = self.client.patch(f'/api/back/main/awards/{self.award.id}/', data=update_data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.delete(f'/api/back/main/awards/{self.award.id}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)

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

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

21
apps/main/views/back.py Normal file
View File

@ -0,0 +1,21 @@
from rest_framework import generics, permissions
from main import serializers
from main.filters import AwardFilter
from main.models import Award
class AwardLstView(generics.ListCreateAPIView):
"""Award list create view."""
queryset = Award.objects.all()
serializer_class = serializers.BackAwardSerializer
permission_classes = (permissions.IsAdminUser,)
filterset_class = AwardFilter
class AwardRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Award RUD view."""
queryset = Award.objects.all()
serializer_class = serializers.BackAwardSerializer
permission_classes = (permissions.IsAdminUser,)
lookup_field = 'id'

View File

@ -24,12 +24,19 @@ def send_email_action(modeladmin, request, queryset):
send_email_action.short_description = "Send the selected news by email"
class NewsGalleryInline(admin.TabularInline):
"""News gallery inline."""
model = models.NewsGallery
extra = 0
@admin.register(models.News)
class NewsAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""News admin."""
raw_id_fields = ('address',)
actions = [send_email_action]
raw_id_fields = ('news_type', 'address', 'country')
inlines = [NewsGalleryInline, ]
@admin.register(models.NewsGallery)

View File

@ -16,6 +16,9 @@ class NewsListFilterSet(filters.FilterSet):
),
method='by_tag_group'
)
tag_value__exclude = filters.CharFilter(method='exclude_tags')
tag_value__in = filters.CharFilter(method='in_tags')
type = filters.CharFilter(method='by_type')
class Meta:
"""Meta class"""
@ -24,8 +27,18 @@ class NewsListFilterSet(filters.FilterSet):
'title',
'is_highlighted',
'tag_group',
'tag_value__exclude',
'tag_value__in',
)
def in_tags(self, queryset, name, value):
tags = value.split('__')
return queryset.filter(tags__value__in=tags)
def exclude_tags(self, queryset, name, value):
tags = value.split('__')
return queryset.exclude(tags__value__in=tags)
def by_tag_group(self, queryset, name, value):
if value == models.News.RECIPES_TAG_VALUE:
queryset = queryset.recipe_news()
@ -39,3 +52,9 @@ class NewsListFilterSet(filters.FilterSet):
return queryset.filter(**filters)
else:
return queryset
def by_type(self, queryset, name, value):
if value:
return queryset.filter(news_type__name=value)
else:
return queryset

View File

@ -7,8 +7,10 @@ from django.utils.translation import gettext_lazy as _
from rest_framework.reverse import reverse
from rating.models import Rating, ViewCount
from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin, ProjectBaseMixin
from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin,
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin)
from utils.querysets import TranslationQuerysetMixin
from django.conf import settings
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
@ -86,6 +88,10 @@ class NewsQuerySet(TranslationQuerysetMixin):
"""Returns news with tag 'cook' qs."""
return self.filter(tags__value=News.RECIPES_TAG_VALUE)
def international_news(self):
"""Returns only international news qs."""
return self.filter(tags__value=News.INTERNATIONAL_TAG_VALUE)
def published(self):
"""Return only published news"""
now = timezone.now()
@ -120,7 +126,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
)
class News(BaseAttributes, TranslatedFieldsMixin):
class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin):
"""News model."""
STR_FIELD_NAME = 'title'
@ -151,6 +157,7 @@ class News(BaseAttributes, TranslatedFieldsMixin):
(PUBLISHED_EXCLUSIVE, _('Published exclusive')),
)
INTERNATIONAL_TAG_VALUE = 'international'
RECIPES_TAG_VALUE = 'cook'
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
@ -242,16 +249,51 @@ class News(BaseAttributes, TranslatedFieldsMixin):
count_value = self.views_count.count
return count_value
# todo: remove in future
@property
def crop_gallery(self):
if hasattr(self, 'gallery'):
gallery = []
images = self.gallery.all()
model_name = self._meta.model_name.lower()
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
if p.startswith(model_name)]
for image in images:
d = {
'id': image.id,
'title': image.title,
'original_url': image.image.url,
'orientation_display': image.get_orientation_display(),
'auto_crop_images': {},
}
for crop in crop_parameters:
d['auto_crop_images'].update(
{f'{crop[len(f"{model_name}_"):]}_url': image.get_image_url(crop)})
gallery.append(d)
return gallery
class NewsGalleryQuerySet(models.QuerySet):
"""QuerySet for model News"""
def main_image(self):
"""Return objects with flag is_main is True"""
return self.filter(is_main=True)
@property
def crop_main_image(self):
if hasattr(self, 'main_image') and self.main_image:
image = self.main_image
model_name = self._meta.model_name.lower()
image_property = {
'id': image.id,
'title': image.title,
'original_url': image.image.url,
'orientation_display': image.get_orientation_display(),
'auto_crop_images': {},
}
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
if p.startswith(self._meta.model_name.lower())]
for crop in crop_parameters:
image_property['auto_crop_images'].update(
{f'{crop[len(f"{model_name}_"):]}_url': image.get_image_url(crop)})
return image_property
class NewsGallery(models.Model):
class NewsGallery(IntermediateGalleryModelMixin):
news = models.ForeignKey(News, null=True,
related_name='news_gallery',
on_delete=models.CASCADE,
@ -260,10 +302,6 @@ class NewsGallery(models.Model):
related_name='news_gallery',
on_delete=models.CASCADE,
verbose_name=_('gallery'))
is_main = models.BooleanField(default=False,
verbose_name=_('Is the main image'))
objects = NewsGalleryQuerySet.as_manager()
class Meta:
"""NewsGallery meta class."""

View File

@ -10,7 +10,8 @@ from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
from news import models
from tag.serializers import TagBaseSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import TranslatedField, ProjectModelSerializer, FavoritesCreateSerializer
from utils.serializers import (TranslatedField, ProjectModelSerializer,
FavoritesCreateSerializer, ImageBaseSerializer)
class AgendaSerializer(ProjectModelSerializer):
@ -47,78 +48,6 @@ class NewsBannerSerializer(ProjectModelSerializer):
)
class CropImageSerializer(serializers.Serializer):
"""Serializer for crop images for News object."""
preview_url = serializers.SerializerMethodField()
promo_horizontal_web_url = serializers.SerializerMethodField()
promo_horizontal_mobile_url = serializers.SerializerMethodField()
tile_horizontal_web_url = serializers.SerializerMethodField()
tile_horizontal_mobile_url = serializers.SerializerMethodField()
tile_vertical_web_url = serializers.SerializerMethodField()
highlight_vertical_web_url = serializers.SerializerMethodField()
editor_web_url = serializers.SerializerMethodField()
editor_mobile_url = serializers.SerializerMethodField()
def get_preview_url(self, obj):
"""Get crop preview."""
return obj.instance.get_image_url('news_preview')
def get_promo_horizontal_web_url(self, obj):
"""Get crop promo_horizontal_web."""
return obj.instance.get_image_url('news_promo_horizontal_web')
def get_promo_horizontal_mobile_url(self, obj):
"""Get crop promo_horizontal_mobile."""
return obj.instance.get_image_url('news_promo_horizontal_mobile')
def get_tile_horizontal_web_url(self, obj):
"""Get crop tile_horizontal_web."""
return obj.instance.get_image_url('news_tile_horizontal_web')
def get_tile_horizontal_mobile_url(self, obj):
"""Get crop tile_horizontal_mobile."""
return obj.instance.get_image_url('news_tile_horizontal_mobile')
def get_tile_vertical_web_url(self, obj):
"""Get crop tile_vertical_web."""
return obj.instance.get_image_url('news_tile_vertical_web')
def get_highlight_vertical_web_url(self, obj):
"""Get crop highlight_vertical_web."""
return obj.instance.get_image_url('news_highlight_vertical_web')
def get_editor_web_url(self, obj):
"""Get crop editor_web."""
return obj.instance.get_image_url('news_editor_web')
def get_editor_mobile_url(self, obj):
"""Get crop editor_mobile."""
return obj.instance.get_image_url('news_editor_mobile')
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')
auto_crop_images = CropImageSerializer(source='image', allow_null=True)
class Meta:
model = Image
fields = [
'id',
'title',
'orientation_display',
'original_url',
'auto_crop_images',
]
extra_kwargs = {
'orientation': {'write_only': True}
}
class NewsTypeSerializer(serializers.ModelSerializer):
"""News type serializer."""
@ -135,7 +64,7 @@ class NewsBaseSerializer(ProjectModelSerializer):
title_translated = TranslatedField()
subtitle_translated = TranslatedField()
news_type = NewsTypeSerializer(read_only=True)
tags = TagBaseSerializer(read_only=True, many=True)
tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags')
in_favorites = serializers.BooleanField(allow_null=True)
view_counter = serializers.IntegerField(read_only=True)
@ -170,7 +99,7 @@ class NewsSimilarListSerializer(NewsBaseSerializer):
class NewsListSerializer(NewsBaseSerializer):
"""List serializer for News model."""
image = NewsImageSerializer(source='main_image', allow_null=True)
image = ImageBaseSerializer(source='crop_main_image', allow_null=True)
class Meta(NewsBaseSerializer.Meta):
"""Meta class."""
@ -188,7 +117,7 @@ class NewsDetailSerializer(NewsBaseSerializer):
author = UserBaseSerializer(source='created_by', read_only=True)
state_display = serializers.CharField(source='get_state_display',
read_only=True)
gallery = NewsImageSerializer(read_only=True, many=True)
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
class Meta(NewsBaseSerializer.Meta):
"""Meta class."""

View File

@ -1,15 +1,13 @@
"""News app views."""
from django.conf import settings
from django.db.transaction import on_commit
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions, status
from rest_framework.response import Response
from rest_framework import generics, permissions
from gallery.tasks import delete_image
from news import filters, models, serializers
from rating.tasks import add_rating
from utils.permissions import IsCountryAdmin, IsContentPageManager
from utils.views import CreateDestroyGalleryViewMixin
from utils.serializers import ImageBaseSerializer
class NewsMixinView:
@ -18,7 +16,7 @@ class NewsMixinView:
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.NewsBaseSerializer
def get_queryset(self):
def get_queryset(self, *args, **kwargs):
"""Override get_queryset method."""
qs = models.News.objects.published() \
.with_base_related() \
@ -27,7 +25,10 @@ class NewsMixinView:
country_code = self.request.country_code
if country_code:
qs = qs.by_country_code(country_code)
if kwargs.get('international_preferred') and country_code in settings.INTERNATIONAL_COUNTRY_CODES:
qs = qs.international_news()
else:
qs = qs.by_country_code(country_code)
return qs
@ -37,6 +38,10 @@ class NewsListView(NewsMixinView, generics.ListAPIView):
serializer_class = serializers.NewsListSerializer
filter_class = filters.NewsListFilterSet
def get_queryset(self, *args, **kwargs):
kwargs.update({'international_preferred': True})
return super().get_queryset(*args, **kwargs)
class NewsDetailView(NewsMixinView, generics.RetrieveAPIView):
"""News detail view."""
@ -77,6 +82,7 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
"""Resource for a list of news for back-office users."""
serializer_class = serializers.NewsBackOfficeBaseSerializer
filter_class = filters.NewsListFilterSet
create_serializers_class = serializers.NewsBackOfficeDetailSerializer
permission_classes = [IsCountryAdmin | IsContentPageManager]
@ -102,8 +108,8 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
"""
news_qs = self.filter_queryset(self.get_queryset())
news = get_object_or_404(news_qs, pk=self.kwargs['pk'])
gallery = get_object_or_404(news.news_gallery, image_id=self.kwargs['image_id'])
news = get_object_or_404(news_qs, pk=self.kwargs.get('pk'))
gallery = get_object_or_404(news.news_gallery, image_id=self.kwargs.get('image_id'))
# May raise a permission denied
self.check_object_permissions(self.request, gallery)
@ -111,14 +117,15 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
return gallery
class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIView):
class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView,
generics.ListAPIView):
"""Resource for returning gallery for news for back-office users."""
serializer_class = serializers.NewsImageSerializer
serializer_class = ImageBaseSerializer
def get_object(self):
"""Override get_object method."""
qs = super(NewsBackOfficeGalleryListView, self).get_queryset()
news = get_object_or_404(qs, pk=self.kwargs['pk'])
news = get_object_or_404(qs, pk=self.kwargs.get('pk'))
# May raise a permission denied
self.check_object_permissions(self.request, news)
@ -127,7 +134,7 @@ class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIVie
def get_queryset(self):
"""Override get_queryset method."""
return self.get_object().gallery.all()
return self.get_object().crop_gallery
class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
@ -153,7 +160,7 @@ class NewsFavoritesCreateDestroyView(generics.CreateAPIView, generics.DestroyAPI
"""
Returns the object the view is displaying.
"""
news = get_object_or_404(models.News, slug=self.kwargs['slug'])
news = get_object_or_404(models.News, slug=self.kwargs.get('slug'))
favorites = get_object_or_404(news.favorites.filter(user=self.request.user))
# May raise a permission denied
self.check_object_permissions(self.request, favorites)

View File

@ -0,0 +1,20 @@
# Generated by Django 2.2.7 on 2019-11-16 12:48
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('notification', '0002_subscriber_old_id'),
]
operations = [
migrations.AlterField(
model_name='subscriber',
name='user',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subscriber', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
]

View File

@ -74,7 +74,7 @@ class Subscriber(ProjectBaseMixin):
(USABLE, _('Usable')),
)
user = models.OneToOneField(
user = models.ForeignKey(
User,
blank=True,
null=True,

View File

@ -1,7 +1,5 @@
from pprint import pprint
from django.db.models import Count
from transfer.models import EmailAddresses, NewsletterSubscriber
from transfer.serializers.notification import SubscriberSerializer, NewsletterSubscriberSerializer
@ -25,14 +23,14 @@ def transfer_newsletter_subscriber():
'email_address__ip',
'email_address__country_code',
'email_address__locale',
'created_at',
'updated_at',
)
# serialized_data = NewsletterSubscriberSerializer(data=list(queryset.values()), many=True)
# if serialized_data.is_valid():
# serialized_data.save()
# else:
# pprint(f'NewsletterSubscriber serializer errors: {serialized_data.errors}')
serialized_data = NewsletterSubscriberSerializer(data=list(queryset), many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
pprint(f'NewsletterSubscriber serializer errors: {serialized_data.errors}')
data_types = {

View File

@ -6,3 +6,4 @@ from partner import models
@admin.register(models.Partner)
class PartnerModelAdmin(admin.ModelAdmin):
"""Model admin for Partner model."""
raw_id_fields = ('establishment',)

View File

@ -0,0 +1,50 @@
# Generated by Django 2.2.7 on 2019-11-21 10:59
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('establishment', '0065_establishment_purchased_products'),
('partner', '0002_auto_20191101_0939'),
]
operations = [
migrations.AddField(
model_name='partner',
name='establishment',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='partners', to='establishment.Establishment', verbose_name='Establishment'),
),
migrations.AddField(
model_name='partner',
name='expiry_date',
field=models.DateField(blank=True, null=True, verbose_name='expiry date'),
),
migrations.AddField(
model_name='partner',
name='name',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='name'),
),
migrations.AddField(
model_name='partner',
name='old_id',
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id'),
),
migrations.AddField(
model_name='partner',
name='price_per_month',
field=models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='price per month'),
),
migrations.AddField(
model_name='partner',
name='starting_date',
field=models.DateField(blank=True, null=True, verbose_name='starting date'),
),
migrations.AddField(
model_name='partner',
name='type',
field=models.PositiveSmallIntegerField(choices=[(0, 'Partner'), (1, 'Sponsor')], default=0),
),
]

View File

@ -1,13 +1,36 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from establishment.models import Establishment
from utils.models import ImageMixin, ProjectBaseMixin
class Partner(ProjectBaseMixin):
"""Partner model."""
PARTNER = 0
SPONSOR = 1
MODEL_TYPES = (
(PARTNER, _('Partner')),
(SPONSOR, _('Sponsor')),
)
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
name = models.CharField(_('name'), max_length=255, blank=True, null=True)
url = models.URLField(verbose_name=_('Partner URL'))
image = models.URLField(verbose_name=_('Partner image URL'), null=True)
establishment = models.ForeignKey(
Establishment,
verbose_name=_('Establishment'),
related_name='partners',
on_delete=models.CASCADE,
blank=True,
null=True,
)
type = models.PositiveSmallIntegerField(choices=MODEL_TYPES, default=PARTNER)
starting_date = models.DateField(_('starting date'), blank=True, null=True)
expiry_date = models.DateField(_('expiry date'), blank=True, null=True)
price_per_month = models.DecimalField(_('price per month'), max_digits=10, decimal_places=2, blank=True, null=True)
class Meta:
verbose_name = _('partner')

View File

@ -1,19 +1,32 @@
from django.db.models import Value, IntegerField, F
from pprint import pprint
from establishment.models import Establishment
from transfer.models import EstablishmentBacklinks
from transfer.serializers.partner import PartnerSerializer
def transfer_partner():
queryset = EstablishmentBacklinks.objects.filter(type="Partner")
establishments = Establishment.objects.filter(old_id__isnull=False).values_list('old_id', flat=True)
queryset = EstablishmentBacklinks.objects.filter(
establishment_id__in=list(establishments),
).values(
'id',
'establishment_id',
'partnership_name',
'partnership_icon',
'backlink_url',
'created_at',
'type',
'starting_date',
'expiry_date',
'price_per_month',
)
# queryset = EstablishmentBacklinks.objects.all() # Partner and Sponsor
serialized_data = PartnerSerializer(data=list(queryset.values()), many=True)
serialized_data = PartnerSerializer(data=list(queryset), many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
pprint(f"News serializer errors: {serialized_data.errors}")
pprint(f"Partner serializer errors: {serialized_data.errors}")
data_types = {

View File

@ -4,22 +4,23 @@ from utils.admin import BaseModelAdminMixin
from .models import Product, ProductType, ProductSubType, ProductGallery, Unit
class ProductGalleryInline(admin.TabularInline):
"""Product gallery inline."""
model = ProductGallery
extra = 0
@admin.register(Product)
class ProductAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""Admin page for model Product."""
search_fields = ('name', )
list_filter = ('available', 'product_type')
list_display = ('id', '__str__', 'get_category_display', 'product_type')
inlines = [ProductGalleryInline, ]
raw_id_fields = ('subtypes', 'classifications', 'standards',
'tags', 'gallery', 'establishment',)
@admin.register(ProductGallery)
class ProductGalleryAdmin(admin.ModelAdmin):
"""Admin page for model ProductGallery."""
raw_id_fields = ('product', 'image', )
@admin.register(ProductType)
class ProductTypeAdmin(admin.ModelAdmin):
"""Admin page for model ProductType."""

View File

@ -0,0 +1,19 @@
from django.core.management.base import BaseCommand
from product.models import Product
from transfer.models import Products
class Command(BaseCommand):
help = """Add average price to product from legacy table products."""
def handle(self, *args, **kwarg):
update_products = []
old_products = Products.objects.values_list('id', 'price')
for old_id, price in old_products:
product = Product.objects.get(old_id=old_id)
product.average_price = price
update_products.append(product)
Product.objects.bulk_update(update_products, ['average_price', ])
self.stdout.write(self.style.WARNING(f'Updated products: {len(update_products)}'))

View File

@ -2,7 +2,7 @@ from django.core.management.base import BaseCommand
from django.db import connections
from establishment.management.commands.add_position import namedtuplefetchall
from tag.models import Tag, TagCategory
from product.models import Product
from product.models import Product, ProductType
from tqdm import tqdm
@ -26,24 +26,47 @@ class Command(BaseCommand):
def add_category_tag(self):
objects = []
for c in tqdm(self.category_sql(), desc='Add category tags'):
categories = TagCategory.objects.filter(index_name=c.category
)
categories = TagCategory.objects.filter(index_name=c.category)
if not categories.exists():
objects.append(
TagCategory(label={"en-GB": c.category},
value_type=c.value_type,
index_name=c.category
index_name=c.category,
public=True
)
)
else:
categories.update(public=True)
TagCategory.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Add or get tag category objects.'))
def product_type_category_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select
DISTINCT
trim(CONVERT(v.key_name USING utf8)) as tag_category
FROM product_metadata m
join product_key_value_metadata v on v.id = m.product_key_value_metadatum_id
join products p on p.id = m.product_id
where UPPER(trim(p.type)) = 'WINE'
''')
return namedtuplefetchall(cursor)
def add_type_product_category(self):
for c in tqdm(self.product_type_category_sql(), desc='Add type product category'):
type = ProductType.objects.get(index_name=ProductType.WINE)
category = TagCategory.objects.get(index_name=c.tag_category)
if category not in type.tag_categories.all():
type.tag_categories.add(category)
self.stdout.write(self.style.WARNING(f'Add type product category objects.'))
def tag_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select
DISTINCT
m.id as old_id,
DISTINCT
trim(CONVERT(m.value USING utf8)) as tag_value,
trim(CONVERT(v.key_name USING utf8)) as tag_category
FROM product_metadata m
@ -64,24 +87,35 @@ class Command(BaseCommand):
if not tags.exists():
objects.append(Tag(label={"en-GB": t.tag_value},
category=category,
value=t.tag_value,
old_id_meta_product=t.old_id
))
else:
qs = tags.filter(old_id_meta_product__isnull=True)\
.update(old_id_meta_product=t.old_id)
value=t.tag_value)
)
Tag.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Add or get tag objects.'))
def remove_tags_product(self):
print('Begin clear tags product')
products = Product.objects.all()
for p in tqdm(products, desc='Clear tags product'):
p.tags.clear()
print('End clear tags product')
def remove_tags(self):
print('Begin delete many tags')
Tag.objects.\
filter(news__isnull=True, establishments__isnull=True).delete()
print('End delete many tags')
def product_sql(self):
with connections['legacy'].cursor() as cursor:
cursor.execute('''
select
DISTINCT
m.id as old_id_tag,
DISTINCT
m.product_id,
lower(trim(CONVERT(m.value USING utf8))) as tag_value,
trim(CONVERT(m.value USING utf8)) as tag_value,
trim(CONVERT(v.key_name USING utf8)) as tag_category
FROM product_metadata m
JOIN product_key_value_metadata v on v.id = m.product_key_value_metadatum_id
@ -90,10 +124,15 @@ class Command(BaseCommand):
def add_product_tag(self):
for t in tqdm(self.product_sql(), desc='Add product tag'):
tags = Tag.objects.filter(old_id_meta_product=t.old_id_tag)
category = TagCategory.objects.get(index_name=t.tag_category)
tags = Tag.objects.filter(
category=category,
value=t.tag_value
)
product = Product.objects.get(old_id=t.product_id)
for tag in tags:
if product not in tag.products.all():
if tag not in product.tags.all():
product.tags.add(tag)
self.stdout.write(self.style.WARNING(f'Add or get tag objects.'))
@ -111,7 +150,10 @@ class Command(BaseCommand):
tag.save()
def handle(self, *args, **kwargs):
self.remove_tags_product()
self.remove_tags()
self.add_category_tag()
self.add_type_product_category()
self.add_tag()
self.check_tag()
self.add_product_tag()

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.7 on 2019-11-17 11:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('product', '0013_auto_20191113_1512'),
]
operations = [
migrations.AlterField(
model_name='productgallery',
name='image',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='product_gallery', to='gallery.Image', verbose_name='image'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-11-17 19:54
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0014_auto_20191117_1117'),
]
operations = [
migrations.AlterField(
model_name='producttype',
name='tag_categories',
field=models.ManyToManyField(related_name='product_types', to='tag.TagCategory', verbose_name='Tag categories'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-11-19 13:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0015_auto_20191117_1954'),
]
operations = [
migrations.AddField(
model_name='product',
name='average_price',
field=models.DecimalField(blank=True, decimal_places=2, default=None, max_digits=14, null=True, verbose_name='average price'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.7 on 2019-11-19 15:46
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('product', '0016_product_average_price'),
]
operations = [
migrations.AlterField(
model_name='productnote',
name='product',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='notes', to='product.Product', verbose_name='product'),
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 2.2.7 on 2019-11-20 12:49
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('establishment', '0064_auto_20191119_1546'),
('product', '0017_auto_20191119_1546'),
]
operations = [
migrations.CreateModel(
name='PurchasedProduct',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_gifted', models.NullBooleanField(default=None, verbose_name='is gifted')),
('quantity', models.PositiveSmallIntegerField(verbose_name='quantity')),
('establishment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='purchased_plaques', to='establishment.Establishment', verbose_name='establishment')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='purchased_by_establishments', to='product.Product', verbose_name='plaque')),
],
options={
'verbose_name': 'purchased plaque',
'verbose_name_plural': 'purchased plaques',
'unique_together': {('establishment', 'product')},
},
),
]

View File

@ -7,8 +7,9 @@ from django.db.models import Case, When
from django.utils.translation import gettext_lazy as _
from django.core.validators import MaxValueValidator, MinValueValidator
from utils.models import (BaseAttributes, ProjectBaseMixin,
TranslatedFieldsMixin, TJSONField)
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
TranslatedFieldsMixin, TJSONField,
GalleryModelMixin, IntermediateGalleryModelMixin)
class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
@ -30,7 +31,7 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='product_types',
verbose_name=_('Tag'))
verbose_name=_('Tag categories'))
class Meta:
"""Meta class."""
@ -82,7 +83,12 @@ class ProductQuerySet(models.QuerySet):
def with_extended_related(self):
"""Returns qs with almost all related objects."""
return self.with_base_related() \
.prefetch_related('tags', 'standards', 'classifications', 'classifications__standard',
.prefetch_related('tags', 'tags__category', 'tags__category__country',
'standards', 'classifications', 'classifications__standard',
'establishment__address', 'establishment__establishment_type',
'establishment__address__city', 'establishment__address__city__country',
'establishment__establishment_subtypes', 'product_gallery',
'gallery', 'product_type', 'subtypes',
'classifications__classification_type', 'classifications__tags') \
.select_related('wine_region', 'wine_sub_region')
@ -125,7 +131,7 @@ class ProductQuerySet(models.QuerySet):
)
class Product(TranslatedFieldsMixin, BaseAttributes):
class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, HasTagsMixin):
"""Product models."""
EARLIEST_VINTAGE_YEAR = 1700
@ -205,6 +211,9 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
null=True, blank=True, default=None,
validators=[MinValueValidator(EARLIEST_VINTAGE_YEAR),
MaxValueValidator(LATEST_VINTAGE_YEAR)])
average_price = models.DecimalField(max_digits=14, decimal_places=2,
blank=True, null=True, default=None,
verbose_name=_('average price'))
gallery = models.ManyToManyField('gallery.Image', through='ProductGallery')
reviews = generic.GenericRelation(to='review.Review')
comments = generic.GenericRelation(to='comment.Comment')
@ -222,15 +231,11 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
"""Override str dunder method."""
return f'{self.name}'
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.'))
def delete(self, using=None, keep_parents=False):
"""Overridden delete method"""
# Delete all related notes
self.notes.all().delete()
return super().delete(using, keep_parents)
@property
def product_type_translated_name(self):
@ -255,33 +260,50 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
return self.tags.filter(category__index_name='bottles-produced')
@property
def main_image(self):
qs = ProductGallery.objects.filter(product=self, is_main=True)
def grape_variety(self):
return self.tags.filter(category__index_name='grape-variety')
@property
def bottle_sizes(self):
return self.tags.filter(category__index_name='bottle_size')
@property
def alcohol_percentage(self):
qs = self.tags.filter(category__index_name='alcohol_percentage')
if qs.exists():
return qs.first().image
@property
def main_image_url(self):
return self.main_image.image if self.main_image else None
@property
def preview_main_image_url(self):
return self.main_image.get_image_url('product_preview') if self.main_image else None
return qs.first()
@property
def related_tags(self):
return self.tags.exclude(
category__index_name__in=['sugar-content', 'wine-color', 'bottles-produced',
'serial-number', 'grape-variety'])
return super().visible_tags.exclude(category__index_name__in=[
'sugar-content', 'wine-color', 'bottles-produced',
'serial-number', 'grape-variety', 'serial_number',
'alcohol_percentage', 'bottle_size',
])
@property
def display_name(self):
name = f'{self.name} ' \
f'({self.vintage if self.vintage else "BSA"})'
if self.establishment.name:
if self.establishment and self.establishment.name:
name = f'{self.establishment.name} - ' + name
return name
@property
def main_image(self):
qs = self.product_gallery.main_image()
if qs.exists():
return qs.first().image
@property
def image_url(self):
return self.main_image.image.url if self.main_image else None
@property
def preview_image_url(self):
if self.main_image:
return self.main_image.get_image_url(thumbnail_key='product_preview')
class OnlineProductManager(ProductManager):
"""Extended manger for OnlineProduct model."""
@ -304,6 +326,26 @@ class OnlineProduct(Product):
verbose_name_plural = _('Online products')
class PurchasedProduct(models.Model):
"""Model for storing establishment purchased plaques."""
establishment = models.ForeignKey('establishment.Establishment', on_delete=models.CASCADE,
related_name='purchased_plaques',
verbose_name=_('establishment'))
product = models.ForeignKey('product.Product', on_delete=models.CASCADE,
related_name='purchased_by_establishments',
verbose_name=_('plaque'))
is_gifted = models.NullBooleanField(default=None,
verbose_name=_('is gifted'))
quantity = models.PositiveSmallIntegerField(verbose_name=_('quantity'))
class Meta:
"""Meta class."""
verbose_name = _('purchased plaque')
verbose_name_plural = _('purchased plaques')
unique_together = ('establishment', 'product')
class Unit(models.Model):
"""Product unit model."""
name = models.CharField(max_length=255,
@ -353,15 +395,8 @@ class ProductStandard(models.Model):
verbose_name = _('wine standard')
class ProductGalleryQuerySet(models.QuerySet):
"""QuerySet for model Product"""
def main_image(self):
"""Return objects with flag is_main is True"""
return self.filter(is_main=True)
class ProductGallery(models.Model):
class ProductGallery(IntermediateGalleryModelMixin):
"""Gallery for model Product."""
product = models.ForeignKey(Product, null=True,
related_name='product_gallery',
on_delete=models.CASCADE,
@ -369,11 +404,7 @@ class ProductGallery(models.Model):
image = models.ForeignKey('gallery.Image', null=True,
related_name='product_gallery',
on_delete=models.CASCADE,
verbose_name=_('gallery'))
is_main = models.BooleanField(default=False,
verbose_name=_('Is the main image'))
objects = ProductGalleryQuerySet.as_manager()
verbose_name=_('image'))
class Meta:
"""ProductGallery meta class."""
@ -439,7 +470,7 @@ class ProductNote(ProjectBaseMixin):
old_id = models.PositiveIntegerField(null=True, blank=True)
text = models.TextField(verbose_name=_('text'))
product = models.ForeignKey(Product, on_delete=models.PROTECT,
related_name='product_notes',
related_name='notes',
verbose_name=_('product'))
user = models.ForeignKey('account.User', on_delete=models.PROTECT,
null=True,

View File

@ -7,6 +7,7 @@ from product import models
from product.serializers import ProductDetailSerializer, ProductTypeBaseSerializer, \
ProductSubTypeBaseSerializer
from tag.models import TagCategory
from account.serializers.common import UserShortSerializer
class ProductBackOfficeGallerySerializer(serializers.ModelSerializer):
@ -127,3 +128,55 @@ class ProductSubTypeBackOfficeDetailSerializer(ProductSubTypeBaseSerializer):
'name',
'index_name',
]
class ProductNoteBaseSerializer(serializers.ModelSerializer):
"""Serializer for model ProductNote."""
user_detail = UserShortSerializer(read_only=True, source='user')
class Meta:
"""Meta class."""
model = models.ProductNote
fields = [
'id',
'created',
'modified',
'text',
'user',
'user_detail',
'product',
]
extra_kwargs = {
'created': {'read_only': True},
'modified': {'read_only': True},
'product': {'required': False, 'write_only': True},
'user': {'required': False, 'write_only': True},
}
@property
def serializer_view(self):
"""Return view instance."""
return self.context.get('view')
class ProductNoteListCreateSerializer(ProductNoteBaseSerializer):
"""Serializer for List|Create action for model ProductNote."""
def create(self, validated_data):
"""Overridden create method."""
validated_data['user'] = self.user
validated_data['product'] = self.product
return super().create(validated_data)
@property
def user(self):
"""Return user instance from view."""
if self.serializer_view:
return self.serializer_view.request.user
@property
def product(self):
"""Return product instance from view."""
if self.serializer_view:
return self.serializer_view.get_object()

View File

@ -4,21 +4,21 @@ from rest_framework import serializers
from comment.models import Comment
from comment.serializers import CommentSerializer
from establishment.serializers import EstablishmentShortSerializer, EstablishmentProductSerializer
from establishment.serializers import EstablishmentShortSerializer, EstablishmentProductSerializer, EstablishmentProductShortSerializer
from gallery.models import Image
from product import models
from review.serializers import ReviewShortSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import TranslatedField, FavoritesCreateSerializer
from utils.serializers import TranslatedField, FavoritesCreateSerializer, ImageBaseSerializer
from main.serializers import AwardSerializer
from location.serializers import WineRegionBaseSerializer, WineSubRegionBaseSerializer
from tag.serializers import TagBaseSerializer, TagCategoryShortSerializer
from tag.serializers import TagBaseSerializer, TagCategoryProductSerializer
class ProductTagSerializer(TagBaseSerializer):
"""Serializer for model Tag."""
category = TagCategoryShortSerializer(read_only=True)
category = TagCategoryProductSerializer(read_only=True)
class Meta(TagBaseSerializer.Meta):
"""Meta class."""
@ -88,12 +88,11 @@ class ProductBaseSerializer(serializers.ModelSerializer):
name = serializers.CharField(source='display_name', read_only=True)
product_type = ProductTypeBaseSerializer(read_only=True)
subtypes = ProductSubTypeBaseSerializer(many=True, read_only=True)
establishment_detail = EstablishmentShortSerializer(source='establishment', read_only=True)
establishment_detail = EstablishmentProductShortSerializer(source='establishment', read_only=True)
tags = ProductTagSerializer(source='related_tags', many=True, read_only=True)
wine_region = WineRegionBaseSerializer(read_only=True)
wine_colors = TagBaseSerializer(many=True, read_only=True)
preview_image_url = serializers.URLField(source='preview_main_image_url',
allow_null=True,
preview_image_url = serializers.URLField(allow_null=True,
read_only=True)
in_favorites = serializers.BooleanField(allow_null=True)
@ -120,6 +119,7 @@ class ProductBaseSerializer(serializers.ModelSerializer):
class ProductDetailSerializer(ProductBaseSerializer):
"""Product detail serializer."""
description_translated = TranslatedField()
establishment_detail = EstablishmentShortSerializer(source='establishment', read_only=True)
review = ReviewShortSerializer(source='last_published_review', read_only=True)
awards = AwardSerializer(many=True, read_only=True)
classifications = ProductClassificationBaseSerializer(many=True, read_only=True)
@ -127,9 +127,12 @@ class ProductDetailSerializer(ProductBaseSerializer):
wine_sub_region = WineSubRegionBaseSerializer(read_only=True)
bottles_produced = TagBaseSerializer(many=True, read_only=True)
sugar_contents = TagBaseSerializer(many=True, read_only=True)
image_url = serializers.ImageField(source='main_image_url',
allow_null=True,
read_only=True)
grape_variety = TagBaseSerializer(many=True, read_only=True)
bottle_sizes = TagBaseSerializer(many=True, read_only=True)
alcohol_percentage = TagBaseSerializer(read_only=True)
image_url = serializers.URLField(allow_null=True,
read_only=True)
new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True)
class Meta(ProductBaseSerializer.Meta):
fields = ProductBaseSerializer.Meta.fields + [
@ -142,6 +145,11 @@ class ProductDetailSerializer(ProductBaseSerializer):
'bottles_produced',
'sugar_contents',
'image_url',
'new_image',
'grape_variety',
'average_price',
'bottle_sizes',
'alcohol_percentage',
]
@ -175,78 +183,6 @@ class ProductFavoritesCreateSerializer(FavoritesCreateSerializer):
return super().create(validated_data)
# class CropImageSerializer(serializers.Serializer):
# """Serializer for crop images for News object."""
#
# preview_url = serializers.SerializerMethodField()
# promo_horizontal_web_url = serializers.SerializerMethodField()
# promo_horizontal_mobile_url = serializers.SerializerMethodField()
# tile_horizontal_web_url = serializers.SerializerMethodField()
# tile_horizontal_mobile_url = serializers.SerializerMethodField()
# tile_vertical_web_url = serializers.SerializerMethodField()
# highlight_vertical_web_url = serializers.SerializerMethodField()
# editor_web_url = serializers.SerializerMethodField()
# editor_mobile_url = serializers.SerializerMethodField()
#
# def get_preview_url(self, obj):
# """Get crop preview."""
# return obj.instance.get_image_url('news_preview')
#
# def get_promo_horizontal_web_url(self, obj):
# """Get crop promo_horizontal_web."""
# return obj.instance.get_image_url('news_promo_horizontal_web')
#
# def get_promo_horizontal_mobile_url(self, obj):
# """Get crop promo_horizontal_mobile."""
# return obj.instance.get_image_url('news_promo_horizontal_mobile')
#
# def get_tile_horizontal_web_url(self, obj):
# """Get crop tile_horizontal_web."""
# return obj.instance.get_image_url('news_tile_horizontal_web')
#
# def get_tile_horizontal_mobile_url(self, obj):
# """Get crop tile_horizontal_mobile."""
# return obj.instance.get_image_url('news_tile_horizontal_mobile')
#
# def get_tile_vertical_web_url(self, obj):
# """Get crop tile_vertical_web."""
# return obj.instance.get_image_url('news_tile_vertical_web')
#
# def get_highlight_vertical_web_url(self, obj):
# """Get crop highlight_vertical_web."""
# return obj.instance.get_image_url('news_highlight_vertical_web')
#
# def get_editor_web_url(self, obj):
# """Get crop editor_web."""
# return obj.instance.get_image_url('news_editor_web')
#
# def get_editor_mobile_url(self, obj):
# """Get crop editor_mobile."""
# return obj.instance.get_image_url('news_editor_mobile')
class ProductImageSerializer(serializers.ModelSerializer):
"""Serializer for returning crop images of product image."""
orientation_display = serializers.CharField(source='get_orientation_display',
read_only=True)
original_url = serializers.URLField(source='image.url')
# auto_crop_images = CropImageSerializer(source='image', allow_null=True)
class Meta:
model = Image
fields = [
'id',
'title',
'orientation_display',
'original_url',
# 'auto_crop_images',
]
extra_kwargs = {
'orientation': {'write_only': True}
}
class ProductCommentCreateSerializer(CommentSerializer):
"""Create comment serializer"""
mark = serializers.IntegerField()

View File

@ -6,6 +6,8 @@ from product import views
urlpatterns = [
path('', views.ProductListCreateBackOfficeView.as_view(), name='list-create'),
path('<int:pk>/', views.ProductDetailBackOfficeView.as_view(), name='rud'),
path('<int:pk>/notes/', views.ProductNoteListCreateView.as_view(), name='note-list-create'),
path('<int:pk>/notes/<int:note_pk>/', views.ProductNoteRUDView.as_view(), name='note-rud'),
path('<int:pk>/gallery/', views.ProductBackOfficeGalleryListView.as_view(),
name='gallery-list'),
path('<int:pk>/gallery/<int:image_id>/', views.ProductBackOfficeGalleryCreateDestroyView.as_view(),

View File

@ -5,13 +5,14 @@ from rest_framework.response import Response
from product import serializers, models
from product.views import ProductBaseView
from utils.serializers import ImageBaseSerializer
from utils.views import CreateDestroyGalleryViewMixin
class ProductBackOfficeMixinView(ProductBaseView):
"""Product back-office mixin view."""
permission_classes = (permissions.IsAuthenticated,)
permission_classes = (permissions.IsAuthenticated, )
def get_queryset(self):
"""Override get_queryset method."""
@ -56,8 +57,8 @@ class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView,
"""
product_qs = self.filter_queryset(self.get_queryset())
product = get_object_or_404(product_qs, pk=self.kwargs['pk'])
gallery = get_object_or_404(product.product_gallery, image_id=self.kwargs['image_id'])
product = get_object_or_404(product_qs, pk=self.kwargs.get('pk'))
gallery = get_object_or_404(product.product_gallery, image_id=self.kwargs.get('image_id'))
# May raise a permission denied
self.check_object_permissions(self.request, gallery)
@ -65,14 +66,16 @@ class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView,
return gallery
class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.ListAPIView):
class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView,
generics.ListAPIView):
"""Resource for returning gallery for product for back-office users."""
serializer_class = serializers.ProductImageSerializer
serializer_class = ImageBaseSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_object(self):
"""Override get_object method."""
qs = super(ProductBackOfficeGalleryListView, self).get_queryset()
product = get_object_or_404(qs, pk=self.kwargs['pk'])
product = get_object_or_404(qs, pk=self.kwargs.get('pk'))
# May raise a permission denied
self.check_object_permissions(self.request, product)
@ -81,10 +84,11 @@ class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.List
def get_queryset(self):
"""Override get_queryset method."""
return self.get_object().gallery.all()
return self.get_object().crop_gallery
class ProductDetailBackOfficeView(ProductBackOfficeMixinView, generics.RetrieveUpdateDestroyAPIView):
class ProductDetailBackOfficeView(ProductBackOfficeMixinView,
generics.RetrieveUpdateDestroyAPIView):
"""Product back-office R/U/D view."""
serializer_class = serializers.ProductBackOfficeDetailSerializer
@ -131,3 +135,48 @@ class ProductSubTypeRUDBackOfficeView(BackOfficeListCreateMixin,
generics.RetrieveUpdateDestroyAPIView):
"""Product sub type back-office retrieve-update-destroy view."""
serializer_class = serializers.ProductSubTypeBackOfficeDetailSerializer
class ProductNoteListCreateView(ProductBackOfficeMixinView,
BackOfficeListCreateMixin,
generics.ListCreateAPIView):
"""Retrieve|Update|Destroy product note view."""
serializer_class = serializers.ProductNoteListCreateSerializer
def get_object(self):
"""Returns the object the view is displaying."""
product_qs = models.Product.objects.all()
filtered_product_qs = self.filter_queryset(product_qs)
product = get_object_or_404(filtered_product_qs, pk=self.kwargs.get('pk'))
# May raise a permission denied
self.check_object_permissions(self.request, product)
return product
def get_queryset(self):
"""Overridden get_queryset method."""
return self.get_object().notes.all()
class ProductNoteRUDView(ProductBackOfficeMixinView,
BackOfficeListCreateMixin,
generics.RetrieveUpdateDestroyAPIView):
"""Create|Retrieve|Update|Destroy product note view."""
serializer_class = serializers.ProductNoteBaseSerializer
def get_object(self):
"""Returns the object the view is displaying."""
product_qs = models.Product.objects.all()
filtered_product_qs = self.filter_queryset(product_qs)
product = get_object_or_404(filtered_product_qs, pk=self.kwargs.get('pk'))
note = get_object_or_404(product.notes.all(), pk=self.kwargs.get('note_pk'))
# May raise a permission denied
self.check_object_permissions(self.request, note)
return note

View File

@ -17,7 +17,6 @@ class ProductBaseView(generics.GenericAPIView):
return Product.objects.published() \
.with_base_related() \
.annotate_in_favorites(self.request.user) \
.by_country_code(self.request.country_code) \
.order_by('-created')
@ -26,6 +25,11 @@ class ProductListView(ProductBaseView, generics.ListAPIView):
serializer_class = serializers.ProductBaseSerializer
filter_class = filters.ProductFilterSet
def get_queryset(self):
qs = super().get_queryset().with_extended_related() \
.by_country_code(self.request.country_code)
return qs
class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
"""Detail view fro model Product."""

30
apps/review/filters.py Normal file
View File

@ -0,0 +1,30 @@
from django.core.validators import EMPTY_VALUES
from django_filters import rest_framework as filters
from review import models
class ReviewFilter(filters.FilterSet):
"""Review filter set."""
establishment_id = filters.NumberFilter(field_name='object_id', )
product_id = filters.NumberFilter(field_name='object_id', )
class Meta:
"""Meta class."""
model = models.Review
fields = (
'establishment_id',
'product_id',
)
def by_establishment_id(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_establishment_id(value, content_type='establishment')
return queryset
def by_product_id(self, queryset, name, value):
if value not in EMPTY_VALUES:
return queryset.by_product_id(value, content_type='product')
return queryset

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.7 on 2019-11-17 11:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('review', '0017_auto_20191115_0737'),
]
operations = [
migrations.AlterField(
model_name='inquiriesgallery',
name='image',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inquiries_gallery', to='gallery.Image', verbose_name='image'),
),
]

View File

@ -4,8 +4,9 @@ from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from utils.models import BaseAttributes, TranslatedFieldsMixin, ProjectBaseMixin
from utils.models import TJSONField
from utils.models import (BaseAttributes, TranslatedFieldsMixin,
ProjectBaseMixin, GalleryModelMixin,
TJSONField, IntermediateGalleryModelMixin)
class ReviewQuerySet(models.QuerySet):
@ -92,7 +93,7 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
verbose_name_plural = _('Reviews')
class Inquiries(ProjectBaseMixin):
class Inquiries(GalleryModelMixin, ProjectBaseMixin):
NONE = 0
DINER = 1
LUNCH = 2
@ -145,15 +146,7 @@ class GridItems(ProjectBaseMixin):
return f'inquiry: {self.inquiry.id}, grid id: {self.id}'
class InquiriesGalleryQuerySet(models.QuerySet):
"""QuerySet for model Inquiries"""
def main_image(self):
"""Return objects with flag is_main is True"""
return self.filter(is_main=True)
class InquiriesGallery(models.Model):
class InquiriesGallery(IntermediateGalleryModelMixin):
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
inquiry = models.ForeignKey(
Inquiries,
@ -167,11 +160,8 @@ class InquiriesGallery(models.Model):
null=True,
related_name='inquiries_gallery',
on_delete=models.CASCADE,
verbose_name=_('gallery'),
verbose_name=_('image'),
)
is_main = models.BooleanField(default=False, verbose_name=_('Is the main image'))
objects = InquiriesGalleryQuerySet.as_manager()
class Meta:
verbose_name = _('inquiry gallery')

View File

@ -14,7 +14,9 @@ class ReviewBaseSerializer(serializers.ModelSerializer):
'child',
'published_at',
'vintage',
'country'
'country',
'content_type',
'object_id',
)

View File

@ -19,6 +19,7 @@ class BaseTestCase(APITestCase):
username=self.username,
email=self.email,
password=self.password,
is_staff=True,
)
tokens = User.create_jwt_tokens(self.user)
@ -61,6 +62,49 @@ class BaseTestCase(APITestCase):
)
class ReviewTestCase(BaseTestCase):
def setUp(self):
super().setUp()
def test_review_list(self):
response = self.client.get('/api/back/review/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_review_post(self):
test_review = {
'reviewer': self.user.id,
'status': Review.READY,
'vintage': 2019,
'country': self.country_ru.id,
'object_id': 1,
'content_type': 1,
}
response = self.client.post('/api/back/review/', data=test_review)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_review_detail(self):
response = self.client.get(f'/api/back/review/{self.test_review.id}/', format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_review_detail_put(self):
data = {
'id': self.test_review.id,
'vintage': 2018,
'reviewer': self.user.id,
'status': Review.READY,
'country': self.country_ru.id,
'object_id': 1,
'content_type': 1,
}
response = self.client.put(f'/api/back/review/{self.test_review.id}/', data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_review_delete(self):
response = self.client.delete(f'/api/back/review/{self.test_review.id}/')
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
class InquiriesTestCase(BaseTestCase):
def setUp(self):
super().setUp()

View File

@ -2,10 +2,10 @@ from pprint import pprint
from django.db.models import Q
from product.models import Product
from account.models import User
from account.transfer_data import STOP_LIST
from establishment.models import Establishment
from product.models import Product
from review.models import Inquiries as NewInquiries, Review
from transfer.models import Reviews, ReviewTexts, Inquiries, GridItems, InquiryPhotos
from transfer.serializers.grid import GridItemsSerializer
@ -34,11 +34,11 @@ def transfer_languages():
def transfer_reviews():
establishments = Establishment.objects.filter(old_id__isnull=False).values_list('old_id', flat=True)
queryset = Reviews.objects.filter(
queryset = Reviews.objects.exclude(product_id__isnull=False).filter(
establishment_id__in=list(establishments),
).values('id', 'reviewer_id', 'aasm_state', 'created_at', 'establishment_id', 'mark', 'vintage')
serialized_data = ReviewSerializer(data=list(queryset.values()), many=True)
serialized_data = ReviewSerializer(data=list(queryset), many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
@ -48,17 +48,23 @@ def transfer_reviews():
def transfer_text_review():
reviews = Review.objects.filter(old_id__isnull=False).values_list('old_id', flat=True)
queryset = ReviewTexts.objects.filter(
review_id__in=list(reviews),
review_id__in=list(reviews)
).exclude(
Q(text__isnull=True) | Q(text='')
id__in=(23183, 25348, 43930, 23199, 26226, 34006) # пробелы вместо текста
).exclude(
text__isnull=True
).exclude(
text__iexact=''
).values('review_id', 'locale', 'text')
serialized_data = ReviewTextSerializer(data=list(queryset.values()), many=True)
serialized_data = ReviewTextSerializer(data=list(queryset), many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
pprint(f"ReviewTextSerializer serializer errors: {serialized_data.errors}")
def make_en_text_review():
for review in Review.objects.filter(old_id__isnull=False):
text = review.text
if text and 'en-GB' not in text:
@ -106,7 +112,6 @@ def transfer_inquiry_photos():
def transfer_product_reviews():
products = Product.objects.filter(
old_id__isnull=False).values_list('old_id', flat=True)
@ -130,6 +135,7 @@ data_types = {
# transfer_languages,
transfer_reviews,
transfer_text_review,
make_en_text_review,
],
'inquiries': [
transfer_inquiries,

View File

@ -1,5 +1,6 @@
from rest_framework import generics, permissions
from review import filters
from review import models
from review import serializers
from utils.permissions import IsReviewerManager, IsRestaurantReviewer
@ -10,13 +11,14 @@ class ReviewLstView(generics.ListCreateAPIView):
serializer_class = serializers.ReviewBaseSerializer
queryset = models.Review.objects.all()
permission_classes = [permissions.IsAuthenticatedOrReadOnly, ]
filterset_class = filters.ReviewFilter
class ReviewRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Comment RUD view."""
serializer_class = serializers.ReviewBaseSerializer
queryset = models.Review.objects.all()
permission_classes = [IsReviewerManager | IsRestaurantReviewer]
permission_classes = [permissions.IsAdminUser | IsReviewerManager | IsRestaurantReviewer]
lookup_field = 'id'

View File

@ -48,6 +48,34 @@ class EstablishmentDocument(Document):
properties=OBJECT_FIELD_PROPERTIES),
},
multi=True)
visible_tags = fields.ObjectField(
properties={
'id': fields.IntegerField(attr='id'),
'label': fields.ObjectField(attr='label_indexing',
properties=OBJECT_FIELD_PROPERTIES),
},
multi=True)
products = fields.ObjectField(
properties={
'wine_region': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
'country': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'code': fields.KeywordField(),
}),
# 'coordinates': fields.GeoPointField(),
'description': fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES),
}),
'wine_sub_region': fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
}),
},
multi=True
)
schedule = fields.ListField(fields.ObjectField(
properties={
'id': fields.IntegerField(attr='id'),

View File

@ -34,6 +34,13 @@ class NewsDocument(Document):
'value': fields.KeywordField()
},
multi=True)
visible_tags = fields.ObjectField(
properties={
'id': fields.IntegerField(attr='id'),
'label': fields.ObjectField(attr='label_indexing',
properties=OBJECT_FIELD_PROPERTIES),
},
multi=True)
class Django:

View File

@ -28,12 +28,43 @@ class ProductDocument(Document):
},
multi=True
)
preview_image_url = fields.KeywordField(attr='preview_image_url')
establishment = fields.ObjectField(
properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
'index_name': fields.KeywordField(),
'slug': fields.KeywordField(),
# 'city' TODO: city indexing
'city': fields.ObjectField(
attr='address.city',
properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
'code': fields.KeywordField(),
'country': fields.ObjectField(
properties={
'id': fields.IntegerField(),
'name': fields.ObjectField(attr='name_indexing',
properties=OBJECT_FIELD_PROPERTIES),
'code': fields.KeywordField(),
'svg_image': fields.KeywordField(attr='svg_image_indexing')
}
),
}
),
'address': fields.ObjectField(
properties={
'city': fields.ObjectField(
properties={
'country': fields.ObjectField(
properties={
'code': fields.KeywordField()
}
)
}
)
}
)
}
)
wine_colors = fields.ObjectField(
@ -44,6 +75,14 @@ class ProductDocument(Document):
},
multi=True,
)
grape_variety = fields.ObjectField(
properties={
'id': fields.IntegerField(),
'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(),
},
multi=True,
)
wine_region = fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
@ -56,7 +95,10 @@ class ProductDocument(Document):
# 'coordinates': fields.GeoPointField(),
'description': fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES),
})
wine_sub_region = fields.ObjectField(properties={'name': fields.KeywordField()})
wine_sub_region = fields.ObjectField(properties={
'id': fields.IntegerField(),
'name': fields.KeywordField(),
})
classifications = fields.ObjectField( # TODO
properties={
'classification_type': fields.ObjectField(properties={}),
@ -95,13 +137,23 @@ class ProductDocument(Document):
},
multi=True
)
related_tags = fields.ObjectField(
properties={
'id': fields.IntegerField(),
'label': fields.ObjectField(attr='label_indexing', properties=OBJECT_FIELD_PROPERTIES),
'value': fields.KeywordField(),
},
multi=True
)
name = fields.TextField(attr='display_name', analyzer='english')
name_ru = fields.TextField(attr='display_name', analyzer='russian')
name_fr = fields.TextField(attr='display_name', analyzer='french')
class Django:
model = models.Product
fields = (
'id',
'category',
'name',
'available',
'public_mark',
'slug',
@ -109,6 +161,7 @@ class ProductDocument(Document):
'state',
'old_unique_key',
'vintage',
'average_price',
)
related_models = [models.ProductType]

View File

@ -1,19 +1,22 @@
"""Search indexes filters."""
from elasticsearch_dsl.query import Q
from django_elasticsearch_dsl_drf.filter_backends import SearchFilterBackend
from utils.models import get_current_locale
from search_indexes.utils import OBJECT_FIELD_PROPERTIES
class CustomSearchFilterBackend(SearchFilterBackend):
"""Custom SearchFilterBackend."""
@staticmethod
def get_field_name(view, field):
field_name = field
def search_among_all_locales(view, search_kwargs: dict):
if hasattr(view, 'search_fields') and hasattr(view, 'translated_search_fields'):
if field in view.translated_search_fields:
field_name = f'{field}.{get_current_locale()}'
return field_name
all_supported_locales = OBJECT_FIELD_PROPERTIES.keys()
fields = search_kwargs.copy().keys()
for field in fields:
if field in view.translated_search_fields:
value = search_kwargs.pop(field)
search_kwargs.update({f'{field}.{locale}': value for locale in all_supported_locales})
def construct_search(self, request, view):
"""Construct search.
@ -54,29 +57,69 @@ class CustomSearchFilterBackend(SearchFilterBackend):
field, value = __values
if field in view.search_fields:
# Initial kwargs for the match query
field_kwargs = {self.get_field_name(view, field): {'query': value}}
field_kwargs = {field: {'query': value}}
# In case if we deal with structure 2
if isinstance(view.search_fields, dict):
extra_field_kwargs = view.search_fields[field]
if extra_field_kwargs:
field_kwargs[self.get_field_name(view, field)].update(extra_field_kwargs)
field_kwargs[field].update(extra_field_kwargs)
# The match query
__queries.append(
Q("match", **field_kwargs)
)
self.search_among_all_locales(view, field_kwargs)
if len(field_kwargs.keys()) > 1:
for k, v in field_kwargs.items():
__queries.append(
Q("match", **{k: v})
)
__queries.append(
Q('wildcard',
**{k: {
'value': f'*{search_term.lower()}*',
'boost': v.get('boost', 1) + 30
}
}
)
)
else:
__queries.append(
Q("match", **field_kwargs)
)
__queries.append(
Q('wildcard', **{field: {'value': f'*{search_term.lower()}*',
'boost': field_kwargs[field].get('boost', 1) + 30}})
)
else:
for field in view.search_fields:
# Initial kwargs for the match query
field_kwargs = {self.get_field_name(view, field): {'query': search_term}}
field_kwargs = {field: {'query': search_term}}
# In case if we deal with structure 2
if isinstance(view.search_fields, dict):
extra_field_kwargs = view.search_fields[field]
if extra_field_kwargs:
field_kwargs[self.get_field_name(view, field)].update(extra_field_kwargs)
field_kwargs[field].update(extra_field_kwargs)
# The match query
__queries.append(
Q("match", **field_kwargs)
)
self.search_among_all_locales(view, field_kwargs)
if len(field_kwargs.keys()) > 1:
for k, v in field_kwargs.items():
__queries.append(
Q("match", **{k: v})
)
__queries.append(
Q('wildcard',
**{k: {
'value': f'*{search_term.lower()}*',
'boost': v.get('boost', 1) + 30
}
}
)
)
else:
__queries.append(
Q("match", **field_kwargs)
)
__queries.append(
Q('wildcard', **{field: {'value': f'*{search_term.lower()}*',
'boost': field_kwargs[field].get('boost', 1) + 30}})
)
return __queries

View File

@ -39,7 +39,8 @@ class ProductSubtypeDocumentSerializer(serializers.Serializer):
id = serializers.IntegerField()
name_translated = serializers.SerializerMethodField()
get_name_translated = lambda obj: get_translated_value(obj.name)
def get_name_translated(self, obj):
return get_translated_value(obj.name)
class WineRegionCountryDocumentSerialzer(serializers.Serializer):
@ -64,9 +65,12 @@ class WineRegionDocumentSerializer(serializers.Serializer):
name = serializers.CharField()
country = WineRegionCountryDocumentSerialzer(allow_null=True)
def get_attribute(self, instance):
return instance.wine_region if instance and instance.wine_region else None
class WineColorDocumentSerializer(serializers.Serializer):
"""Wine color ES document serializer,"""
class TagDocumentSerializer(serializers.Serializer):
"""Tag ES document serializer,"""
id = serializers.IntegerField()
label_translated = serializers.SerializerMethodField()
@ -79,12 +83,16 @@ class WineColorDocumentSerializer(serializers.Serializer):
return get_translated_value(obj.label)
class ProductEstablishmentDocumentSerializer(serializers.Serializer):
"""Related to Product Establishment ES document serializer."""
class ProductTypeDocumentSerializer(serializers.Serializer):
"""Product type ES document serializer."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
index_name = serializers.CharField()
name_translated = serializers.SerializerMethodField()
@staticmethod
def get_name_translated(obj):
return get_translated_value(obj.name)
class CityDocumentShortSerializer(serializers.Serializer):
@ -95,6 +103,39 @@ class CityDocumentShortSerializer(serializers.Serializer):
name = serializers.CharField()
class CountryDocumentSerializer(serializers.Serializer):
id = serializers.IntegerField()
code = serializers.CharField(allow_null=True)
svg_image = serializers.CharField()
name_translated = serializers.SerializerMethodField()
@staticmethod
def get_name_translated(obj):
return get_translated_value(obj.name)
class AnotherCityDocumentShortSerializer(CityDocumentShortSerializer):
country = CountryDocumentSerializer()
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 ProductEstablishmentDocumentSerializer(serializers.Serializer):
"""Related to Product Establishment ES document serializer."""
id = serializers.IntegerField()
name = serializers.CharField()
slug = serializers.CharField()
index_name = serializers.CharField()
city = AnotherCityDocumentShortSerializer()
class AddressDocumentSerializer(serializers.Serializer):
"""Address serializer for ES Document."""
@ -132,7 +173,7 @@ class NewsDocumentSerializer(DocumentSerializer):
title_translated = serializers.SerializerMethodField(allow_null=True)
subtitle_translated = serializers.SerializerMethodField(allow_null=True)
news_type = NewsTypeSerializer()
tags = TagsDocumentSerializer(many=True)
tags = TagsDocumentSerializer(many=True, source='visible_tags')
class Meta:
"""Meta class."""
@ -165,7 +206,7 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
establishment_type = EstablishmentTypeSerializer()
establishment_subtypes = EstablishmentTypeSerializer(many=True)
address = AddressDocumentSerializer(allow_null=True)
tags = TagsDocumentSerializer(many=True)
tags = TagsDocumentSerializer(many=True, source='visible_tags')
schedule = ScheduleDocumentSerializer(many=True, allow_null=True)
class Meta:
@ -198,17 +239,14 @@ class EstablishmentDocumentSerializer(DocumentSerializer):
class ProductDocumentSerializer(DocumentSerializer):
"""Product document serializer"""
tags = TagsDocumentSerializer(many=True)
subtypes = ProductSubtypeDocumentSerializer(many=True)
tags = TagsDocumentSerializer(many=True, source='related_tags')
subtypes = ProductSubtypeDocumentSerializer(many=True, allow_null=True)
wine_region = WineRegionDocumentSerializer(allow_null=True)
wine_colors = WineColorDocumentSerializer(many=True)
product_type = serializers.SerializerMethodField()
wine_colors = TagDocumentSerializer(many=True)
grape_variety = TagDocumentSerializer(many=True)
product_type = ProductTypeDocumentSerializer(allow_null=True)
establishment_detail = ProductEstablishmentDocumentSerializer(source='establishment', allow_null=True)
@staticmethod
def get_product_type(obj):
return get_translated_value(obj.product_type.name if obj.product_type else {})
class Meta:
"""Meta class."""
@ -216,6 +254,7 @@ class ProductDocumentSerializer(DocumentSerializer):
fields = (
'id',
'category',
'preview_image_url',
'name',
'available',
'public_mark',
@ -229,5 +268,7 @@ class ProductDocumentSerializer(DocumentSerializer):
'subtypes',
'wine_region',
'wine_colors',
'grape_variety',
'establishment_detail',
'average_price',
)

View File

@ -28,8 +28,10 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
]
search_fields = {
'title': {'fuzziness': 'auto:2,5'},
'subtitle': {'fuzziness': 'auto:2,5'},
'title': {'fuzziness': 'auto:2,5',
'boost': 3},
'subtitle': {'fuzziness': 'auto:2,5',
'boost': 2},
'description': {'fuzziness': 'auto:2,5'},
}
translated_search_fields = (
@ -86,11 +88,9 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
search_fields = {
'name': {'fuzziness': 'auto:2,5',
'boost': '2'},
'boost': 4},
'transliterated_name': {'fuzziness': 'auto:2,5',
'boost': '2'},
'index_name': {'fuzziness': 'auto:2,5',
'boost': '2'},
'boost': 3},
'description': {'fuzziness': 'auto:2,5'},
}
translated_search_fields = (
@ -124,6 +124,20 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
constants.LOOKUP_QUERY_IN,
]
},
'wine_region_id': {
'field': 'products.wine_region.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
],
},
'wine_sub_region_id': {
'field': 'products.wine_sub_region_id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
],
},
'country_id': {
'field': 'address.city.country.id'
},
@ -197,32 +211,27 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
"""Product document ViewSet."""
document = ProductDocument
lookup_field = 'slug'
pagination_class = ProjectMobilePagination
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.ProductDocumentSerializer
# def get_queryset(self):
# qs = super(ProductDocumentViewSet, self).get_queryset()
# qs = qs.filter('match', is_publish=True)
# return qs
filter_backends = [
FilteringFilterBackend,
filters.CustomSearchFilterBackend,
GeoSpatialFilteringFilterBackend,
DefaultOrderingFilterBackend,
]
search_fields = {
'name': {'fuzziness': 'auto:2,5',
'boost': '2'},
'boost': 8},
'name_ru': {'fuzziness': 'auto:2,5',
'boost': 6},
'name_fr': {'fuzziness': 'auto:2,5',
'boost': 7},
'transliterated_name': {'fuzziness': 'auto:2,5',
'boost': '2'},
'index_name': {'fuzziness': 'auto:2,5',
'boost': '2'},
'boost': 3},
'description': {'fuzziness': 'auto:2,5'},
}
translated_search_fields = (
'description',
)
@ -231,10 +240,34 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
'slug': 'slug',
'tags_id': {
'field': 'tags.id',
'lookups': [constants.LOOKUP_QUERY_IN]
'lookups': [constants.LOOKUP_QUERY_IN],
},
'country': {
'field': 'establishment.address.city.country.code',
},
'wine_colors_id': {
'field': 'wine_colors.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
],
},
'wine_region_id': {
'field': 'wine_region.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
],
},
'wine_sub_region_id': {
'field': 'wine_sub_region_id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
],
},
'grape_variety_id': {
'field': 'grape_variety.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
@ -246,7 +279,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
'for_establishment': {
'field': 'establishment.slug',
},
'type': {
'product_type': {
'field': 'product_type.index_name',
},
'subtype': {
@ -254,8 +287,6 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
]
}
}
geo_spatial_filter_fields = {
],
},
}

View File

@ -3,6 +3,7 @@ from django_filters import rest_framework as filters
from establishment.models import EstablishmentType
from django.conf import settings
from tag import models
from product import models as product_models
class TagsBaseFilterSet(filters.FilterSet):
@ -31,27 +32,49 @@ class TagCategoryFilterSet(TagsBaseFilterSet):
"""TagCategory filterset."""
establishment_type = filters.CharFilter(method='by_establishment_type')
product_type = filters.CharFilter(method='by_product_type')
class Meta:
"""Meta class."""
model = models.TagCategory
fields = ('type',
'establishment_type', )
'establishment_type',
'product_type', )
def by_product_type(self, queryset, name, value):
# if value == product_models.ProductType.WINE:
# queryset = queryset.filter(index_name='wine-color').filter(tags__products__isnull=False)
queryset = queryset.by_product_type(value)
return queryset
# todo: filter by establishment type
def by_establishment_type(self, queryset, name, value):
return queryset.by_establishment_type(value)
if value == EstablishmentType.ARTISAN:
qs = models.TagCategory.objects.filter(index_name='shop_category')
else:
qs = queryset.by_establishment_type(value)
return qs
class TagsFilterSet(TagsBaseFilterSet):
"""Chosen tags filterset."""
establishment_type = filters.CharFilter(method='by_establishment_type')
class Meta:
"""Meta class."""
model = models.Tag
fields = ('type',)
fields = (
'type',
'establishment_type',
)
def by_establishment_type(self, queryset, name, value):
if value == EstablishmentType.ARTISAN:
return models.Tag.objects.by_category_index_name('shop_category')[0:8]
return queryset.by_establishment_type(value)
# TMP TODO remove it later
# Временный хардкод для демонстрации 4 ноября, потом удалить!
@ -66,3 +89,4 @@ class TagsFilterSet(TagsBaseFilterSet):
queryset = queryset.for_establishments().filter(value__in=settings.ESTABLISHMENT_CHOSEN_TAGS).distinct(
'value')
return queryset

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