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""" """Back account serializers"""
from rest_framework import serializers from rest_framework import serializers
from account import models from account import models
from account.models import User
class RoleSerializer(serializers.ModelSerializer): class RoleSerializer(serializers.ModelSerializer):
@ -18,4 +19,33 @@ class UserRoleSerializer(serializers.ModelSerializer):
fields = [ fields = [
'user', 'user',
'role' '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 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): class ChangePasswordSerializer(serializers.ModelSerializer):
"""Serializer for model User.""" """Serializer for model User."""

View File

@ -84,3 +84,52 @@ class UserRoleTests(APITestCase):
response = self.client.post(url, data=data, format='json') response = self.client.post(url, data=data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED) 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 = [ urlpatterns = [
path('role/', views.RoleLstView.as_view(), name='role-list-create'), path('role/', views.RoleLstView.as_view(), name='role-list-create'),
path('user-role/', views.UserRoleLstView.as_view(), name='user-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 django_filters.rest_framework import DjangoFilterBackend
from account.serializers import back as serializers from rest_framework import generics, permissions
from account import models from account import models
from account.models import User
from account.serializers import back as serializers
class RoleLstView(generics.ListCreateAPIView): class RoleLstView(generics.ListCreateAPIView):
@ -10,4 +13,27 @@ class RoleLstView(generics.ListCreateAPIView):
class UserRoleLstView(generics.ListCreateAPIView): class UserRoleLstView(generics.ListCreateAPIView):
serializer_class = serializers.UserRoleSerializer 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) return Response(status=status.HTTP_200_OK)
class ConfirmEmailView(JWTGenericViewMixin): class ConfirmEmailView(JWTGenericViewMixin, generics.GenericAPIView):
"""View for confirm changing email""" """View for confirm changing email"""
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)

View File

@ -33,7 +33,7 @@ class PasswordResetView(generics.GenericAPIView):
return Response(status=status.HTTP_200_OK) return Response(status=status.HTTP_200_OK)
class PasswordResetConfirmView(JWTGenericViewMixin): class PasswordResetConfirmView(JWTGenericViewMixin, generics.GenericAPIView):
"""View for confirmation new password""" """View for confirmation new password"""
serializer_class = serializers.PasswordResetConfirmSerializer serializer_class = serializers.PasswordResetConfirmSerializer
permission_classes = (permissions.AllowAny,) 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 import uuid
from django.db import models from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from main.models import Page
from translation.models import Language from translation.models import Language
from utils.models import ProjectBaseMixin, ImageMixin, PlatformMixin, URLImageMixin from utils.models import ProjectBaseMixin, ImageMixin, PlatformMixin, URLImageMixin
from main.models import Page
class AdvertisementQuerySet(models.QuerySet): class AdvertisementQuerySet(models.QuerySet):
@ -25,6 +26,10 @@ class AdvertisementQuerySet(models.QuerySet):
"""Filter by locale.""" """Filter by locale."""
return self.filter(target_languages__locale=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): class Advertisement(ProjectBaseMixin):
"""Advertisement model.""" """Advertisement model."""
@ -50,11 +55,17 @@ class Advertisement(ProjectBaseMixin):
class Meta: class Meta:
verbose_name = _('Advertisement') verbose_name = _('Advertisement')
verbose_name_plural = _('Advertisement') verbose_name_plural = _('Advertisements')
def __str__(self): def __str__(self):
return str(self.url) 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 @property
def mobile_page(self): def mobile_page(self):
"""Return mobile page""" """Return mobile page"""

View File

@ -1,3 +1,4 @@
from .common import * from .common import *
from .mobile import * from .mobile import *
from .web 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 advertisement import models
from translation.serializers import LanguageSerializer from translation.serializers import LanguageSerializer
from main.serializers import SiteShortSerializer from main.serializers import SiteShortSerializer, PageBaseSerializer
from main.serializers import PageBaseSerializer from translation.models import Language
from main.models import SiteSettings
class AdvertisementBaseSerializer(serializers.ModelSerializer): class AdvertisementBaseSerializer(serializers.ModelSerializer):
"""Base serializer for model Advertisement.""" """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) sites = SiteShortSerializer(many=True, read_only=True)
target_sites = serializers.PrimaryKeyRelatedField(
queryset=SiteSettings.objects.all(),
many=True,
write_only=True,
source='sites'
)
class Meta: class Meta:
model = models.Advertisement model = models.Advertisement
@ -21,7 +34,9 @@ class AdvertisementBaseSerializer(serializers.ModelSerializer):
'url', 'url',
'block_level', 'block_level',
'languages', 'languages',
'target_languages',
'sites', 'sites',
'target_sites',
'start', 'start',
'end', 'end',
] ]

View File

@ -1,20 +1,42 @@
from pprint import pprint from pprint import pprint
from transfer.models import Ads from transfer.models import Ads
from transfer.serializers.advertisement import AdvertisementSerializer from transfer.serializers.advertisement import AdvertisementSerializer, AdvertisementImageSerializer
def transfer_advertisement(): 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) serialized_data = AdvertisementSerializer(data=list(queryset.values()), many=True)
if serialized_data.is_valid(): if serialized_data.is_valid():
serialized_data.save() serialized_data.save()
else: 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 = { 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.""" """Advertisement common urlpaths."""
from django.urls import path from django.urls import path
from advertisement import views
app_name = 'advertisements' app_name = 'advertisements'

View File

@ -1,3 +1,4 @@
from .common import * from .common import *
from .mobile import * from .mobile import *
from .web 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 # 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 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) return Response(status=status.HTTP_201_CREATED)
class ConfirmationEmailView(JWTGenericViewMixin): class ConfirmationEmailView(JWTGenericViewMixin, generics.GenericAPIView):
"""View for confirmation email""" """View for confirmation email"""
permission_classes = (permissions.AllowAny, ) permission_classes = (permissions.AllowAny, )
@ -174,7 +174,7 @@ class ConfirmationEmailView(JWTGenericViewMixin):
# Login by username|email + password # Login by username|email + password
class LoginByUsernameOrEmailView(JWTGenericViewMixin): class LoginByUsernameOrEmailView(JWTGenericViewMixin, generics.GenericAPIView):
"""Login by email and password""" """Login by email and password"""
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.LoginByUsernameOrEmailSerializer serializer_class = serializers.LoginByUsernameOrEmailSerializer
@ -197,7 +197,7 @@ class LoginByUsernameOrEmailView(JWTGenericViewMixin):
# Logout # Logout
class LogoutView(JWTGenericViewMixin): class LogoutView(JWTGenericViewMixin, generics.GenericAPIView):
"""Logout user""" """Logout user"""
permission_classes = (IsAuthenticatedAndTokenIsValid, ) permission_classes = (IsAuthenticatedAndTokenIsValid, )
@ -215,7 +215,7 @@ class LogoutView(JWTGenericViewMixin):
# Refresh token # Refresh token
class RefreshTokenView(JWTGenericViewMixin): class RefreshTokenView(JWTGenericViewMixin, generics.GenericAPIView):
"""Refresh access_token""" """Refresh access_token"""
permission_classes = (permissions.AllowAny, ) permission_classes = (permissions.AllowAny, )
serializer_class = serializers.RefreshTokenSerializer serializer_class = serializers.RefreshTokenSerializer

View File

@ -13,10 +13,50 @@ from utils.methods import get_user_ip
class CheckWhetherBookingAvailable(generics.GenericAPIView): class CheckWhetherBookingAvailable(generics.GenericAPIView):
""" Checks which service to use if establishmend is managed by any """ """ 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,) permission_classes = (permissions.AllowAny,)
serializer_class = CheckBookingSerializer serializer_class = CheckBookingSerializer
pagination_class = None 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): def get(self, request, *args, **kwargs):
is_booking_available = False is_booking_available = False
establishment = get_object_or_404(Establishment, pk=kwargs['establishment_id']) establishment = get_object_or_404(Establishment, pk=kwargs['establishment_id'])
@ -24,12 +64,12 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView):
date = request.query_params.get('date') date = request.query_params.get('date')
g_service = GuestonlineService() g_service = GuestonlineService()
l_service = LastableService() 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): .check_whether_booking_available(establishment.lastable_id, date):
is_booking_available = True is_booking_available = True
service = l_service service = l_service
service.service_id = establishment.lastable_id 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, .check_whether_booking_available(establishment.guestonline_id,
**g_service.get_certain_keys(request.query_params, **g_service.get_certain_keys(request.query_params,
{'date', 'persons'})): {'date', 'persons'})):
@ -41,7 +81,11 @@ class CheckWhetherBookingAvailable(generics.GenericAPIView):
'available': is_booking_available, 'available': is_booking_available,
'type': service.service if service else None, '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) return Response(data=response, status=200)
@ -97,8 +141,9 @@ class UpdatePendingBooking(generics.UpdateAPIView):
r = service.update_booking(service.get_certain_keys(data, { 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', '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): if isinstance(r, Response):
return r return r
if data.get('newsletter'): if data.get('newsletter'):

View File

@ -1,8 +1,14 @@
from rest_framework import serializers 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.models import Country
from location.serializers import CountrySimpleSerializer from location.serializers import CountrySimpleSerializer
from collection.serializers.common import CollectionBaseSerializer from product.models import Product
from collection import models from utils.exceptions import (
BindingObjectNotFound, RemovedBindingObjectNotFound, ObjectAlreadyAdded
)
class CollectionBackOfficeSerializer(CollectionBaseSerializer): class CollectionBackOfficeSerializer(CollectionBaseSerializer):
@ -31,3 +37,54 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer):
'start', 'start',
'end', '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.""" """Collection common urlpaths."""
from django.urls import path from rest_framework.routers import SimpleRouter
from collection.views import back as views from collection.views import back as views
app_name = 'collection' app_name = 'collection'
router = SimpleRouter()
router.register(r'', views.CollectionBackOfficeViewSet)
urlpatterns = [ urlpatterns = router.urls
path('', views.CollectionListCreateView.as_view(), name='list-create'),
path('<int:pk>/', views.CollectionRUDView.as_view(), name='rud-collection'),
]

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 import models
from collection.serializers import back from collection.serializers import back as serializers
from utils.views import BindObjectMixin
class CollectionListCreateView(generics.ListCreateAPIView): class CollectionViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
"""Collection list-create view.""" """ViewSet for Collection model."""
pagination_class = None
permission_classes = (permissions.AllowAny,)
queryset = models.Collection.objects.all() queryset = models.Collection.objects.all()
serializer_class = back.CollectionBackOfficeSerializer serializer_class = serializers.CollectionBackOfficeSerializer
# todo: conf. permissions by TT
permission_classes = (permissions.IsAuthenticated, )
class CollectionRUDView(generics.RetrieveUpdateDestroyAPIView): class CollectionBackOfficeViewSet(mixins.CreateModelMixin,
"""Collection list-create view.""" mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.RetrieveModelMixin,
BindObjectMixin,
CollectionViewSet):
"""ViewSet for Collection model for BackOffice users."""
permission_classes = (permissions.IsAuthenticated,)
queryset = models.Collection.objects.all() queryset = models.Collection.objects.all()
serializer_class = back.CollectionBackOfficeSerializer serializer_class = serializers.CollectionBackOfficeSerializer
# todo: conf. permissions by TT bind_object_serializer_class = serializers.CollectionBindObjectSerializer
permission_classes = (permissions.IsAuthenticated, )
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 django.shortcuts import get_object_or_404
from rest_framework import permissions from rest_framework import generics, permissions, pagination
from collection import models 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 collection.serializers import common as serializers
from establishment.serializers import EstablishmentSimilarSerializer
from utils.pagination import ProjectPageNumberPagination, ProjectMobilePagination
# Mixins # Mixins
@ -18,8 +17,8 @@ class CollectionViewMixin(generics.GenericAPIView):
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method.""" """Override get_queryset method."""
return models.Collection.objects.published() \ return models.Collection.objects.published() \
.by_country_code(code=self.request.country_code) \ .by_country_code(code=self.request.country_code) \
.order_by('-on_top', '-modified') .order_by('-on_top', '-modified')
class GuideViewMixin(generics.GenericAPIView): class GuideViewMixin(generics.GenericAPIView):
@ -40,7 +39,7 @@ class CollectionHomePageView(CollectionListView):
def get_queryset(self): def get_queryset(self):
"""Override get_queryset.""" """Override get_queryset."""
return super(CollectionHomePageView, self).get_queryset() \ return super(CollectionHomePageView, self).get_queryset() \
.filter_all_related_gt(3) .filter_all_related_gt(3)
class CollectionDetailView(CollectionViewMixin, generics.RetrieveAPIView): class CollectionDetailView(CollectionViewMixin, generics.RetrieveAPIView):
@ -52,8 +51,8 @@ class CollectionDetailView(CollectionViewMixin, generics.RetrieveAPIView):
class CollectionEstablishmentListView(CollectionListView): class CollectionEstablishmentListView(CollectionListView):
"""Retrieve list of establishment for collection.""" """Retrieve list of establishment for collection."""
lookup_field = 'slug' lookup_field = 'slug'
pagination_class = ProjectPageNumberPagination pagination_class = ProjectMobilePagination
serializer_class = EstablishmentBaseSerializer serializer_class = EstablishmentSimilarSerializer
def get_queryset(self): def get_queryset(self):
""" """
@ -66,7 +65,7 @@ class CollectionEstablishmentListView(CollectionListView):
# May raise a permission denied # May raise a permission denied
self.check_object_permissions(self.request, collection) self.check_object_permissions(self.request, collection)
return collection.establishments.all() return collection.establishments.all().annotate_in_favorites(self.request.user)
# Guide # Guide

View File

@ -7,7 +7,7 @@ from comment.models import Comment
from utils.admin import BaseModelAdminMixin from utils.admin import BaseModelAdminMixin
from establishment import models from establishment import models
from main.models import Award from main.models import Award
from product.models import Product from product.models import Product, PurchasedProduct
from review import models as review_models from review import models as review_models
@ -32,6 +32,12 @@ class ContactPhoneInline(admin.TabularInline):
extra = 0 extra = 0
class GalleryImageInline(admin.TabularInline):
"""Gallery image inline admin."""
model = models.EstablishmentGallery
extra = 0
class ContactEmailInline(admin.TabularInline): class ContactEmailInline(admin.TabularInline):
"""Contact email inline admin.""" """Contact email inline admin."""
model = models.ContactEmail model = models.ContactEmail
@ -53,12 +59,29 @@ class ProductInline(admin.TabularInline):
extra = 0 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) @admin.register(models.Establishment)
class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin): class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""Establishment admin.""" """Establishment admin."""
list_display = ['id', '__str__', 'image_tag', ] list_display = ['id', '__str__', 'image_tag', ]
search_fields = ['id', 'name', 'index_name', 'slug'] search_fields = ['id', 'name', 'index_name', 'slug']
list_filter = ['public_mark', 'toque_number'] list_filter = ['public_mark', 'toque_number']
inlines = [GalleryImageInline, CompanyInline, EstablishmentNote,
PurchasedProduct]
# inlines = [ # inlines = [
# AwardInline, ContactPhoneInline, ContactEmailInline, # AwardInline, ContactPhoneInline, ContactEmailInline,
@ -107,3 +130,9 @@ class SocialChoiceAdmin(BaseModelAdminMixin, admin.ModelAdmin):
class SocialNetworkAdmin(BaseModelAdminMixin, admin.ModelAdmin): class SocialNetworkAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""Admin conf for SocialNetwork model.""" """Admin conf for SocialNetwork model."""
raw_id_fields = ('establishment',) 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.db.models.functions import Distance
from django.contrib.gis.geos import Point from django.contrib.gis.geos import Point
from django.contrib.gis.measure import Distance as DistanceMeasure 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.exceptions import ValidationError
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models from django.db import models
from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q from django.db.models import When, Case, F, ExpressionWrapper, Subquery, Q
from django.utils import timezone from django.utils import timezone
@ -22,7 +24,8 @@ from location.models import Address
from main.models import Award, Currency from main.models import Award, Currency
from review.models import Review from review.models import Review
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes) TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
IntermediateGalleryModelMixin, HasTagsMixin)
# todo: establishment type&subtypes check # todo: establishment type&subtypes check
@ -316,9 +319,11 @@ class EstablishmentQuerySet(models.QuerySet):
return self.exclude(address__city__country__in=countries) return self.exclude(address__city__country__in=countries)
class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin, HasTagsMixin):
"""Establishment model.""" """Establishment model."""
# todo: delete image URL fields after moving on gallery
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
name = models.CharField(_('name'), max_length=255, default='') name = models.CharField(_('name'), max_length=255, default='')
transliterated_name = models.CharField(default='', max_length=255, transliterated_name = models.CharField(default='', max_length=255,
@ -376,6 +381,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
related_name='establishments', related_name='establishments',
blank=True, default=None, blank=True, default=None,
verbose_name=_('Collections')) verbose_name=_('Collections'))
gallery = models.ManyToManyField('gallery.Image', through='EstablishmentGallery')
preview_image_url = models.URLField(verbose_name=_('Preview image URL path'), max_length=255, preview_image_url = models.URLField(verbose_name=_('Preview image URL path'), max_length=255,
blank=True, null=True, default=None) blank=True, null=True, default=None)
slug = models.SlugField(unique=True, max_length=255, null=True, 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, currency = models.ForeignKey(Currency, blank=True, null=True, default=None,
on_delete=models.PROTECT, on_delete=models.PROTECT,
verbose_name=_('currency')) 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() objects = EstablishmentQuerySet.as_manager()
@ -403,6 +416,26 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
def __str__(self): def __str__(self):
return f'id:{self.id}-{self.name}' 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 # todo: recalculate toque_number
def recalculate_toque_number(self): def recalculate_toque_number(self):
toque_number = 0 toque_number = 0
@ -544,6 +577,12 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
"""Return list products with type wine""" """Return list products with type wine"""
return self.products.wines() 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): class EstablishmentNoteQuerySet(models.QuerySet):
"""QuerySet for model EstablishmentNote.""" """QuerySet for model EstablishmentNote."""
@ -554,7 +593,7 @@ class EstablishmentNote(ProjectBaseMixin):
old_id = models.PositiveIntegerField(null=True, blank=True) old_id = models.PositiveIntegerField(null=True, blank=True)
text = models.TextField(verbose_name=_('text')) text = models.TextField(verbose_name=_('text'))
establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT, establishment = models.ForeignKey(Establishment, on_delete=models.PROTECT,
related_name='establishment_notes', related_name='notes',
verbose_name=_('establishment')) verbose_name=_('establishment'))
user = models.ForeignKey('account.User', on_delete=models.PROTECT, user = models.ForeignKey('account.User', on_delete=models.PROTECT,
null=True, null=True,
@ -565,8 +604,26 @@ class EstablishmentNote(ProjectBaseMixin):
class Meta: class Meta:
"""Meta class.""" """Meta class."""
verbose_name_plural = _('product note') verbose_name_plural = _('establishment notes')
verbose_name = _('product 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): class Position(BaseAttributes, TranslatedFieldsMixin):
@ -837,3 +894,46 @@ class RatingStrategy(ProjectBaseMixin):
return f'{self.country.code if self.country else "Other country"}. ' \ return f'{self.country.code if self.country else "Other country"}. ' \
f'"{self.toque_number}": {self.public_mark_min_value}-' \ f'"{self.toque_number}": {self.public_mark_min_value}-' \
f'{self.public_mark_max_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 rest_framework import serializers
from establishment import models from establishment import models
from establishment.serializers import ( from establishment import serializers as model_serializers
EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer,
ContactPhonesSerializer, SocialNetworkRelatedSerializers,
EstablishmentTypeBaseSerializer)
from location.serializers import AddressDetailSerializer from location.serializers import AddressDetailSerializer
from main.models import Currency from main.models import Currency
from utils.decorators import with_base_attributes from utils.decorators import with_base_attributes
from utils.serializers import TimeZoneChoiceField 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""" """Establishment create serializer"""
type_id = serializers.PrimaryKeyRelatedField( type_id = serializers.PrimaryKeyRelatedField(
source='establishment_type', 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, ) phones = model_serializers.ContactPhonesSerializer(read_only=True,
emails = ContactEmailsSerializer(read_only=True, many=True, ) many=True, )
socials = SocialNetworkRelatedSerializers(read_only=True, many=True, ) emails = model_serializers.ContactEmailsSerializer(read_only=True,
type = EstablishmentTypeBaseSerializer(source='establishment_type', 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() tz = TimeZoneChoiceField()
class Meta: class Meta:
@ -50,7 +55,7 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
] ]
class EstablishmentRUDSerializer(EstablishmentBaseSerializer): class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
"""Establishment create serializer""" """Establishment create serializer"""
type_id = serializers.PrimaryKeyRelatedField( type_id = serializers.PrimaryKeyRelatedField(
@ -58,10 +63,13 @@ class EstablishmentRUDSerializer(EstablishmentBaseSerializer):
queryset=models.EstablishmentType.objects.all(), write_only=True queryset=models.EstablishmentType.objects.all(), write_only=True
) )
address = AddressDetailSerializer() address = AddressDetailSerializer()
phones = ContactPhonesSerializer(read_only=False, many=True, ) phones = model_serializers.ContactPhonesSerializer(read_only=False,
emails = ContactEmailsSerializer(read_only=False, many=True, ) many=True, )
socials = SocialNetworkRelatedSerializers(read_only=False, many=True, ) emails = model_serializers.ContactEmailsSerializer(read_only=False,
type = EstablishmentTypeBaseSerializer(source='establishment_type') many=True, )
socials = model_serializers.SocialNetworkRelatedSerializers(read_only=False,
many=True, )
type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type')
class Meta: class Meta:
model = models.Establishment model = models.Establishment
@ -105,7 +113,7 @@ class SocialNetworkSerializers(serializers.ModelSerializer):
] ]
class PlatesSerializers(PlateSerializer): class PlatesSerializers(model_serializers.PlateSerializer):
"""Plates serializers.""" """Plates serializers."""
currency_id = serializers.PrimaryKeyRelatedField( currency_id = serializers.PrimaryKeyRelatedField(
@ -117,14 +125,14 @@ class PlatesSerializers(PlateSerializer):
"""Meta class.""" """Meta class."""
model = models.Plate model = models.Plate
fields = PlateSerializer.Meta.fields + [ fields = model_serializers.PlateSerializer.Meta.fields + [
'name', 'name',
'currency_id', 'currency_id',
'menu' 'menu'
] ]
class ContactPhoneBackSerializers(PlateSerializer): class ContactPhoneBackSerializers(model_serializers.PlateSerializer):
"""ContactPhone serializers.""" """ContactPhone serializers."""
class Meta: class Meta:
@ -136,7 +144,7 @@ class ContactPhoneBackSerializers(PlateSerializer):
] ]
class ContactEmailBackSerializers(PlateSerializer): class ContactEmailBackSerializers(model_serializers.PlateSerializer):
"""ContactEmail serializers.""" """ContactEmail serializers."""
class Meta: class Meta:
@ -160,3 +168,112 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
'user', 'user',
'name' '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.""" """Establishment serializers."""
from django.utils.translation import ugettext_lazy as _ 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 rest_framework import serializers
from comment import models as comment_models from comment import models as comment_models
from comment.serializers import common as comment_serializers from comment.serializers import common as comment_serializers
from establishment import models 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 main.serializers import AwardSerializer, CurrencySerializer
from review.serializers import ReviewShortSerializer
from tag.serializers import TagBaseSerializer from tag.serializers import TagBaseSerializer
from timetable.serialziers import ScheduleRUDSerializer from timetable.serialziers import ScheduleRUDSerializer
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from utils.serializers import ImageBaseSerializer
from utils.serializers import (ProjectModelSerializer, TranslatedField, from utils.serializers import (ProjectModelSerializer, TranslatedField,
FavoritesCreateSerializer) FavoritesCreateSerializer)
from review.serializers import ReviewShortSerializer
class ContactPhonesSerializer(serializers.ModelSerializer): class ContactPhonesSerializer(serializers.ModelSerializer):
"""Contact phone serializer""" """Contact phone serializer"""
class Meta: class Meta:
model = models.ContactPhone model = models.ContactPhone
fields = [ fields = [
@ -26,6 +30,7 @@ class ContactPhonesSerializer(serializers.ModelSerializer):
class ContactEmailsSerializer(serializers.ModelSerializer): class ContactEmailsSerializer(serializers.ModelSerializer):
"""Contact email serializer""" """Contact email serializer"""
class Meta: class Meta:
model = models.ContactEmail model = models.ContactEmail
fields = [ fields = [
@ -35,6 +40,7 @@ class ContactEmailsSerializer(serializers.ModelSerializer):
class SocialNetworkRelatedSerializers(serializers.ModelSerializer): class SocialNetworkRelatedSerializers(serializers.ModelSerializer):
"""Social network serializers.""" """Social network serializers."""
class Meta: class Meta:
model = models.SocialNetwork model = models.SocialNetwork
fields = [ fields = [
@ -45,7 +51,6 @@ class SocialNetworkRelatedSerializers(serializers.ModelSerializer):
class PlateSerializer(ProjectModelSerializer): class PlateSerializer(ProjectModelSerializer):
name_translated = TranslatedField() name_translated = TranslatedField()
currency = CurrencySerializer(read_only=True) currency = CurrencySerializer(read_only=True)
@ -176,6 +181,7 @@ class EstablishmentShortSerializer(serializers.ModelSerializer):
city = CitySerializer(source='address.city', allow_null=True) city = CitySerializer(source='address.city', allow_null=True)
establishment_type = EstablishmentTypeGeoSerializer() establishment_type = EstablishmentTypeGeoSerializer()
establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True) establishment_subtypes = EstablishmentSubTypeBaseSerializer(many=True)
currency = CurrencySerializer(read_only=True)
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -188,6 +194,31 @@ class EstablishmentShortSerializer(serializers.ModelSerializer):
'city', 'city',
'establishment_type', 'establishment_type',
'establishment_subtypes', '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): class EstablishmentBaseSerializer(ProjectModelSerializer):
"""Base serializer for Establishment model.""" """Base serializer for Establishment model."""
preview_image = serializers.URLField(source='preview_image_url')
address = AddressBaseSerializer() address = AddressBaseSerializer()
in_favorites = serializers.BooleanField(allow_null=True) 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() currency = CurrencySerializer()
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True) type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes') 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: class Meta:
"""Meta class.""" """Meta class."""
@ -227,13 +263,15 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
'toque_number', 'toque_number',
'public_mark', 'public_mark',
'slug', 'slug',
'preview_image',
'in_favorites', 'in_favorites',
'address', 'address',
'tags', 'tags',
'currency', 'currency',
'type', 'type',
'subtypes', 'subtypes',
'image',
'preview_image',
'new_image',
] ]
@ -272,7 +310,6 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
"""Serializer for Establishment model.""" """Serializer for Establishment model."""
description_translated = TranslatedField() description_translated = TranslatedField()
image = serializers.URLField(source='image_url')
awards = AwardSerializer(many=True) awards = AwardSerializer(many=True)
schedule = ScheduleRUDSerializer(many=True, allow_null=True) schedule = ScheduleRUDSerializer(many=True, allow_null=True)
phones = ContactPhonesSerializer(read_only=True, many=True) phones = ContactPhonesSerializer(read_only=True, many=True)
@ -288,6 +325,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
range_price_menu = RangePriceSerializer(read_only=True) range_price_menu = RangePriceSerializer(read_only=True)
range_price_carte = RangePriceSerializer(read_only=True) range_price_carte = RangePriceSerializer(read_only=True)
vintage_year = serializers.ReadOnlyField() vintage_year = serializers.ReadOnlyField()
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
class Meta(EstablishmentBaseSerializer.Meta): class Meta(EstablishmentBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -313,9 +351,16 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
'range_price_carte', 'range_price_carte',
'transportation', 'transportation',
'vintage_year', 'vintage_year',
'gallery',
] ]
class EstablishmentSimilarSerializer(EstablishmentBaseSerializer):
"""Serializer for Establishment model."""
address = AddressDetailSerializer(read_only=True)
class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer): class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer):
"""Create comment serializer""" """Create comment serializer"""
mark = serializers.IntegerField() mark = serializers.IntegerField()
@ -380,3 +425,56 @@ class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
}) })
return super().create(validated_data) 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 establishment.models import Establishment
from location.models import Address 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, \ from transfer.serializers.establishment import EstablishmentSerializer, \
EstablishmentNoteSerializer EstablishmentNoteSerializer
from transfer.serializers.plate import PlateSerializer from transfer.serializers.plate import PlateSerializer
@ -140,6 +142,43 @@ def transfer_establishment_note():
pprint(f"transfer_establishment_note errors: {errors}") 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 = { data_types = {
"establishment": [ "establishment": [
transfer_establishment, transfer_establishment,
@ -149,4 +188,7 @@ data_types = {
transfer_establishment_addresses transfer_establishment_addresses
], ],
"menu": [transfer_menu], "menu": [transfer_menu],
"purchased_plaques": [
transfer_purchased_plaques
],
} }

View File

@ -13,6 +13,19 @@ urlpatterns = [
name='schedule-rud'), name='schedule-rud'),
path('<int:pk>/schedule/', views.EstablishmentScheduleCreateView.as_view(), path('<int:pk>/schedule/', views.EstablishmentScheduleCreateView.as_view(),
name='schedule-create'), 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/', views.MenuListCreateView.as_view(), name='menu-list'),
path('menus/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'), path('menus/<int:pk>/', views.MenuRUDView.as_view(), name='menu-rud'),
path('plates/', views.PlateListCreateView.as_view(), name='plates'), path('plates/', views.PlateListCreateView.as_view(), name='plates'),

View File

@ -2,9 +2,13 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions from rest_framework import generics, permissions
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
from establishment import filters, models, serializers from establishment import filters, models, serializers
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer 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: class EstablishmentMixinViews:
@ -33,13 +37,14 @@ class EstablishmentRUDView(generics.RetrieveUpdateDestroyAPIView):
class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView): class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment schedule RUD view""" """Establishment schedule RUD view"""
serializer_class = ScheduleRUDSerializer serializer_class = ScheduleRUDSerializer
permission_classes = [IsEstablishmentManager]
def get_object(self): def get_object(self):
""" """
Returns the object the view is displaying. Returns the object the view is displaying.
""" """
establishment_pk = self.kwargs['pk'] establishment_pk = self.kwargs.get('pk')
schedule_id = self.kwargs['schedule_id'] schedule_id = self.kwargs.get('schedule_id')
establishment = get_object_or_404(klass=models.Establishment.objects.all(), establishment = get_object_or_404(klass=models.Establishment.objects.all(),
pk=establishment_pk) pk=establishment_pk)
@ -56,6 +61,8 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
class EstablishmentScheduleCreateView(generics.CreateAPIView): class EstablishmentScheduleCreateView(generics.CreateAPIView):
"""Establishment schedule Create view""" """Establishment schedule Create view"""
serializer_class = ScheduleCreateSerializer serializer_class = ScheduleCreateSerializer
queryset = Timetable.objects.all()
permission_classes = [IsEstablishmentManager]
class MenuListCreateView(generics.ListCreateAPIView): class MenuListCreateView(generics.ListCreateAPIView):
@ -184,3 +191,130 @@ class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment subtype retrieve/update/destroy view.""" """Establishment subtype retrieve/update/destroy view."""
serializer_class = serializers.EstablishmentSubTypeBaseSerializer serializer_class = serializers.EstablishmentSubTypeBaseSerializer
queryset = models.EstablishmentSubType.objects.all() 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): class EstablishmentSimilarListView(EstablishmentListView):
"""Resource for getting a list of establishments.""" """Resource for getting a list of establishments."""
serializer_class = serializers.EstablishmentBaseSerializer serializer_class = serializers.EstablishmentSimilarSerializer
pagination_class = EstablishmentPortionPagination pagination_class = EstablishmentPortionPagination
def get_queryset(self): def get_queryset(self):

View File

@ -2,7 +2,7 @@
from rest_framework import generics from rest_framework import generics
from establishment.models import Establishment from establishment.models import Establishment
from establishment.filters import EstablishmentFilter from establishment.filters import EstablishmentFilter
from establishment.serializers import EstablishmentBaseSerializer from establishment.serializers import EstablishmentBaseSerializer, EstablishmentSimilarSerializer
from news.filters import NewsListFilterSet from news.filters import NewsListFilterSet
from news.models import News from news.models import News
from news.serializers import NewsBaseSerializer, NewsListSerializer from news.serializers import NewsBaseSerializer, NewsListSerializer
@ -23,7 +23,7 @@ class FavoritesBaseView(generics.GenericAPIView):
class FavoritesEstablishmentListView(generics.ListAPIView): class FavoritesEstablishmentListView(generics.ListAPIView):
"""List views for establishments in favorites.""" """List views for establishments in favorites."""
serializer_class = EstablishmentBaseSerializer serializer_class = EstablishmentSimilarSerializer
filter_class = EstablishmentFilter filter_class = EstablishmentFilter
def get_queryset(self): 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 translation.models import Language
from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField, from utils.models import (ProjectBaseMixin, SVGImageMixin, TJSONField,
TranslatedFieldsMixin, get_current_locale) TranslatedFieldsMixin, get_current_locale,
IntermediateGalleryModelMixin, GalleryModelMixin)
class CountryQuerySet(models.QuerySet): class CountryQuerySet(models.QuerySet):
@ -101,9 +102,8 @@ class CityQuerySet(models.QuerySet):
return self.filter(country__code=code) return self.filter(country__code=code)
class City(models.Model): class City(GalleryModelMixin):
"""Region model.""" """Region model."""
name = models.CharField(_('name'), max_length=250) name = models.CharField(_('name'), max_length=250)
name_translated = TJSONField(blank=True, null=True, default=None, name_translated = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Translated name'), help_text='{"en-GB":"some text"}') verbose_name=_('Translated name'), help_text='{"en-GB":"some text"}')
@ -138,16 +138,8 @@ class City(models.Model):
return self.name return self.name
class CityGalleryQuerySet(models.QuerySet): class CityGallery(IntermediateGalleryModelMixin):
"""QuerySet for model News""" """Gallery for model City."""
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)
city = models.ForeignKey(City, null=True, city = models.ForeignKey(City, null=True,
related_name='city_gallery', related_name='city_gallery',
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -155,14 +147,10 @@ class CityGallery(models.Model):
image = models.ForeignKey('gallery.Image', null=True, image = models.ForeignKey('gallery.Image', null=True,
related_name='city_gallery', related_name='city_gallery',
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_('gallery')) verbose_name=_('image'))
is_main = models.BooleanField(default=False,
verbose_name=_('Is the main image'))
objects = CityGalleryQuerySet.as_manager()
class Meta: class Meta:
"""NewsGallery meta class.""" """CityGallery meta class."""
verbose_name = _('city gallery') verbose_name = _('city gallery')
verbose_name_plural = _('city galleries') verbose_name_plural = _('city galleries')
unique_together = (('city', 'is_main'), ('city', 'image')) unique_together = (('city', 'is_main'), ('city', 'image'))
@ -216,6 +204,13 @@ class Address(models.Model):
class WineRegionQuerySet(models.QuerySet): class WineRegionQuerySet(models.QuerySet):
"""Wine region 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): class WineRegion(models.Model, TranslatedFieldsMixin):
"""Wine region model.""" """Wine region model."""
@ -254,6 +249,7 @@ class WineSubRegion(models.Model):
"""Wine sub region model.""" """Wine sub region model."""
name = models.CharField(_('name'), max_length=255) name = models.CharField(_('name'), max_length=255)
wine_region = models.ForeignKey(WineRegion, on_delete=models.PROTECT, wine_region = models.ForeignKey(WineRegion, on_delete=models.PROTECT,
related_name='wine_sub_region',
verbose_name=_('wine sub region')) verbose_name=_('wine sub region'))
old_id = models.PositiveIntegerField(_('old id'), default=None, old_id = models.PositiveIntegerField(_('old id'), default=None,
blank=True, null=True) blank=True, null=True)

View File

@ -1,5 +1,8 @@
from location import models from location import models
from location.serializers import common 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): class AddressCreateSerializer(common.AddressDetailSerializer):
@ -18,3 +21,45 @@ class CountryBackSerializer(common.CountrySerializer):
'name', 'name',
'country_id' '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' '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): class CropImageSerializer(serializers.Serializer):
"""Serializer for crop images for City object.""" """Serializer for crop images for City object."""
@ -273,3 +287,14 @@ class WineSubRegionBaseSerializer(serializers.ModelSerializer):
'id', 'id',
'name', '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.serializers import location as location_serializers
from transfer import models as transfer_models 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 from pprint import pprint
import json import json
from gallery.models import Image
from pprint import pprint
from django.conf import settings from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned from django.core.exceptions import MultipleObjectsReturned
from collection.models import Collection from collection.models import Collection
@ -494,6 +497,42 @@ def fix_location_models():
fix_chosen_tag() 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 = { data_types = {
"dictionaries": [ "dictionaries": [
transfer_countries, transfer_countries,
@ -515,6 +554,8 @@ data_types = {
], ],
"fix_location": [ "fix_location": [
fix_location_models 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/', views.CityListCreateView.as_view(), name='city-list-create'),
path('cities/<int:pk>/', views.CityRUDView.as_view(), name='city-retrieve'), 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/', views.CountryListCreateView.as_view(), name='country-list-create'),
path('countries/<int:pk>/', views.CountryRUDView.as_view(), name='country-retrieve'), 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/', views.RegionListView.as_view(), name='region-list'),
path('regions/<int:pk>/', views.RegionRetrieveView.as_view(), name='region-retrieve'), 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 import models, serializers
from location.views import common from location.views import common
from utils.permissions import IsCountryAdmin 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 # Address
@ -35,6 +39,48 @@ class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] 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 # Region
class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView): class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region""" """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 import generics, permissions, status
from rest_framework.response import Response from rest_framework.response import Response
from gallery.tasks import delete_image 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 location import models, serializers
from utils.models import get_current_locale
# Mixins # Mixins
@ -41,7 +44,9 @@ class CountryListView(CountryViewMixin, generics.ListAPIView):
"""List view for model Country.""" """List view for model Country."""
pagination_class = None 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): class CountryRetrieveView(CountryViewMixin, generics.RetrieveAPIView):
"""Retrieve view for model Country.""" """Retrieve view for model Country."""
@ -64,6 +69,15 @@ class RegionListView(RegionViewMixin, generics.ListAPIView):
serializer_class = serializers.CountrySerializer 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): class RegionDestroyView(RegionViewMixin, generics.DestroyAPIView):
"""Destroy view for model Country""" """Destroy view for model Country"""
serializer_class = serializers.CountrySerializer serializer_class = serializers.CountrySerializer

View File

@ -40,3 +40,8 @@ class CarouselAdmin(admin.ModelAdmin):
@admin.register(models.PageType) @admin.register(models.PageType)
class PageTypeAdmin(admin.ModelAdmin): class PageTypeAdmin(admin.ModelAdmin):
"""PageType admin.""" """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 PUBLISHED = 1
STATE_CHOICES = ( STATE_CHOICES = (
(WAITING,'waiting'), (WAITING, 'waiting'),
(PUBLISHED, 'published') (PUBLISHED, 'published')
) )

View File

@ -38,7 +38,7 @@ class SiteFeatureSerializer(serializers.ModelSerializer):
'route', 'route',
'source', 'source',
'nested', 'nested',
) )
class CurrencySerializer(ProjectModelSerializer): class CurrencySerializer(ProjectModelSerializer):
@ -145,6 +145,19 @@ class AwardSerializer(AwardBaseSerializer):
fields = AwardBaseSerializer.Meta.fields + ['award_type', ] 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): class CarouselListSerializer(serializers.ModelSerializer):
"""Serializer for retrieving list of carousel items.""" """Serializer for retrieving list of carousel items."""
@ -186,7 +199,11 @@ class PageBaseSerializer(serializers.ModelSerializer):
'image_url', 'image_url',
'width', 'width',
'height', 'height',
'advertisement',
] ]
extra_kwargs = {
'establishment': {'write_only': True}
}
class PageTypeBaseSerializer(serializers.ModelSerializer): class PageTypeBaseSerializer(serializers.ModelSerializer):
@ -198,4 +215,4 @@ class PageTypeBaseSerializer(serializers.ModelSerializer):
fields = [ fields = [
'id', 'id',
'name', '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" 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) @admin.register(models.News)
class NewsAdmin(BaseModelAdminMixin, admin.ModelAdmin): class NewsAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""News admin.""" """News admin."""
raw_id_fields = ('address',) raw_id_fields = ('address',)
actions = [send_email_action] actions = [send_email_action]
raw_id_fields = ('news_type', 'address', 'country') raw_id_fields = ('news_type', 'address', 'country')
inlines = [NewsGalleryInline, ]
@admin.register(models.NewsGallery) @admin.register(models.NewsGallery)

View File

@ -16,6 +16,9 @@ class NewsListFilterSet(filters.FilterSet):
), ),
method='by_tag_group' 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: class Meta:
"""Meta class""" """Meta class"""
@ -24,8 +27,18 @@ class NewsListFilterSet(filters.FilterSet):
'title', 'title',
'is_highlighted', 'is_highlighted',
'tag_group', '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): def by_tag_group(self, queryset, name, value):
if value == models.News.RECIPES_TAG_VALUE: if value == models.News.RECIPES_TAG_VALUE:
queryset = queryset.recipe_news() queryset = queryset.recipe_news()
@ -39,3 +52,9 @@ class NewsListFilterSet(filters.FilterSet):
return queryset.filter(**filters) return queryset.filter(**filters)
else: else:
return queryset 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 rest_framework.reverse import reverse
from rating.models import Rating, ViewCount 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 utils.querysets import TranslationQuerysetMixin
from django.conf import settings
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin): class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
@ -86,6 +88,10 @@ class NewsQuerySet(TranslationQuerysetMixin):
"""Returns news with tag 'cook' qs.""" """Returns news with tag 'cook' qs."""
return self.filter(tags__value=News.RECIPES_TAG_VALUE) 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): def published(self):
"""Return only published news""" """Return only published news"""
now = timezone.now() now = timezone.now()
@ -120,7 +126,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
) )
class News(BaseAttributes, TranslatedFieldsMixin): class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin):
"""News model.""" """News model."""
STR_FIELD_NAME = 'title' STR_FIELD_NAME = 'title'
@ -151,6 +157,7 @@ class News(BaseAttributes, TranslatedFieldsMixin):
(PUBLISHED_EXCLUSIVE, _('Published exclusive')), (PUBLISHED_EXCLUSIVE, _('Published exclusive')),
) )
INTERNATIONAL_TAG_VALUE = 'international'
RECIPES_TAG_VALUE = 'cook' RECIPES_TAG_VALUE = 'cook'
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) 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 count_value = self.views_count.count
return count_value 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): @property
"""QuerySet for model News""" def crop_main_image(self):
if hasattr(self, 'main_image') and self.main_image:
def main_image(self): image = self.main_image
"""Return objects with flag is_main is True""" model_name = self._meta.model_name.lower()
return self.filter(is_main=True) 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, news = models.ForeignKey(News, null=True,
related_name='news_gallery', related_name='news_gallery',
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -260,10 +302,6 @@ class NewsGallery(models.Model):
related_name='news_gallery', related_name='news_gallery',
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_('gallery')) verbose_name=_('gallery'))
is_main = models.BooleanField(default=False,
verbose_name=_('Is the main image'))
objects = NewsGalleryQuerySet.as_manager()
class Meta: class Meta:
"""NewsGallery meta class.""" """NewsGallery meta class."""

View File

@ -10,7 +10,8 @@ from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
from news import models from news import models
from tag.serializers import TagBaseSerializer from tag.serializers import TagBaseSerializer
from utils import exceptions as utils_exceptions 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): 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): class NewsTypeSerializer(serializers.ModelSerializer):
"""News type serializer.""" """News type serializer."""
@ -135,7 +64,7 @@ class NewsBaseSerializer(ProjectModelSerializer):
title_translated = TranslatedField() title_translated = TranslatedField()
subtitle_translated = TranslatedField() subtitle_translated = TranslatedField()
news_type = NewsTypeSerializer(read_only=True) 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) in_favorites = serializers.BooleanField(allow_null=True)
view_counter = serializers.IntegerField(read_only=True) view_counter = serializers.IntegerField(read_only=True)
@ -170,7 +99,7 @@ class NewsSimilarListSerializer(NewsBaseSerializer):
class NewsListSerializer(NewsBaseSerializer): class NewsListSerializer(NewsBaseSerializer):
"""List serializer for News model.""" """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): class Meta(NewsBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -188,7 +117,7 @@ class NewsDetailSerializer(NewsBaseSerializer):
author = UserBaseSerializer(source='created_by', read_only=True) author = UserBaseSerializer(source='created_by', read_only=True)
state_display = serializers.CharField(source='get_state_display', state_display = serializers.CharField(source='get_state_display',
read_only=True) read_only=True)
gallery = NewsImageSerializer(read_only=True, many=True) gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
class Meta(NewsBaseSerializer.Meta): class Meta(NewsBaseSerializer.Meta):
"""Meta class.""" """Meta class."""

View File

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

View File

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

View File

@ -6,3 +6,4 @@ from partner import models
@admin.register(models.Partner) @admin.register(models.Partner)
class PartnerModelAdmin(admin.ModelAdmin): class PartnerModelAdmin(admin.ModelAdmin):
"""Model admin for Partner model.""" """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.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from establishment.models import Establishment
from utils.models import ImageMixin, ProjectBaseMixin from utils.models import ImageMixin, ProjectBaseMixin
class Partner(ProjectBaseMixin): class Partner(ProjectBaseMixin):
"""Partner model.""" """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')) url = models.URLField(verbose_name=_('Partner URL'))
image = models.URLField(verbose_name=_('Partner image URL'), null=True) 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: class Meta:
verbose_name = _('partner') verbose_name = _('partner')

View File

@ -1,19 +1,32 @@
from django.db.models import Value, IntegerField, F
from pprint import pprint from pprint import pprint
from establishment.models import Establishment
from transfer.models import EstablishmentBacklinks from transfer.models import EstablishmentBacklinks
from transfer.serializers.partner import PartnerSerializer from transfer.serializers.partner import PartnerSerializer
def transfer_partner(): 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), many=True)
serialized_data = PartnerSerializer(data=list(queryset.values()), many=True)
if serialized_data.is_valid(): if serialized_data.is_valid():
serialized_data.save() serialized_data.save()
else: else:
pprint(f"News serializer errors: {serialized_data.errors}") pprint(f"Partner serializer errors: {serialized_data.errors}")
data_types = { data_types = {

View File

@ -4,22 +4,23 @@ from utils.admin import BaseModelAdminMixin
from .models import Product, ProductType, ProductSubType, ProductGallery, Unit from .models import Product, ProductType, ProductSubType, ProductGallery, Unit
class ProductGalleryInline(admin.TabularInline):
"""Product gallery inline."""
model = ProductGallery
extra = 0
@admin.register(Product) @admin.register(Product)
class ProductAdmin(BaseModelAdminMixin, admin.ModelAdmin): class ProductAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""Admin page for model Product.""" """Admin page for model Product."""
search_fields = ('name', ) search_fields = ('name', )
list_filter = ('available', 'product_type') list_filter = ('available', 'product_type')
list_display = ('id', '__str__', 'get_category_display', 'product_type') list_display = ('id', '__str__', 'get_category_display', 'product_type')
inlines = [ProductGalleryInline, ]
raw_id_fields = ('subtypes', 'classifications', 'standards', raw_id_fields = ('subtypes', 'classifications', 'standards',
'tags', 'gallery', 'establishment',) 'tags', 'gallery', 'establishment',)
@admin.register(ProductGallery)
class ProductGalleryAdmin(admin.ModelAdmin):
"""Admin page for model ProductGallery."""
raw_id_fields = ('product', 'image', )
@admin.register(ProductType) @admin.register(ProductType)
class ProductTypeAdmin(admin.ModelAdmin): class ProductTypeAdmin(admin.ModelAdmin):
"""Admin page for model ProductType.""" """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 django.db import connections
from establishment.management.commands.add_position import namedtuplefetchall from establishment.management.commands.add_position import namedtuplefetchall
from tag.models import Tag, TagCategory from tag.models import Tag, TagCategory
from product.models import Product from product.models import Product, ProductType
from tqdm import tqdm from tqdm import tqdm
@ -26,24 +26,47 @@ class Command(BaseCommand):
def add_category_tag(self): def add_category_tag(self):
objects = [] objects = []
for c in tqdm(self.category_sql(), desc='Add category tags'): 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(): if not categories.exists():
objects.append( objects.append(
TagCategory(label={"en-GB": c.category}, TagCategory(label={"en-GB": c.category},
value_type=c.value_type, 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) TagCategory.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Add or get tag category 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): def tag_sql(self):
with connections['legacy'].cursor() as cursor: with connections['legacy'].cursor() as cursor:
cursor.execute(''' cursor.execute('''
select select
DISTINCT DISTINCT
m.id as old_id,
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 trim(CONVERT(v.key_name USING utf8)) as tag_category
FROM product_metadata m FROM product_metadata m
@ -64,24 +87,35 @@ class Command(BaseCommand):
if not tags.exists(): if not tags.exists():
objects.append(Tag(label={"en-GB": t.tag_value}, objects.append(Tag(label={"en-GB": t.tag_value},
category=category, category=category,
value=t.tag_value, 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)
Tag.objects.bulk_create(objects) Tag.objects.bulk_create(objects)
self.stdout.write(self.style.WARNING(f'Add or get tag 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): def product_sql(self):
with connections['legacy'].cursor() as cursor: with connections['legacy'].cursor() as cursor:
cursor.execute(''' cursor.execute('''
select select
DISTINCT DISTINCT
m.id as old_id_tag,
m.product_id, 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 trim(CONVERT(v.key_name USING utf8)) as tag_category
FROM product_metadata m FROM product_metadata m
JOIN product_key_value_metadata v on v.id = m.product_key_value_metadatum_id 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): def add_product_tag(self):
for t in tqdm(self.product_sql(), desc='Add product tag'): 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) product = Product.objects.get(old_id=t.product_id)
for tag in tags: for tag in tags:
if product not in tag.products.all(): if tag not in product.tags.all():
product.tags.add(tag) product.tags.add(tag)
self.stdout.write(self.style.WARNING(f'Add or get tag objects.')) self.stdout.write(self.style.WARNING(f'Add or get tag objects.'))
@ -111,7 +150,10 @@ class Command(BaseCommand):
tag.save() tag.save()
def handle(self, *args, **kwargs): def handle(self, *args, **kwargs):
self.remove_tags_product()
self.remove_tags()
self.add_category_tag() self.add_category_tag()
self.add_type_product_category()
self.add_tag() self.add_tag()
self.check_tag() self.check_tag()
self.add_product_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.utils.translation import gettext_lazy as _
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from utils.models import (BaseAttributes, ProjectBaseMixin, from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
TranslatedFieldsMixin, TJSONField) TranslatedFieldsMixin, TJSONField,
GalleryModelMixin, IntermediateGalleryModelMixin)
class ProductType(TranslatedFieldsMixin, ProjectBaseMixin): class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
@ -30,7 +31,7 @@ class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
use_subtypes = models.BooleanField(_('Use subtypes'), default=True) use_subtypes = models.BooleanField(_('Use subtypes'), default=True)
tag_categories = models.ManyToManyField('tag.TagCategory', tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='product_types', related_name='product_types',
verbose_name=_('Tag')) verbose_name=_('Tag categories'))
class Meta: class Meta:
"""Meta class.""" """Meta class."""
@ -82,7 +83,12 @@ class ProductQuerySet(models.QuerySet):
def with_extended_related(self): def with_extended_related(self):
"""Returns qs with almost all related objects.""" """Returns qs with almost all related objects."""
return self.with_base_related() \ 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') \ 'classifications__classification_type', 'classifications__tags') \
.select_related('wine_region', 'wine_sub_region') .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.""" """Product models."""
EARLIEST_VINTAGE_YEAR = 1700 EARLIEST_VINTAGE_YEAR = 1700
@ -205,6 +211,9 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
null=True, blank=True, default=None, null=True, blank=True, default=None,
validators=[MinValueValidator(EARLIEST_VINTAGE_YEAR), validators=[MinValueValidator(EARLIEST_VINTAGE_YEAR),
MaxValueValidator(LATEST_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') gallery = models.ManyToManyField('gallery.Image', through='ProductGallery')
reviews = generic.GenericRelation(to='review.Review') reviews = generic.GenericRelation(to='review.Review')
comments = generic.GenericRelation(to='comment.Comment') comments = generic.GenericRelation(to='comment.Comment')
@ -222,15 +231,11 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
"""Override str dunder method.""" """Override str dunder method."""
return f'{self.name}' return f'{self.name}'
def clean_fields(self, exclude=None): def delete(self, using=None, keep_parents=False):
super().clean_fields(exclude=exclude) """Overridden delete method"""
if self.product_type.index_name == ProductType.WINE and not self.wine_region: # Delete all related notes
raise ValidationError(_('wine_region field must be specified.')) self.notes.all().delete()
if not self.product_type.index_name == ProductType.WINE and self.wine_region: return super().delete(using, keep_parents)
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.'))
@property @property
def product_type_translated_name(self): def product_type_translated_name(self):
@ -255,33 +260,50 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
return self.tags.filter(category__index_name='bottles-produced') return self.tags.filter(category__index_name='bottles-produced')
@property @property
def main_image(self): def grape_variety(self):
qs = ProductGallery.objects.filter(product=self, is_main=True) 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(): if qs.exists():
return qs.first().image return qs.first()
@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
@property @property
def related_tags(self): def related_tags(self):
return self.tags.exclude( return super().visible_tags.exclude(category__index_name__in=[
category__index_name__in=['sugar-content', 'wine-color', 'bottles-produced', 'sugar-content', 'wine-color', 'bottles-produced',
'serial-number', 'grape-variety']) 'serial-number', 'grape-variety', 'serial_number',
'alcohol_percentage', 'bottle_size',
])
@property @property
def display_name(self): def display_name(self):
name = f'{self.name} ' \ name = f'{self.name} ' \
f'({self.vintage if self.vintage else "BSA"})' 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 name = f'{self.establishment.name} - ' + name
return 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): class OnlineProductManager(ProductManager):
"""Extended manger for OnlineProduct model.""" """Extended manger for OnlineProduct model."""
@ -304,6 +326,26 @@ class OnlineProduct(Product):
verbose_name_plural = _('Online products') 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): class Unit(models.Model):
"""Product unit model.""" """Product unit model."""
name = models.CharField(max_length=255, name = models.CharField(max_length=255,
@ -353,15 +395,8 @@ class ProductStandard(models.Model):
verbose_name = _('wine standard') verbose_name = _('wine standard')
class ProductGalleryQuerySet(models.QuerySet): class ProductGallery(IntermediateGalleryModelMixin):
"""QuerySet for model Product""" """Gallery 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):
product = models.ForeignKey(Product, null=True, product = models.ForeignKey(Product, null=True,
related_name='product_gallery', related_name='product_gallery',
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -369,11 +404,7 @@ class ProductGallery(models.Model):
image = models.ForeignKey('gallery.Image', null=True, image = models.ForeignKey('gallery.Image', null=True,
related_name='product_gallery', related_name='product_gallery',
on_delete=models.CASCADE, on_delete=models.CASCADE,
verbose_name=_('gallery')) verbose_name=_('image'))
is_main = models.BooleanField(default=False,
verbose_name=_('Is the main image'))
objects = ProductGalleryQuerySet.as_manager()
class Meta: class Meta:
"""ProductGallery meta class.""" """ProductGallery meta class."""
@ -439,7 +470,7 @@ class ProductNote(ProjectBaseMixin):
old_id = models.PositiveIntegerField(null=True, blank=True) old_id = models.PositiveIntegerField(null=True, blank=True)
text = models.TextField(verbose_name=_('text')) text = models.TextField(verbose_name=_('text'))
product = models.ForeignKey(Product, on_delete=models.PROTECT, product = models.ForeignKey(Product, on_delete=models.PROTECT,
related_name='product_notes', related_name='notes',
verbose_name=_('product')) verbose_name=_('product'))
user = models.ForeignKey('account.User', on_delete=models.PROTECT, user = models.ForeignKey('account.User', on_delete=models.PROTECT,
null=True, null=True,

View File

@ -7,6 +7,7 @@ from product import models
from product.serializers import ProductDetailSerializer, ProductTypeBaseSerializer, \ from product.serializers import ProductDetailSerializer, ProductTypeBaseSerializer, \
ProductSubTypeBaseSerializer ProductSubTypeBaseSerializer
from tag.models import TagCategory from tag.models import TagCategory
from account.serializers.common import UserShortSerializer
class ProductBackOfficeGallerySerializer(serializers.ModelSerializer): class ProductBackOfficeGallerySerializer(serializers.ModelSerializer):
@ -127,3 +128,55 @@ class ProductSubTypeBackOfficeDetailSerializer(ProductSubTypeBaseSerializer):
'name', 'name',
'index_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.models import Comment
from comment.serializers import CommentSerializer from comment.serializers import CommentSerializer
from establishment.serializers import EstablishmentShortSerializer, EstablishmentProductSerializer from establishment.serializers import EstablishmentShortSerializer, EstablishmentProductSerializer, EstablishmentProductShortSerializer
from gallery.models import Image from gallery.models import Image
from product import models from product import models
from review.serializers import ReviewShortSerializer from review.serializers import ReviewShortSerializer
from utils import exceptions as utils_exceptions 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 main.serializers import AwardSerializer
from location.serializers import WineRegionBaseSerializer, WineSubRegionBaseSerializer from location.serializers import WineRegionBaseSerializer, WineSubRegionBaseSerializer
from tag.serializers import TagBaseSerializer, TagCategoryShortSerializer from tag.serializers import TagBaseSerializer, TagCategoryProductSerializer
class ProductTagSerializer(TagBaseSerializer): class ProductTagSerializer(TagBaseSerializer):
"""Serializer for model Tag.""" """Serializer for model Tag."""
category = TagCategoryShortSerializer(read_only=True) category = TagCategoryProductSerializer(read_only=True)
class Meta(TagBaseSerializer.Meta): class Meta(TagBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -88,12 +88,11 @@ class ProductBaseSerializer(serializers.ModelSerializer):
name = serializers.CharField(source='display_name', read_only=True) name = serializers.CharField(source='display_name', read_only=True)
product_type = ProductTypeBaseSerializer(read_only=True) product_type = ProductTypeBaseSerializer(read_only=True)
subtypes = ProductSubTypeBaseSerializer(many=True, 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) tags = ProductTagSerializer(source='related_tags', many=True, read_only=True)
wine_region = WineRegionBaseSerializer(read_only=True) wine_region = WineRegionBaseSerializer(read_only=True)
wine_colors = TagBaseSerializer(many=True, read_only=True) wine_colors = TagBaseSerializer(many=True, read_only=True)
preview_image_url = serializers.URLField(source='preview_main_image_url', preview_image_url = serializers.URLField(allow_null=True,
allow_null=True,
read_only=True) read_only=True)
in_favorites = serializers.BooleanField(allow_null=True) in_favorites = serializers.BooleanField(allow_null=True)
@ -120,6 +119,7 @@ class ProductBaseSerializer(serializers.ModelSerializer):
class ProductDetailSerializer(ProductBaseSerializer): class ProductDetailSerializer(ProductBaseSerializer):
"""Product detail serializer.""" """Product detail serializer."""
description_translated = TranslatedField() description_translated = TranslatedField()
establishment_detail = EstablishmentShortSerializer(source='establishment', read_only=True)
review = ReviewShortSerializer(source='last_published_review', read_only=True) review = ReviewShortSerializer(source='last_published_review', read_only=True)
awards = AwardSerializer(many=True, read_only=True) awards = AwardSerializer(many=True, read_only=True)
classifications = ProductClassificationBaseSerializer(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) wine_sub_region = WineSubRegionBaseSerializer(read_only=True)
bottles_produced = TagBaseSerializer(many=True, read_only=True) bottles_produced = TagBaseSerializer(many=True, read_only=True)
sugar_contents = TagBaseSerializer(many=True, read_only=True) sugar_contents = TagBaseSerializer(many=True, read_only=True)
image_url = serializers.ImageField(source='main_image_url', grape_variety = TagBaseSerializer(many=True, read_only=True)
allow_null=True, bottle_sizes = TagBaseSerializer(many=True, read_only=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): class Meta(ProductBaseSerializer.Meta):
fields = ProductBaseSerializer.Meta.fields + [ fields = ProductBaseSerializer.Meta.fields + [
@ -142,6 +145,11 @@ class ProductDetailSerializer(ProductBaseSerializer):
'bottles_produced', 'bottles_produced',
'sugar_contents', 'sugar_contents',
'image_url', 'image_url',
'new_image',
'grape_variety',
'average_price',
'bottle_sizes',
'alcohol_percentage',
] ]
@ -175,78 +183,6 @@ class ProductFavoritesCreateSerializer(FavoritesCreateSerializer):
return super().create(validated_data) 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): class ProductCommentCreateSerializer(CommentSerializer):
"""Create comment serializer""" """Create comment serializer"""
mark = serializers.IntegerField() mark = serializers.IntegerField()

View File

@ -6,6 +6,8 @@ from product import views
urlpatterns = [ urlpatterns = [
path('', views.ProductListCreateBackOfficeView.as_view(), name='list-create'), path('', views.ProductListCreateBackOfficeView.as_view(), name='list-create'),
path('<int:pk>/', views.ProductDetailBackOfficeView.as_view(), name='rud'), 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(), path('<int:pk>/gallery/', views.ProductBackOfficeGalleryListView.as_view(),
name='gallery-list'), name='gallery-list'),
path('<int:pk>/gallery/<int:image_id>/', views.ProductBackOfficeGalleryCreateDestroyView.as_view(), 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 import serializers, models
from product.views import ProductBaseView from product.views import ProductBaseView
from utils.serializers import ImageBaseSerializer
from utils.views import CreateDestroyGalleryViewMixin from utils.views import CreateDestroyGalleryViewMixin
class ProductBackOfficeMixinView(ProductBaseView): class ProductBackOfficeMixinView(ProductBaseView):
"""Product back-office mixin view.""" """Product back-office mixin view."""
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated, )
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method.""" """Override get_queryset method."""
@ -56,8 +57,8 @@ class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView,
""" """
product_qs = self.filter_queryset(self.get_queryset()) product_qs = self.filter_queryset(self.get_queryset())
product = get_object_or_404(product_qs, pk=self.kwargs['pk']) product = get_object_or_404(product_qs, pk=self.kwargs.get('pk'))
gallery = get_object_or_404(product.product_gallery, image_id=self.kwargs['image_id']) gallery = get_object_or_404(product.product_gallery, image_id=self.kwargs.get('image_id'))
# May raise a permission denied # May raise a permission denied
self.check_object_permissions(self.request, gallery) self.check_object_permissions(self.request, gallery)
@ -65,14 +66,16 @@ class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView,
return gallery return gallery
class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.ListAPIView): class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView,
generics.ListAPIView):
"""Resource for returning gallery for product for back-office users.""" """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): def get_object(self):
"""Override get_object method.""" """Override get_object method."""
qs = super(ProductBackOfficeGalleryListView, self).get_queryset() 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 # May raise a permission denied
self.check_object_permissions(self.request, product) self.check_object_permissions(self.request, product)
@ -81,10 +84,11 @@ class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.List
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method.""" """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.""" """Product back-office R/U/D view."""
serializer_class = serializers.ProductBackOfficeDetailSerializer serializer_class = serializers.ProductBackOfficeDetailSerializer
@ -131,3 +135,48 @@ class ProductSubTypeRUDBackOfficeView(BackOfficeListCreateMixin,
generics.RetrieveUpdateDestroyAPIView): generics.RetrieveUpdateDestroyAPIView):
"""Product sub type back-office retrieve-update-destroy view.""" """Product sub type back-office retrieve-update-destroy view."""
serializer_class = serializers.ProductSubTypeBackOfficeDetailSerializer 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() \ return Product.objects.published() \
.with_base_related() \ .with_base_related() \
.annotate_in_favorites(self.request.user) \ .annotate_in_favorites(self.request.user) \
.by_country_code(self.request.country_code) \
.order_by('-created') .order_by('-created')
@ -26,6 +25,11 @@ class ProductListView(ProductBaseView, generics.ListAPIView):
serializer_class = serializers.ProductBaseSerializer serializer_class = serializers.ProductBaseSerializer
filter_class = filters.ProductFilterSet 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): class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
"""Detail view fro model Product.""" """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.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from utils.models import BaseAttributes, TranslatedFieldsMixin, ProjectBaseMixin from utils.models import (BaseAttributes, TranslatedFieldsMixin,
from utils.models import TJSONField ProjectBaseMixin, GalleryModelMixin,
TJSONField, IntermediateGalleryModelMixin)
class ReviewQuerySet(models.QuerySet): class ReviewQuerySet(models.QuerySet):
@ -92,7 +93,7 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
verbose_name_plural = _('Reviews') verbose_name_plural = _('Reviews')
class Inquiries(ProjectBaseMixin): class Inquiries(GalleryModelMixin, ProjectBaseMixin):
NONE = 0 NONE = 0
DINER = 1 DINER = 1
LUNCH = 2 LUNCH = 2
@ -145,15 +146,7 @@ class GridItems(ProjectBaseMixin):
return f'inquiry: {self.inquiry.id}, grid id: {self.id}' return f'inquiry: {self.inquiry.id}, grid id: {self.id}'
class InquiriesGalleryQuerySet(models.QuerySet): class InquiriesGallery(IntermediateGalleryModelMixin):
"""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):
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None) old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
inquiry = models.ForeignKey( inquiry = models.ForeignKey(
Inquiries, Inquiries,
@ -167,11 +160,8 @@ class InquiriesGallery(models.Model):
null=True, null=True,
related_name='inquiries_gallery', related_name='inquiries_gallery',
on_delete=models.CASCADE, 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: class Meta:
verbose_name = _('inquiry gallery') verbose_name = _('inquiry gallery')

View File

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

View File

@ -19,6 +19,7 @@ class BaseTestCase(APITestCase):
username=self.username, username=self.username,
email=self.email, email=self.email,
password=self.password, password=self.password,
is_staff=True,
) )
tokens = User.create_jwt_tokens(self.user) 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): class InquiriesTestCase(BaseTestCase):
def setUp(self): def setUp(self):
super().setUp() super().setUp()

View File

@ -2,10 +2,10 @@ from pprint import pprint
from django.db.models import Q from django.db.models import Q
from product.models import Product
from account.models import User from account.models import User
from account.transfer_data import STOP_LIST from account.transfer_data import STOP_LIST
from establishment.models import Establishment from establishment.models import Establishment
from product.models import Product
from review.models import Inquiries as NewInquiries, Review from review.models import Inquiries as NewInquiries, Review
from transfer.models import Reviews, ReviewTexts, Inquiries, GridItems, InquiryPhotos from transfer.models import Reviews, ReviewTexts, Inquiries, GridItems, InquiryPhotos
from transfer.serializers.grid import GridItemsSerializer from transfer.serializers.grid import GridItemsSerializer
@ -34,11 +34,11 @@ def transfer_languages():
def transfer_reviews(): def transfer_reviews():
establishments = Establishment.objects.filter(old_id__isnull=False).values_list('old_id', flat=True) 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), establishment_id__in=list(establishments),
).values('id', 'reviewer_id', 'aasm_state', 'created_at', 'establishment_id', 'mark', 'vintage') ).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(): if serialized_data.is_valid():
serialized_data.save() serialized_data.save()
else: else:
@ -48,17 +48,23 @@ def transfer_reviews():
def transfer_text_review(): def transfer_text_review():
reviews = Review.objects.filter(old_id__isnull=False).values_list('old_id', flat=True) reviews = Review.objects.filter(old_id__isnull=False).values_list('old_id', flat=True)
queryset = ReviewTexts.objects.filter( queryset = ReviewTexts.objects.filter(
review_id__in=list(reviews), review_id__in=list(reviews)
).exclude( ).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') ).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(): if serialized_data.is_valid():
serialized_data.save() serialized_data.save()
else: else:
pprint(f"ReviewTextSerializer serializer errors: {serialized_data.errors}") pprint(f"ReviewTextSerializer serializer errors: {serialized_data.errors}")
def make_en_text_review():
for review in Review.objects.filter(old_id__isnull=False): for review in Review.objects.filter(old_id__isnull=False):
text = review.text text = review.text
if text and 'en-GB' not in text: if text and 'en-GB' not in text:
@ -106,7 +112,6 @@ def transfer_inquiry_photos():
def transfer_product_reviews(): def transfer_product_reviews():
products = Product.objects.filter( products = Product.objects.filter(
old_id__isnull=False).values_list('old_id', flat=True) old_id__isnull=False).values_list('old_id', flat=True)
@ -130,6 +135,7 @@ data_types = {
# transfer_languages, # transfer_languages,
transfer_reviews, transfer_reviews,
transfer_text_review, transfer_text_review,
make_en_text_review,
], ],
'inquiries': [ 'inquiries': [
transfer_inquiries, transfer_inquiries,

View File

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

View File

@ -48,6 +48,34 @@ class EstablishmentDocument(Document):
properties=OBJECT_FIELD_PROPERTIES), properties=OBJECT_FIELD_PROPERTIES),
}, },
multi=True) 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( schedule = fields.ListField(fields.ObjectField(
properties={ properties={
'id': fields.IntegerField(attr='id'), 'id': fields.IntegerField(attr='id'),

View File

@ -34,6 +34,13 @@ class NewsDocument(Document):
'value': fields.KeywordField() 'value': fields.KeywordField()
}, },
multi=True) 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: class Django:

View File

@ -28,12 +28,43 @@ class ProductDocument(Document):
}, },
multi=True multi=True
) )
preview_image_url = fields.KeywordField(attr='preview_image_url')
establishment = fields.ObjectField( establishment = fields.ObjectField(
properties={ properties={
'id': fields.IntegerField(), 'id': fields.IntegerField(),
'name': fields.KeywordField(), 'name': fields.KeywordField(),
'index_name': fields.KeywordField(),
'slug': 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( wine_colors = fields.ObjectField(
@ -44,6 +75,14 @@ class ProductDocument(Document):
}, },
multi=True, 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={ wine_region = fields.ObjectField(properties={
'id': fields.IntegerField(), 'id': fields.IntegerField(),
'name': fields.KeywordField(), 'name': fields.KeywordField(),
@ -56,7 +95,10 @@ class ProductDocument(Document):
# 'coordinates': fields.GeoPointField(), # 'coordinates': fields.GeoPointField(),
'description': fields.ObjectField(attr='description_indexing', properties=OBJECT_FIELD_PROPERTIES), '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 classifications = fields.ObjectField( # TODO
properties={ properties={
'classification_type': fields.ObjectField(properties={}), 'classification_type': fields.ObjectField(properties={}),
@ -95,13 +137,23 @@ class ProductDocument(Document):
}, },
multi=True 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: class Django:
model = models.Product model = models.Product
fields = ( fields = (
'id', 'id',
'category', 'category',
'name',
'available', 'available',
'public_mark', 'public_mark',
'slug', 'slug',
@ -109,6 +161,7 @@ class ProductDocument(Document):
'state', 'state',
'old_unique_key', 'old_unique_key',
'vintage', 'vintage',
'average_price',
) )
related_models = [models.ProductType] related_models = [models.ProductType]

View File

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

View File

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

View File

@ -28,8 +28,10 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
] ]
search_fields = { search_fields = {
'title': {'fuzziness': 'auto:2,5'}, 'title': {'fuzziness': 'auto:2,5',
'subtitle': {'fuzziness': 'auto:2,5'}, 'boost': 3},
'subtitle': {'fuzziness': 'auto:2,5',
'boost': 2},
'description': {'fuzziness': 'auto:2,5'}, 'description': {'fuzziness': 'auto:2,5'},
} }
translated_search_fields = ( translated_search_fields = (
@ -86,11 +88,9 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
search_fields = { search_fields = {
'name': {'fuzziness': 'auto:2,5', 'name': {'fuzziness': 'auto:2,5',
'boost': '2'}, 'boost': 4},
'transliterated_name': {'fuzziness': 'auto:2,5', 'transliterated_name': {'fuzziness': 'auto:2,5',
'boost': '2'}, 'boost': 3},
'index_name': {'fuzziness': 'auto:2,5',
'boost': '2'},
'description': {'fuzziness': 'auto:2,5'}, 'description': {'fuzziness': 'auto:2,5'},
} }
translated_search_fields = ( translated_search_fields = (
@ -124,6 +124,20 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet):
constants.LOOKUP_QUERY_IN, 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': { 'country_id': {
'field': 'address.city.country.id' 'field': 'address.city.country.id'
}, },
@ -197,32 +211,27 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
"""Product document ViewSet.""" """Product document ViewSet."""
document = ProductDocument document = ProductDocument
lookup_field = 'slug'
pagination_class = ProjectMobilePagination pagination_class = ProjectMobilePagination
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.ProductDocumentSerializer 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 = [ filter_backends = [
FilteringFilterBackend, FilteringFilterBackend,
filters.CustomSearchFilterBackend, filters.CustomSearchFilterBackend,
GeoSpatialFilteringFilterBackend,
DefaultOrderingFilterBackend,
] ]
search_fields = { search_fields = {
'name': {'fuzziness': 'auto:2,5', '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', 'transliterated_name': {'fuzziness': 'auto:2,5',
'boost': '2'}, 'boost': 3},
'index_name': {'fuzziness': 'auto:2,5',
'boost': '2'},
'description': {'fuzziness': 'auto:2,5'}, 'description': {'fuzziness': 'auto:2,5'},
} }
translated_search_fields = ( translated_search_fields = (
'description', 'description',
) )
@ -231,10 +240,34 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
'slug': 'slug', 'slug': 'slug',
'tags_id': { 'tags_id': {
'field': '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': { 'wine_colors_id': {
'field': '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': [ 'lookups': [
constants.LOOKUP_QUERY_IN, constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE, constants.LOOKUP_QUERY_EXCLUDE,
@ -246,7 +279,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
'for_establishment': { 'for_establishment': {
'field': 'establishment.slug', 'field': 'establishment.slug',
}, },
'type': { 'product_type': {
'field': 'product_type.index_name', 'field': 'product_type.index_name',
}, },
'subtype': { 'subtype': {
@ -254,8 +287,6 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
'lookups': [ 'lookups': [
constants.LOOKUP_QUERY_IN, constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE, 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 establishment.models import EstablishmentType
from django.conf import settings from django.conf import settings
from tag import models from tag import models
from product import models as product_models
class TagsBaseFilterSet(filters.FilterSet): class TagsBaseFilterSet(filters.FilterSet):
@ -31,27 +32,49 @@ class TagCategoryFilterSet(TagsBaseFilterSet):
"""TagCategory filterset.""" """TagCategory filterset."""
establishment_type = filters.CharFilter(method='by_establishment_type') establishment_type = filters.CharFilter(method='by_establishment_type')
product_type = filters.CharFilter(method='by_product_type')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
model = models.TagCategory model = models.TagCategory
fields = ('type', 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 # todo: filter by establishment type
def by_establishment_type(self, queryset, name, value): 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): class TagsFilterSet(TagsBaseFilterSet):
"""Chosen tags filterset.""" """Chosen tags filterset."""
establishment_type = filters.CharFilter(method='by_establishment_type')
class Meta: class Meta:
"""Meta class.""" """Meta class."""
model = models.Tag 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 # TMP TODO remove it later
# Временный хардкод для демонстрации 4 ноября, потом удалить! # Временный хардкод для демонстрации 4 ноября, потом удалить!
@ -66,3 +89,4 @@ class TagsFilterSet(TagsBaseFilterSet):
queryset = queryset.for_establishments().filter(value__in=settings.ESTABLISHMENT_CHOSEN_TAGS).distinct( queryset = queryset.for_establishments().filter(value__in=settings.ESTABLISHMENT_CHOSEN_TAGS).distinct(
'value') 'value')
return queryset return queryset

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