Merge branch 'develop' into panels

# Conflicts:
#	apps/main/admin.py
This commit is contained in:
Dmitriy Kuzmenko 2019-12-19 19:09:49 +03:00
commit 847412bb2f
66 changed files with 1296 additions and 253 deletions

View File

@ -1,4 +1,5 @@
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from drf_yasg.utils import swagger_auto_schema
from rest_framework import generics, permissions, status, serializers from rest_framework import generics, permissions, status, serializers
from rest_framework.response import Response from rest_framework.response import Response
@ -96,6 +97,13 @@ class CreatePendingBooking(generics.CreateAPIView):
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = PendingBookingSerializer serializer_class = PendingBookingSerializer
@swagger_auto_schema(operation_description="Request body params\n\n"
"IN GUESTONLINE (type:G): {"
"'restaurant_id', 'booking_time', "
"'booking_date', 'booked_persons_number'}\n"
"IN LASTABLE (type:L): {'booking_time', "
"'booked_persons_number', 'offer_id' (Req), "
"'email', 'phone', 'first_name', 'last_name'}")
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
data = request.data.copy() data = request.data.copy()
if data.get('type') == Booking.LASTABLE and data.get("offer_id") is None: if data.get('type') == Booking.LASTABLE and data.get("offer_id") is None:
@ -135,6 +143,10 @@ class UpdatePendingBooking(generics.UpdateAPIView):
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = UpdateBookingSerializer serializer_class = UpdateBookingSerializer
@swagger_auto_schema(operation_description="Request body params\n\n"
"Required: 'email', 'phone', 'last_name', "
"'first_name', 'country_code', 'pending_booking_id',"
"Not req: 'note'")
def patch(self, request, *args, **kwargs): def patch(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
data = request.data.copy() data = request.data.copy()

View File

@ -0,0 +1,34 @@
# Generated by Django 2.2.7 on 2019-12-13 08:59
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('gallery', '0006_merge_20191027_1758'),
('collection', '0023_advertorial'),
]
operations = [
migrations.CreateModel(
name='AdvertorialGallery',
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')),
('advertorial', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='advertorial_gallery', to='collection.Advertorial', verbose_name='advertorial')),
('image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='advertorial_gallery', to='gallery.Image', verbose_name='image')),
],
options={
'verbose_name': 'advertorial gallery',
'verbose_name_plural': 'advertorial galleries',
'unique_together': {('advertorial', 'image')},
},
),
migrations.AddField(
model_name='advertorial',
name='gallery',
field=models.ManyToManyField(through='collection.AdvertorialGallery', to='gallery.Image'),
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 2.2.7 on 2019-12-15 21:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('collection', '0023_advertorial'),
]
operations = [
migrations.AddField(
model_name='collection',
name='rank',
field=models.IntegerField(default=None, null=True),
),
migrations.AlterField(
model_name='collection',
name='start',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='start'),
),
migrations.RemoveField(
model_name='collection',
name='description',
)
]

View File

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

View File

@ -0,0 +1,14 @@
# Generated by Django 2.2.7 on 2019-12-17 11:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('collection', '0024_auto_20191213_0859'),
('collection', '0025_collection_description'),
]
operations = [
]

View File

@ -0,0 +1,27 @@
# Generated by Django 2.2.7 on 2019-12-18 07:53
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('gallery', '0007_auto_20191211_1528'),
('collection', '0026_merge_20191217_1151'),
]
operations = [
migrations.RemoveField(
model_name='advertorial',
name='gallery',
),
migrations.AddField(
model_name='guideelement',
name='label_photo',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='gallery.Image', verbose_name='label photo'),
),
migrations.DeleteModel(
name='AdvertorialGallery',
),
]

View File

@ -6,10 +6,12 @@ from django.core.validators import MaxValueValidator, MinValueValidator
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 ProjectBaseMixin, URLImageMixin from utils.models import (
from utils.models import TJSONField ProjectBaseMixin, TJSONField, TranslatedFieldsMixin,
from utils.models import TranslatedFieldsMixin URLImageMixin,
)
from utils.querysets import RelatedObjectsCountMixin from utils.querysets import RelatedObjectsCountMixin
from utils.models import IntermediateGalleryModelMixin, GalleryModelMixin
# Mixins # Mixins
@ -24,7 +26,8 @@ class CollectionNameMixin(models.Model):
class CollectionDateMixin(models.Model): class CollectionDateMixin(models.Model):
"""CollectionDate mixin""" """CollectionDate mixin"""
start = models.DateTimeField(_('start')) start = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('start'))
end = models.DateTimeField(blank=True, null=True, default=None, end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('end')) verbose_name=_('end'))
@ -80,6 +83,8 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
verbose_name=_('Collection slug'), editable=True, null=True) verbose_name=_('Collection slug'), editable=True, null=True)
old_id = models.IntegerField(null=True, blank=True) old_id = models.IntegerField(null=True, blank=True)
rank = models.IntegerField(null=True, default=None)
objects = CollectionQuerySet.as_manager() objects = CollectionQuerySet.as_manager()
class Meta: class Meta:
@ -108,20 +113,32 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
@property @property
def related_object_names(self) -> list: def related_object_names(self) -> list:
"""Return related object names.""" """Return related object names."""
raw_object_names = [] raw_objects = []
for related_object in [related_object.name for related_object in self._related_objects]: for related_object in [related_object.name for related_object in self._related_objects]:
instances = getattr(self, f'{related_object}') instances = getattr(self, f'{related_object}')
if instances.exists(): if instances.exists():
for instance in instances.all(): for instance in instances.all():
raw_object_names.append(instance.slug if hasattr(instance, 'slug') else None) raw_object = (instance.id, instance.slug) if hasattr(instance, 'slug') else (
instance.id, None
)
raw_objects.append(raw_object)
# parse slugs # parse slugs
object_names = [] related_objects = []
object_names = set()
re_pattern = r'[\w]+' re_pattern = r'[\w]+'
for raw_name in raw_object_names: for object_id, raw_name, in raw_objects:
result = re.findall(re_pattern, raw_name) result = re.findall(re_pattern, raw_name)
if result: object_names.append(' '.join(result).capitalize()) if result:
return set(object_names) name = ' '.join(result).capitalize()
if name not in object_names:
related_objects.append({
'id': object_id,
'name': name
})
object_names.add(name)
return related_objects
class GuideTypeQuerySet(models.QuerySet): class GuideTypeQuerySet(models.QuerySet):
@ -194,6 +211,17 @@ class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
"""String method.""" """String method."""
return f'{self.name}' return f'{self.name}'
@property
def entities(self):
"""Return entities and its count."""
# todo: to work
return {
'Current': 0,
'Initial': 0,
'Restaurants': 0,
'Shops': 0,
}
class AdvertorialQuerySet(models.QuerySet): class AdvertorialQuerySet(models.QuerySet):
"""QuerySet for model Advertorial.""" """QuerySet for model Advertorial."""
@ -364,6 +392,9 @@ class GuideElement(ProjectBaseMixin, MPTTModel):
parent = TreeForeignKey('self', on_delete=models.CASCADE, parent = TreeForeignKey('self', on_delete=models.CASCADE,
null=True, blank=True, null=True, blank=True,
related_name='children') related_name='children')
label_photo = models.ForeignKey('gallery.Image', on_delete=models.SET_NULL,
null=True, blank=True, default=None,
verbose_name=_('label photo'))
old_id = models.PositiveIntegerField(blank=True, null=True, default=None, old_id = models.PositiveIntegerField(blank=True, null=True, default=None,
verbose_name=_('old id')) verbose_name=_('old id'))

View File

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

View File

@ -7,7 +7,8 @@ from location.models import Country
from location.serializers import CountrySimpleSerializer from location.serializers import CountrySimpleSerializer
from product.models import Product from product.models import Product
from utils.exceptions import ( from utils.exceptions import (
BindingObjectNotFound, RemovedBindingObjectNotFound, ObjectAlreadyAdded BindingObjectNotFound, ObjectAlreadyAdded,
RemovedBindingObjectNotFound,
) )
@ -33,13 +34,14 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer):
'on_top', 'on_top',
'country', 'country',
'country_id', 'country_id',
'block_size', # 'block_size',
'description', 'description',
'slug', 'slug',
'start', # 'start',
'end', # 'end',
'count_related_objects', 'count_related_objects',
'related_object_names', 'related_object_names',
'rank',
] ]
@ -68,15 +70,15 @@ class CollectionBindObjectSerializer(serializers.Serializer):
attrs['collection'] = collection attrs['collection'] = collection
if obj_type == self.ESTABLISHMENT: if obj_type == self.ESTABLISHMENT:
establishment = Establishment.objects.filter(pk=obj_id).\ establishment = Establishment.objects.filter(pk=obj_id). \
first() first()
if not establishment: if not establishment:
raise BindingObjectNotFound() raise BindingObjectNotFound()
if request.method == 'POST' and collection.establishments.\ if request.method == 'POST' and collection.establishments. \
filter(pk=establishment.pk).exists(): filter(pk=establishment.pk).exists():
raise ObjectAlreadyAdded() raise ObjectAlreadyAdded()
if request.method == 'DELETE' and not collection.\ if request.method == 'DELETE' and not collection. \
establishments.filter(pk=establishment.pk).\ establishments.filter(pk=establishment.pk). \
exists(): exists():
raise RemovedBindingObjectNotFound() raise RemovedBindingObjectNotFound()
attrs['related_object'] = establishment attrs['related_object'] = establishment
@ -84,10 +86,10 @@ class CollectionBindObjectSerializer(serializers.Serializer):
product = Product.objects.filter(pk=obj_id).first() product = Product.objects.filter(pk=obj_id).first()
if not product: if not product:
raise BindingObjectNotFound() raise BindingObjectNotFound()
if request.method == 'POST' and collection.products.\ if request.method == 'POST' and collection.products. \
filter(pk=product.pk).exists(): filter(pk=product.pk).exists():
raise ObjectAlreadyAdded() raise ObjectAlreadyAdded()
if request.method == 'DELETE' and not collection.products.\ if request.method == 'DELETE' and not collection.products. \
filter(pk=product.pk).exists(): filter(pk=product.pk).exists():
raise RemovedBindingObjectNotFound() raise RemovedBindingObjectNotFound()
attrs['related_object'] = product attrs['related_object'] = product

View File

@ -2,6 +2,7 @@ from rest_framework import serializers
from collection import models from collection import models
from location import models as location_models from location import models as location_models
from main.serializers import SiteShortSerializer
from utils.serializers import TranslatedField from utils.serializers import TranslatedField
@ -47,8 +48,28 @@ class CollectionSerializer(CollectionBaseSerializer):
] ]
class GuideSerializer(serializers.ModelSerializer): class GuideTypeBaseSerializer(serializers.ModelSerializer):
"""GuideType serializer."""
class Meta:
"""Meta class."""
model = models.GuideType
fields = [
'id',
'name',
]
class GuideBaseSerializer(serializers.ModelSerializer):
"""Guide serializer""" """Guide serializer"""
state_display = serializers.CharField(source='get_state_display',
read_only=True)
guide_type_detail = GuideTypeBaseSerializer(read_only=True,
source='guide_type')
site_detail = SiteShortSerializer(read_only=True,
source='site')
entities = serializers.DictField(read_only=True)
class Meta: class Meta:
model = models.Guide model = models.Guide
fields = [ fields = [
@ -56,4 +77,60 @@ class GuideSerializer(serializers.ModelSerializer):
'name', 'name',
'start', 'start',
'end', 'end',
'vintage',
'slug',
'guide_type',
'guide_type_detail',
'site',
'site_detail',
'state',
'state_display',
'entities',
] ]
extra_kwargs = {
'guide_type': {'write_only': True},
'site': {'write_only': True},
'state': {'write_only': True},
'start': {'required': True},
'slug': {'required': True},
}
class GuideFilterBaseSerializer(serializers.ModelSerializer):
"""GuideFilter serializer"""
class Meta:
"""Meta class."""
model = models.GuideFilter
fields = [
'id',
'establishment_type_json',
'country_json',
'region_json',
'sub_region_json',
'wine_region_json',
'with_mark',
'locale_json',
'max_mark',
'min_mark',
'review_vintage_json',
'review_state_json',
'guide',
]
extra_kwargs = {
'guide': {'write_only': True}
}
@property
def request_kwargs(self):
"""Get url kwargs from request."""
return self.context.get('request').parser_context.get('kwargs')
def get_guide(self):
"""Get guide instance from kwargs."""
return self.request_kwargs.get()
def create(self, validated_data):
"""Overridden create method."""
validated_data['guide'] = self.get_guide(validated_data.pop('guide', None))
return super().create(validated_data)

View File

@ -1,15 +1,19 @@
from pprint import pprint from pprint import pprint
from tqdm import tqdm from tqdm import tqdm
from collection.models import GuideElementSection, GuideElementSectionCategory, \
GuideWineColorSection, GuideElementType, GuideElement, \
Guide, Advertorial
from establishment.models import Establishment from establishment.models import Establishment
from review.models import Review from gallery.models import Image
from location.models import WineRegion, City from location.models import WineRegion, City
from product.models import Product from product.models import Product
from review.models import Review
from transfer.models import Guides, GuideFilters, GuideSections, GuideElements, \ from transfer.models import Guides, GuideFilters, GuideSections, GuideElements, \
GuideAds GuideAds, LabelPhotos
from transfer.serializers.guide import GuideSerializer, GuideFilterSerializer from transfer.serializers.guide import GuideSerializer, GuideFilterSerializer
from collection.models import GuideElementSection, GuideElementSectionCategory, \ from django.db.models import Subquery
GuideWineColorSection, GuideElementType, GuideElement, \
Guide, Advertorial
def transfer_guide(): def transfer_guide():
@ -252,7 +256,7 @@ def transfer_guide_element_advertorials():
qs = GuideElement.objects.filter(old_id=old_id) qs = GuideElement.objects.filter(old_id=old_id)
legacy_qs = GuideElements.objects.exclude(guide__isnull=True) \ legacy_qs = GuideElements.objects.exclude(guide__isnull=True) \
.exclude(guide__title__icontains='test') \ .exclude(guide__title__icontains='test') \
.filter(id=guide_ad_node_id) .filter(id=old_id)
if qs.exists() and legacy_qs.exists(): if qs.exists() and legacy_qs.exists():
return qs.first() return qs.first()
elif legacy_qs.exists() and not qs.exists(): elif legacy_qs.exists() and not qs.exists():
@ -285,6 +289,55 @@ def transfer_guide_element_advertorials():
print(f'COUNT OF CREATED OBJECTS: {len(objects_to_update)}') print(f'COUNT OF CREATED OBJECTS: {len(objects_to_update)}')
def transfer_guide_element_label_photo():
"""Transfer galleries for Guide Advertorial model."""
def get_guide_element(guide_ad):
legacy_guide_element_id = guide_ad.guide_ad_node.id
legacy_guide_element_qs = GuideElements.objects.filter(id=legacy_guide_element_id)
guide_element_qs = GuideElement.objects.filter(old_id=legacy_guide_element_id)
if guide_element_qs.exists() and legacy_guide_element_qs.exists():
return guide_element_qs.first()
else:
raise ValueError(f'Guide element was not transfer correctly - '
f'{legacy_guide_element_id}.')
to_update = []
not_updated = 0
guide_element_label_photos = LabelPhotos.objects.exclude(guide_ad__isnull=True) \
.filter(guide_ad__type='GuideAdLabel') \
.distinct() \
.values_list('guide_ad', 'attachment_suffix_url')
for guide_ad_id, attachment_suffix_url in tqdm(guide_element_label_photos):
legacy_guide_element_ids = Subquery(
GuideElements.objects.exclude(guide__isnull=True)
.exclude(guide__title__icontains='test')
.values_list('id', flat=True)
)
legacy_guide_ad_qs = GuideAds.objects.filter(id=guide_ad_id,
guide_ad_node_id__in=legacy_guide_element_ids)
if legacy_guide_ad_qs.exists():
guide_element = get_guide_element(legacy_guide_ad_qs.first())
if guide_element:
image, _ = Image.objects.get_or_create(image=attachment_suffix_url,
defaults={
'image': attachment_suffix_url,
'orientation': Image.HORIZONTAL,
'title': f'{guide_element.__str__()} '
f'{guide_element.id} - '
f'{attachment_suffix_url}'})
if not guide_element.label_photo:
guide_element.label_photo = image
to_update.append(guide_element)
else:
not_updated += 1
GuideElement.objects.bulk_update(to_update, ['label_photo', ])
print(f'Added label photo to {len(to_update)} objects\n'
f'Objects {not_updated} not updated')
data_types = { data_types = {
'guides': [ 'guides': [
transfer_guide, transfer_guide,
@ -305,7 +358,10 @@ data_types = {
transfer_guide_elements_bulk, transfer_guide_elements_bulk,
], ],
'guide_element_advertorials': [ 'guide_element_advertorials': [
transfer_guide_element_advertorials transfer_guide_element_advertorials,
],
'guide_element_label_photo': [
transfer_guide_element_label_photo,
], ],
'guide_complete': [ 'guide_complete': [
transfer_guide, # transfer guides from Guides transfer_guide, # transfer guides from Guides
@ -315,5 +371,6 @@ data_types = {
transfer_guide_element_type, # partial transfer section types from GuideElements transfer_guide_element_type, # partial transfer section types from GuideElements
transfer_guide_elements_bulk, # transfer result of GuideFilters from GuideElements transfer_guide_elements_bulk, # transfer result of GuideFilters from GuideElements
transfer_guide_element_advertorials, # transfer advertorials that linked to GuideElements transfer_guide_element_advertorials, # transfer advertorials that linked to GuideElements
transfer_guide_element_label_photo, # transfer guide element label photos
] ]
} }

View File

@ -1,10 +1,17 @@
"""Collection common urlpaths.""" """Collection common urlpaths."""
from rest_framework.routers import SimpleRouter from rest_framework.routers import SimpleRouter
from django.urls import path
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 = router.urls router = SimpleRouter()
router.register(r'collections', views.CollectionBackOfficeViewSet)
urlpatterns = [
path('guides/', views.GuideListCreateView.as_view(),
name='guide-list-create'),
path('guides/<int:pk>/filters/', views.GuideFilterCreateView.as_view(),
name='guide-filter-list-create'),
] + router.urls

View File

@ -7,10 +7,7 @@ app_name = 'collection'
urlpatterns = [ urlpatterns = [
path('', views.CollectionHomePageView.as_view(), name='list'), path('', views.CollectionHomePageView.as_view(), name='list'),
path('<slug:slug>/', views.CollectionDetailView.as_view(), name='detail'), path('slug/<slug:slug>/', views.CollectionDetailView.as_view(), name='detail'),
path('<slug:slug>/establishments/', views.CollectionEstablishmentListView.as_view(), path('slug/<slug:slug>/establishments/', views.CollectionEstablishmentListView.as_view(),
name='detail'), name='detail'),
path('guides/', views.GuideListView.as_view(), name='guides-list'),
path('guides/<int:pk>/', views.GuideRetrieveView.as_view(), name='guides-detail'),
] ]

View File

@ -2,6 +2,8 @@
from collection.urls.common import urlpatterns as common_url_patterns from collection.urls.common import urlpatterns as common_url_patterns
app_name = 'web'
urlpatterns_api = [] urlpatterns_api = []

View File

@ -1,8 +1,11 @@
from rest_framework import permissions from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import viewsets, mixins from rest_framework import generics
from rest_framework import mixins, permissions, viewsets
from rest_framework import status
from rest_framework.filters import OrderingFilter
from rest_framework.response import Response
from collection import models from collection import models, serializers
from collection.serializers import back as serializers
from utils.views import BindObjectMixin from utils.views import BindObjectMixin
@ -21,6 +24,22 @@ class CollectionViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
return qs return qs
class GuideBaseView(generics.GenericAPIView):
"""ViewSet for Guide model."""
pagination_class = None
queryset = models.Guide.objects.all()
serializer_class = serializers.GuideBaseSerializer
permission_classes = (permissions.IsAuthenticated,)
class GuideFilterBaseView(generics.GenericAPIView):
"""ViewSet for GuideFilter model."""
pagination_class = None
queryset = models.GuideFilter.objects.all()
serializer_class = serializers.GuideFilterBaseSerializer
permission_classes = (permissions.IsAuthenticated,)
class CollectionBackOfficeViewSet(mixins.CreateModelMixin, class CollectionBackOfficeViewSet(mixins.CreateModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
@ -31,9 +50,13 @@ class CollectionBackOfficeViewSet(mixins.CreateModelMixin,
permission_classes = (permissions.IsAuthenticated,) permission_classes = (permissions.IsAuthenticated,)
queryset = models.Collection.objects.all() queryset = models.Collection.objects.all()
filter_backends = [DjangoFilterBackend, OrderingFilter]
serializer_class = serializers.CollectionBackOfficeSerializer serializer_class = serializers.CollectionBackOfficeSerializer
bind_object_serializer_class = serializers.CollectionBindObjectSerializer bind_object_serializer_class = serializers.CollectionBindObjectSerializer
ordering_fields = ('rank', 'start')
ordering = ('-start', )
def perform_binding(self, serializer): def perform_binding(self, serializer):
data = serializer.validated_data data = serializer.validated_data
collection = data.pop('collection') collection = data.pop('collection')
@ -53,3 +76,19 @@ class CollectionBackOfficeViewSet(mixins.CreateModelMixin,
collection.establishments.remove(related_object) collection.establishments.remove(related_object)
elif obj_type == self.bind_object_serializer_class.PRODUCT: elif obj_type == self.bind_object_serializer_class.PRODUCT:
collection.products.remove(related_object) collection.products.remove(related_object)
class GuideListCreateView(GuideBaseView,
generics.ListCreateAPIView):
"""ViewSet for Guide model for BackOffice users."""
def post(self, request, *args, **kwargs):
super().create(request, *args, **kwargs)
return Response(status=status.HTTP_200_OK)
class GuideFilterCreateView(GuideFilterBaseView,
generics.CreateAPIView):
"""ViewSet for GuideFilter model for BackOffice users."""
def post(self, request, *args, **kwargs):
super().create(request, *args, **kwargs)
return Response(status=status.HTTP_200_OK)

View File

@ -17,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', '-created')
class GuideViewMixin(generics.GenericAPIView): class GuideViewMixin(generics.GenericAPIView):
@ -39,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):
@ -72,10 +72,10 @@ class CollectionEstablishmentListView(CollectionListView):
class GuideListView(GuideViewMixin, generics.ListAPIView): class GuideListView(GuideViewMixin, generics.ListAPIView):
"""List Guide view""" """List Guide view"""
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.GuideSerializer serializer_class = serializers.GuideBaseSerializer
class GuideRetrieveView(GuideViewMixin, generics.RetrieveAPIView): class GuideRetrieveView(GuideViewMixin, generics.RetrieveAPIView):
"""Retrieve Guide view""" """Retrieve Guide view"""
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.GuideSerializer serializer_class = serializers.GuideBaseSerializer

View File

@ -1,5 +1,5 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from tqdm import tqdm
from establishment.models import Establishment from establishment.models import Establishment
from transfer.models import Reviews, ReviewTexts from transfer.models import Reviews, ReviewTexts
@ -22,7 +22,7 @@ class Command(BaseCommand):
'updated_at', 'updated_at',
) )
for r_id, establishment_id, new_date in queryset: for r_id, establishment_id, new_date in tqdm(queryset):
try: try:
review_id, date = valid_reviews[establishment_id] review_id, date = valid_reviews[establishment_id]
except KeyError: except KeyError:
@ -41,7 +41,7 @@ class Command(BaseCommand):
'text', 'text',
) )
for es_id, locale, text in text_qs: for es_id, locale, text in tqdm(text_qs):
establishment = Establishment.objects.filter(old_id=es_id).first() establishment = Establishment.objects.filter(old_id=es_id).first()
if establishment: if establishment:
description = establishment.description description = establishment.description
@ -53,7 +53,7 @@ class Command(BaseCommand):
count += 1 count += 1
# Если нет en-GB в поле # Если нет en-GB в поле
for establishment in Establishment.objects.filter(old_id__isnull=False): for establishment in tqdm(Establishment.objects.filter(old_id__isnull=False)):
description = establishment.description description = establishment.description
if len(description) and 'en-GB' not in description: if len(description) and 'en-GB' not in description:
description.update({ description.update({

View File

@ -0,0 +1,45 @@
from django.core.management.base import BaseCommand
from tqdm import tqdm
from establishment.models import Establishment
from transfer.models import Descriptions
class Command(BaseCommand):
help = """Add description to establishment from old db."""
def handle(self, *args, **kwarg):
establishments = Establishment.objects.exclude(old_id__isnull=True)
self.stdout.write(self.style.WARNING(f'Clear old descriptions'))
for item in tqdm(establishments):
item.description = None
item.save()
queryset = Descriptions.objects.filter(
establishment_id__in=list(establishments.values_list('old_id', flat=True)),
).values_list('establishment_id', 'locale', 'text')
self.stdout.write(self.style.WARNING(f'Update new description'))
for establishment_id, locale, text in tqdm(queryset):
establishment = Establishment.objects.filter(old_id=establishment_id).first()
if establishment:
if establishment.description:
establishment.description.update({
locale: text
})
else:
establishment.description = {locale: text}
establishment.save()
self.stdout.write(self.style.WARNING(f'Update en-GB description'))
for establishment in tqdm(establishments.filter(description__isnull=False)):
description = establishment.description
if len(description) and 'en-GB' not in description:
description.update({
'en-GB': next(iter(description.values()))
})
establishment.description = description
establishment.save()
self.stdout.write(self.style.WARNING(f'Done'))

View File

@ -212,6 +212,10 @@ class EstablishmentQuerySet(models.QuerySet):
output_field=models.FloatField(default=0) output_field=models.FloatField(default=0)
)) ))
def has_location(self):
"""Return objects with geo location."""
return self.filter(address__coordinates__isnull=False)
def similar_base(self, establishment): def similar_base(self, establishment):
""" """
Return filtered QuerySet by base filters. Return filtered QuerySet by base filters.
@ -267,25 +271,30 @@ class EstablishmentQuerySet(models.QuerySet):
else: else:
return self.none() return self.none()
def similar_artisans(self, slug): def same_subtype(self, establishment):
"""Annotate flag same subtype."""
return self.annotate(same_subtype=Case(
models.When(
establishment_subtypes__in=establishment.establishment_subtypes.all(),
then=True
),
default=False,
output_field=models.BooleanField(default=False)
))
def similar_artisans_producers(self, slug):
""" """
Return QuerySet with objects that similar to Artisan. Return QuerySet with objects that similar to Artisan/Producer(s).
:param slug: str artisan slug :param slug: str artisan/producer slug
""" """
artisan_qs = self.filter(slug=slug) establishment_qs = self.filter(slug=slug)
if artisan_qs.exists(): if establishment_qs.exists():
artisan = artisan_qs.first() establishment = establishment_qs.first()
ids_by_subquery = self.similar_base_subquery( return self.similar_base(establishment) \
establishment=artisan, .same_subtype(establishment) \
filters={ .order_by(F('same_subtype').desc(),
'public_mark__gte': 10, F('distance').asc()) \
} .distinct('same_subtype', 'distance', 'id')
)
return self.filter(id__in=ids_by_subquery) \
.annotate_intermediate_public_mark() \
.annotate_mark_similarity(mark=artisan.public_mark) \
.order_by('mark_similarity') \
.distinct('mark_similarity', 'id')
else: else:
return self.none() return self.none()
@ -541,9 +550,15 @@ class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin,
def visible_tags(self): def visible_tags(self):
return super().visible_tags \ return super().visible_tags \
.exclude(category__index_name__in=['guide', 'collection', 'purchased_item', .exclude(category__index_name__in=['guide', 'collection', 'purchased_item',
'business_tag', 'business_tags_de', 'tag']) 'business_tag', 'business_tags_de']) \
.exclude(value__in=['rss', 'rss_selection'])
# todo: recalculate toque_number # todo: recalculate toque_number
@property
def visible_tags_detail(self):
"""Removes some tags from detail Establishment representation"""
return self.visible_tags.exclude(category__index_name__in=['tag'])
def recalculate_toque_number(self): def recalculate_toque_number(self):
toque_number = 0 toque_number = 0
if self.address and self.public_mark: if self.address and self.public_mark:

View File

@ -225,18 +225,19 @@ class EstablishmentBackOfficeGallerySerializer(serializers.ModelSerializer):
'is_main', 'is_main',
] ]
def get_request_kwargs(self): @property
def request_kwargs(self):
"""Get url kwargs from request.""" """Get url kwargs from request."""
return self.context.get('request').parser_context.get('kwargs') return self.context.get('request').parser_context.get('kwargs')
def validate(self, attrs): def validate(self, attrs):
"""Override validate method.""" """Override validate method."""
establishment_pk = self.get_request_kwargs().get('pk') establishment_pk = self.request_kwargs.get('pk')
establishment_slug = self.get_request_kwargs().get('slug') establishment_slug = self.request_kwargs.get('slug')
search_kwargs = {'pk': establishment_pk} if establishment_pk else {'slug': establishment_slug} search_kwargs = {'pk': establishment_pk} if establishment_pk else {'slug': establishment_slug}
image_id = self.get_request_kwargs().get('image_id') image_id = self.request_kwargs.get('image_id')
establishment_qs = models.Establishment.objects.filter(**search_kwargs) establishment_qs = models.Establishment.objects.filter(**search_kwargs)
image_qs = Image.objects.filter(id=image_id) image_qs = Image.objects.filter(id=image_id)

View File

@ -17,12 +17,14 @@ urlpatterns = [
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(), path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
name='create-destroy-favorites'), name='create-destroy-favorites'),
# similar establishments # similar establishments by type/subtype
path('slug/<slug:slug>/similar/', views.RestaurantSimilarListView.as_view(), path('slug/<slug:slug>/similar/', views.RestaurantSimilarListView.as_view(),
name='similar-restaurants'), name='similar-restaurants'),
path('slug/<slug:slug>/similar/wineries/', views.WinerySimilarListView.as_view(), path('slug/<slug:slug>/similar/wineries/', views.WinerySimilarListView.as_view(),
name='similar-wineries'), name='similar-wineries'),
path('slug/<slug:slug>/similar/artisans/', views.ArtisanSimilarListView.as_view(), # temporary uses single mechanism, bec. description in process
path('slug/<slug:slug>/similar/artisans/', views.ArtisanProducerSimilarListView.as_view(),
name='similar-artisans'), name='similar-artisans'),
path('slug/<slug:slug>/similar/producers/', views.ArtisanProducerSimilarListView.as_view(),
name='similar-producers'),
] ]

View File

@ -8,7 +8,7 @@ from comment import models as comment_models
from comment.serializers import CommentRUDSerializer from comment.serializers import CommentRUDSerializer
from establishment import filters, models, serializers from establishment import filters, models, serializers
from main import methods from main import methods
from utils.pagination import EstablishmentPortionPagination from utils.pagination import PortionPagination
from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
@ -41,6 +41,12 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
.with_certain_tag_category_related('shop_category', 'artisan_category') .with_certain_tag_category_related('shop_category', 'artisan_category')
class EstablishmentSimilarView(EstablishmentListView):
"""Resource for getting a list of similar establishments."""
serializer_class = serializers.EstablishmentSimilarSerializer
pagination_class = PortionPagination
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView): class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
"""Resource for getting a establishment.""" """Resource for getting a establishment."""
@ -61,7 +67,7 @@ class EstablishmentMobileRetrieveView(EstablishmentRetrieveView):
class EstablishmentRecentReviewListView(EstablishmentListView): class EstablishmentRecentReviewListView(EstablishmentListView):
"""List view for last reviewed establishments.""" """List view for last reviewed establishments."""
pagination_class = EstablishmentPortionPagination pagination_class = PortionPagination
def get_queryset(self): def get_queryset(self):
"""Overridden method 'get_queryset'.""" """Overridden method 'get_queryset'."""
@ -77,37 +83,34 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
return qs.last_reviewed(point=point) return qs.last_reviewed(point=point)
class EstablishmentSimilarList(EstablishmentListView): class RestaurantSimilarListView(EstablishmentSimilarView):
"""Resource for getting a list of similar establishments."""
serializer_class = serializers.EstablishmentSimilarSerializer
pagination_class = EstablishmentPortionPagination
class RestaurantSimilarListView(EstablishmentSimilarList):
"""Resource for getting a list of similar restaurants.""" """Resource for getting a list of similar restaurants."""
def get_queryset(self): def get_queryset(self):
"""Overridden get_queryset method""" """Overridden get_queryset method"""
return EstablishmentMixinView.get_queryset(self) \ return EstablishmentMixinView.get_queryset(self) \
.has_location() \
.similar_restaurants(slug=self.kwargs.get('slug')) .similar_restaurants(slug=self.kwargs.get('slug'))
class WinerySimilarListView(EstablishmentSimilarList): class WinerySimilarListView(EstablishmentSimilarView):
"""Resource for getting a list of similar wineries.""" """Resource for getting a list of similar wineries."""
def get_queryset(self): def get_queryset(self):
"""Overridden get_queryset method""" """Overridden get_queryset method"""
return EstablishmentMixinView.get_queryset(self) \ return EstablishmentMixinView.get_queryset(self) \
.has_location() \
.similar_wineries(slug=self.kwargs.get('slug')) .similar_wineries(slug=self.kwargs.get('slug'))
class ArtisanSimilarListView(EstablishmentSimilarList): class ArtisanProducerSimilarListView(EstablishmentSimilarView):
"""Resource for getting a list of similar artisans.""" """Resource for getting a list of similar artisan/producer(s)."""
def get_queryset(self): def get_queryset(self):
"""Overridden get_queryset method""" """Overridden get_queryset method"""
return EstablishmentMixinView.get_queryset(self) \ return EstablishmentMixinView.get_queryset(self) \
.similar_artisans(slug=self.kwargs.get('slug')) .has_location() \
.similar_artisans_producers(slug=self.kwargs.get('slug'))
class EstablishmentTypeListView(generics.ListAPIView): class EstablishmentTypeListView(generics.ListAPIView):

View File

@ -35,14 +35,15 @@ class CityGallerySerializer(serializers.ModelSerializer):
'is_main', 'is_main',
] ]
def get_request_kwargs(self): @property
def request_kwargs(self):
"""Get url kwargs from request.""" """Get url kwargs from request."""
return self.context.get('request').parser_context.get('kwargs') return self.context.get('request').parser_context.get('kwargs')
def validate(self, attrs): def validate(self, attrs):
"""Override validate method.""" """Override validate method."""
city_pk = self.get_request_kwargs().get('pk') city_pk = self.request_kwargs.get('pk')
image_id = self.get_request_kwargs().get('image_id') image_id = self.request_kwargs.get('image_id')
city_qs = models.City.objects.filter(pk=city_pk) city_qs = models.City.objects.filter(pk=city_pk)
image_qs = Image.objects.filter(id=image_id) image_qs = Image.objects.filter(id=image_id)

View File

@ -54,6 +54,7 @@ class RegionSerializer(serializers.ModelSerializer):
'country_id' 'country_id'
] ]
class CityShortSerializer(serializers.ModelSerializer): class CityShortSerializer(serializers.ModelSerializer):
"""Short city serializer""" """Short city serializer"""
country = CountrySerializer(read_only=True) country = CountrySerializer(read_only=True)
@ -89,7 +90,6 @@ class CitySerializer(serializers.ModelSerializer):
fields = [ fields = [
'id', 'id',
'name', 'name',
'code',
'region', 'region',
'region_id', 'region_id',
'country_id', 'country_id',

View File

@ -1,4 +1,5 @@
"""Location app views.""" """Location app views."""
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics from rest_framework import generics
from location import models, serializers from location import models, serializers
@ -11,6 +12,7 @@ from utils.serializers import ImageBaseSerializer
from location import filters from location import filters
# Address # Address
@ -18,29 +20,36 @@ class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView)
"""Create view for model Address.""" """Create view for model Address."""
serializer_class = serializers.AddressDetailSerializer serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all() queryset = models.Address.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView): class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Address.""" """RUD view for model Address."""
serializer_class = serializers.AddressDetailSerializer serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all() queryset = models.Address.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
# City # City
class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView): class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City.""" """Create view for model City."""
serializer_class = serializers.CitySerializer serializer_class = serializers.CitySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
queryset = models.City.objects.all() # queryset = models.City.objects.all()
filter_class = filters.CityBackFilter filter_class = filters.CityBackFilter
def get_queryset(self):
"""Overridden method 'get_queryset'."""
qs = models.City.objects.all()
if self.request.country_code:
qs = qs.by_country_code(self.request.country_code)
return qs
class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView): class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City.""" """Create view for model City."""
serializer_class = serializers.CitySerializer serializer_class = serializers.CitySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
queryset = models.City.objects.all() queryset = models.City.objects.all()
filter_class = filters.CityBackFilter filter_class = filters.CityBackFilter
pagination_class = None pagination_class = None
@ -49,14 +58,14 @@ class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView):
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView): class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model City.""" """RUD view for model City."""
serializer_class = serializers.CitySerializer serializer_class = serializers.CitySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
class CityGalleryCreateDestroyView(common.CityViewMixin, class CityGalleryCreateDestroyView(common.CityViewMixin,
CreateDestroyGalleryViewMixin): CreateDestroyGalleryViewMixin):
"""Resource for a create gallery for product for back-office users.""" """Resource for a create gallery for product for back-office users."""
serializer_class = serializers.CityGallerySerializer serializer_class = serializers.CityGallerySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
def get_object(self): def get_object(self):
""" """
@ -77,7 +86,7 @@ class CityGalleryListView(common.CityViewMixin,
generics.ListAPIView): generics.ListAPIView):
"""Resource for returning gallery for product for back-office users.""" """Resource for returning gallery for product for back-office users."""
serializer_class = ImageBaseSerializer serializer_class = ImageBaseSerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
def get_object(self): def get_object(self):
"""Override get_object method.""" """Override get_object method."""
@ -99,13 +108,18 @@ class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region""" """Create view for model Region"""
pagination_class = None pagination_class = None
serializer_class = serializers.RegionSerializer serializer_class = serializers.RegionSerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
filter_backends = (DjangoFilterBackend,)
ordering_fields = '__all__'
filterset_fields = (
'country',
)
class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView): class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""Retrieve view for model Region""" """Retrieve view for model Region"""
serializer_class = serializers.RegionSerializer serializer_class = serializers.RegionSerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
# Country # Country
@ -114,11 +128,11 @@ class CountryListCreateView(generics.ListCreateAPIView):
queryset = models.Country.objects.all() queryset = models.Country.objects.all()
serializer_class = serializers.CountryBackSerializer serializer_class = serializers.CountryBackSerializer
pagination_class = None pagination_class = None
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
class CountryRUDView(generics.RetrieveUpdateDestroyAPIView): class CountryRUDView(generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Country.""" """RUD view for model Country."""
serializer_class = serializers.CountryBackSerializer serializer_class = serializers.CountryBackSerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin] permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
queryset = models.Country.objects.all() queryset = models.Country.objects.all()

View File

@ -11,7 +11,7 @@ class SiteSettingsInline(admin.TabularInline):
@admin.register(models.SiteSettings) @admin.register(models.SiteSettings)
class SiteSettingsAdmin(admin.ModelAdmin): class SiteSettingsAdmin(admin.ModelAdmin):
"""Site settings admin conf.""" """Site settings admin conf."""
inlines = [SiteSettingsInline,] inlines = [SiteSettingsInline, ]
@admin.register(models.Feature) @admin.register(models.Feature)
@ -62,6 +62,11 @@ class FooterAdmin(admin.ModelAdmin):
list_display = ('id', 'site', ) list_display = ('id', 'site', )
@admin.register(models.FooterLink)
class FooterLinkAdmin(admin.ModelAdmin):
"""FooterLink admin."""
@admin.register(models.Panel) @admin.register(models.Panel)
class PanelAdmin(admin.ModelAdmin): class PanelAdmin(admin.ModelAdmin):
"""Panel admin.""" """Panel admin."""

View File

@ -37,6 +37,13 @@ def determine_country_code(request):
return country_code.lower() return country_code.lower()
def determine_country_name(request):
"""Determine country name."""
META = request.META
return META.get('X-GeoIP-Country-Name',
META.get('HTTP_X_GEOIP_COUNTRY_NAME'))
def determine_coordinates(request): def determine_coordinates(request):
META = request.META META = request.META
longitude = META.get('X-GeoIP-Longitude', longitude = META.get('X-GeoIP-Longitude',

View File

@ -0,0 +1,32 @@
# Generated by Django 2.2.7 on 2019-12-17 11:20
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('main', '0042_panel'),
]
operations = [
migrations.CreateModel(
name='FooterLinks',
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')),
('link', models.URLField(verbose_name='link')),
('title', models.CharField(max_length=255, verbose_name='title')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='footer',
name='links',
field=models.ManyToManyField(related_name='link_footer', to='main.FooterLinks', verbose_name='links'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.7 on 2019-12-17 11:25
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('main', '0043_auto_20191217_1120'),
]
operations = [
migrations.RenameModel(
old_name='FooterLinks',
new_name='FooterLink',
),
]

View File

@ -283,6 +283,11 @@ class Carousel(models.Model):
@property @property
def slug(self): def slug(self):
if hasattr(self.content_object, 'slugs'):
try:
return next(iter(self.content_object.slugs.values()))
except StopIteration:
return None
if hasattr(self.content_object, 'slug'): if hasattr(self.content_object, 'slug'):
return self.content_object.slug return self.content_object.slug
@ -358,6 +363,11 @@ class PageType(ProjectBaseMixin):
return self.name return self.name
class FooterLink(ProjectBaseMixin):
link = models.URLField(_('link'))
title = models.CharField(_('title'), max_length=255)
class Footer(ProjectBaseMixin): class Footer(ProjectBaseMixin):
site = models.ForeignKey( site = models.ForeignKey(
'main.SiteSettings', related_name='footers', verbose_name=_('footer'), 'main.SiteSettings', related_name='footers', verbose_name=_('footer'),
@ -365,6 +375,7 @@ class Footer(ProjectBaseMixin):
) )
about_us = models.TextField(_('about_us')) about_us = models.TextField(_('about_us'))
copyright = models.TextField(_('copyright')) copyright = models.TextField(_('copyright'))
links = models.ManyToManyField(FooterLink, verbose_name=_('links'), related_name='link_footer')
class PanelQuerySet(models.QuerySet): class PanelQuerySet(models.QuerySet):

View File

@ -6,7 +6,6 @@ from rest_framework.response import Response
from main import methods, models, serializers from main import methods, models, serializers
# #
# class FeatureViewMixin: # class FeatureViewMixin:
# """Feature view mixin.""" # """Feature view mixin."""
@ -85,8 +84,14 @@ class DetermineLocation(generics.GenericAPIView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
longitude, latitude = methods.determine_coordinates(request) longitude, latitude = methods.determine_coordinates(request)
city = methods.determine_user_city(request) city = methods.determine_user_city(request)
if longitude and latitude and city: country_name = methods.determine_country_name(request)
return Response(data={'latitude': latitude, 'longitude': longitude, 'city': city}) country_code = methods.determine_country_code(request)
else: if longitude and latitude and city and country_name:
raise Http404 return Response(data={
'latitude': latitude,
'longitude': longitude,
'city': city,
'country_name': country_name,
'country_code': country_code,
})
raise Http404

View File

@ -72,4 +72,6 @@ class NewsListFilterSet(filters.FilterSet):
return queryset return queryset
def sort_by_field(self, queryset, name, value): def sort_by_field(self, queryset, name, value):
if value == self.SORT_BY_START_CHOICE:
return queryset.order_by('-publication_date', '-publication_time')
return queryset.order_by(f'-{value}') return queryset.order_by(f'-{value}')

View File

@ -0,0 +1,37 @@
# Generated by Django 2.2.7 on 2019-12-16 19:20
import django.contrib.postgres.fields.hstore
from django.db import migrations, models
import uuid
def fill_uuid(apps, schemaeditor):
News = apps.get_model('news', 'News')
for news in News.objects.all():
news.duplication_uuid = uuid.uuid4()
news.save()
class Migration(migrations.Migration):
dependencies = [
('news', '0042_news_duplication_date'),
]
operations = [
migrations.AddField(
model_name='news',
name='description_to_locale_is_active',
field=django.contrib.postgres.fields.hstore.HStoreField(blank=True, default=dict, help_text='{"en-GB": true, "fr-FR": false}', null=True, verbose_name='Is description for certain locale active'),
),
migrations.AddField(
model_name='news',
name='duplication_uuid',
field=models.UUIDField(default=uuid.uuid4, verbose_name='Field to detect doubles'),
),
migrations.AlterField(
model_name='news',
name='slugs',
field=django.contrib.postgres.fields.hstore.HStoreField(blank=True, default=dict, help_text='{"en-GB":"some slug"}', null=True, verbose_name='Slugs for current news obj'),
),
migrations.RunPython(fill_uuid, migrations.RunPython.noop),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-16 20:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0043_auto_20191216_1920'),
]
operations = [
migrations.RenameField(
model_name='news',
old_name='description_to_locale_is_active',
new_name='locale_to_description_is_active',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-17 17:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0044_auto_20191216_2044'),
]
operations = [
migrations.AddField(
model_name='news',
name='must_of_the_week',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 2.2.7 on 2019-12-18 14:37
from django.db import migrations, models
def fill_publication_date_and_time(apps, schema_editor):
News = apps.get_model('news', 'News')
for news in News.objects.all():
if news.start is not None:
news.publication_date = news.start.date()
news.publication_time = news.start.time()
news.save()
class Migration(migrations.Migration):
dependencies = [
('news', '0045_news_must_of_the_week'),
]
operations = [
migrations.AddField(
model_name='news',
name='publication_date',
field=models.DateField(blank=True, help_text='date since when news item is published', null=True, verbose_name='News publication date'),
),
migrations.AddField(
model_name='news',
name='publication_time',
field=models.TimeField(blank=True, help_text='time since when news item is published', null=True, verbose_name='News publication time'),
),
migrations.AlterField(
model_name='news',
name='must_of_the_week',
field=models.BooleanField(default=False, verbose_name='Show in the carousel'),
),
migrations.RunPython(fill_publication_date_and_time, migrations.RunPython.noop),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.7 on 2019-12-18 16:20
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0046_auto_20191218_1437'),
]
operations = [
migrations.RemoveField(
model_name='news',
name='start',
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.7 on 2019-12-19 13:47
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('news', '0047_remove_news_start'),
]
operations = [
migrations.RemoveField(
model_name='news',
name='must_of_the_week',
),
]

View File

@ -1,18 +1,23 @@
"""News app models.""" """News app models."""
import uuid
from django.conf import settings
from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes import fields as generic
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import HStoreField
from django.db import models from django.db import models
from django.db.models import Case, When from django.db.models import Case, When
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from main.models import Carousel
from rating.models import Rating, ViewCount from rating.models import Rating, ViewCount
from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin, from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin,
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin, ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin,
FavoritesMixin) FavoritesMixin)
from utils.querysets import TranslationQuerysetMixin from utils.querysets import TranslationQuerysetMixin
from django.conf import settings from datetime import datetime
from django.contrib.postgres.fields import HStoreField
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin): class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
@ -62,7 +67,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
def sort_by_start(self): def sort_by_start(self):
"""Return qs sorted by start DESC""" """Return qs sorted by start DESC"""
return self.order_by('-start') return self.order_by('-publication_date', '-publication_time')
def rating_value(self): def rating_value(self):
return self.annotate(rating=models.Count('ratings__ip', distinct=True)) return self.annotate(rating=models.Count('ratings__ip', distinct=True))
@ -97,9 +102,13 @@ class NewsQuerySet(TranslationQuerysetMixin):
def published(self): def published(self):
"""Return only published news""" """Return only published news"""
now = timezone.now() now = timezone.now()
return self.filter(models.Q(models.Q(end__gte=now) | date_now = now.date()
time_now = now.time()
return self.exclude(models.Q(publication_date__isnull=True) | models.Q(publication_time__isnull=True)). \
filter(models.Q(models.Q(end__gte=now) |
models.Q(end__isnull=True)), models.Q(end__isnull=True)),
state__in=self.model.PUBLISHED_STATES, start__lte=now) state__in=self.model.PUBLISHED_STATES, publication_date__lte=date_now,
publication_time__lte=time_now)
# todo: filter by best score # todo: filter by best score
# todo: filter by country? # todo: filter by country?
@ -112,7 +121,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
return self.model.objects.exclude(pk=news.pk).published(). \ return self.model.objects.exclude(pk=news.pk).published(). \
annotate_in_favorites(user). \ annotate_in_favorites(user). \
with_base_related().by_type(news.news_type). \ with_base_related().by_type(news.news_type). \
by_tags(news.tags.all()).distinct().order_by('-start') by_tags(news.tags.all()).distinct().sort_by_start()
def annotate_in_favorites(self, user): def annotate_in_favorites(self, user):
"""Annotate flag in_favorites""" """Annotate flag in_favorites"""
@ -127,6 +136,9 @@ class NewsQuerySet(TranslationQuerysetMixin):
) )
) )
def by_locale(self, locale):
return self.filter(title__icontains=locale)
class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin, class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixin,
FavoritesMixin): FavoritesMixin):
@ -170,20 +182,25 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
verbose_name=_('title'), verbose_name=_('title'),
help_text='{"en-GB":"some text"}') help_text='{"en-GB":"some text"}')
backoffice_title = models.TextField(null=True, default=None, backoffice_title = models.TextField(null=True, default=None,
verbose_name=_('Title for searching via BO')) verbose_name=_('Title for searching via BO'))
subtitle = TJSONField(blank=True, null=True, default=None, subtitle = TJSONField(blank=True, null=True, default=None,
verbose_name=_('subtitle'), verbose_name=_('subtitle'),
help_text='{"en-GB":"some text"}') help_text='{"en-GB":"some text"}')
description = TJSONField(blank=True, null=True, default=None, description = TJSONField(blank=True, null=True, default=None,
verbose_name=_('description'), verbose_name=_('description'),
help_text='{"en-GB":"some text"}') help_text='{"en-GB":"some text"}')
start = models.DateTimeField(blank=True, null=True, default=None, locale_to_description_is_active = HStoreField(null=True, default=dict, blank=True,
verbose_name=_('Start')) verbose_name=_('Is description for certain locale active'),
help_text='{"en-GB": true, "fr-FR": false}')
publication_date = models.DateField(blank=True, null=True, verbose_name=_('News publication date'),
help_text=_('date since when news item is published'))
publication_time = models.TimeField(blank=True, null=True, verbose_name=_('News publication time'),
help_text=_('time since when news item is published'))
end = models.DateTimeField(blank=True, null=True, default=None, end = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('End')) verbose_name=_('End'))
slugs = HStoreField(null=True, blank=True, default=None, slugs = HStoreField(null=True, blank=True, default=dict,
verbose_name=_('Slugs for current news obj'), verbose_name=_('Slugs for current news obj'),
help_text='{"en-GB":"some slug"}') help_text='{"en-GB":"some slug"}')
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES, state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
verbose_name=_('State')) verbose_name=_('State'))
is_highlighted = models.BooleanField(default=False, is_highlighted = models.BooleanField(default=False,
@ -213,6 +230,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
on_delete=models.SET_NULL, verbose_name=_('site settings')) on_delete=models.SET_NULL, verbose_name=_('site settings'))
duplication_date = models.DateTimeField(blank=True, null=True, default=None, duplication_date = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('Duplication datetime')) verbose_name=_('Duplication datetime'))
duplication_uuid = models.UUIDField(default=uuid.uuid4, editable=True, unique=False,
verbose_name=_('Field to detect doubles'))
objects = NewsQuerySet.as_manager() objects = NewsQuerySet.as_manager()
class Meta: class Meta:
@ -233,6 +252,34 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
self.duplication_date = timezone.now() self.duplication_date = timezone.now()
self.save() self.save()
@property
def must_of_the_week(self) -> bool:
"""Detects whether current item in carousel"""
kwargs = {
'content_type': ContentType.objects.get_for_model(self),
'object_id': self.pk,
'country': self.country,
}
return Carousel.objects.filter(**kwargs).exists()
@property
def publication_datetime(self):
"""Represents datetime object combined from `publication_date` & `publication_time` fields"""
try:
return datetime.combine(date=self.publication_date, time=self.publication_time)
except TypeError:
return None
@property
def duplicates(self):
"""Duplicates for this news item excluding same country code labeled"""
return News.objects.filter(duplication_uuid=self.duplication_uuid).exclude(country=self.country)
@property
def has_any_desc_active(self):
"""Detects whether news item has any active description"""
return any(list(map(lambda v: v.lower() == 'true', self.locale_to_description_is_active.values())))
@property @property
def is_publish(self): def is_publish(self):
return self.state in self.PUBLISHED_STATES return self.state in self.PUBLISHED_STATES
@ -317,7 +364,6 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
class NewsGallery(IntermediateGalleryModelMixin): 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,
@ -331,4 +377,4 @@ class NewsGallery(IntermediateGalleryModelMixin):
"""NewsGallery meta class.""" """NewsGallery meta class."""
verbose_name = _('news gallery') verbose_name = _('news gallery')
verbose_name_plural = _('news galleries') verbose_name_plural = _('news galleries')
unique_together = [['news', 'image'],] unique_together = [['news', 'image'], ]

View File

@ -5,7 +5,7 @@ from rest_framework.fields import SerializerMethodField
from account.serializers.common import UserBaseSerializer from account.serializers.common import UserBaseSerializer
from gallery.models import Image from gallery.models import Image
from main.models import SiteSettings from main.models import SiteSettings, Carousel
from location import models as location_models from location import models as location_models
from location.serializers import CountrySimpleSerializer, AddressBaseSerializer from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
from news import models from news import models
@ -128,6 +128,7 @@ class NewsDetailSerializer(NewsBaseSerializer):
state_display = serializers.CharField(source='get_state_display', state_display = serializers.CharField(source='get_state_display',
read_only=True) read_only=True)
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True) gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
start = serializers.DateTimeField(source='publication_datetime', read_only=True)
class Meta(NewsBaseSerializer.Meta): class Meta(NewsBaseSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -182,12 +183,17 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
'backoffice_title', 'backoffice_title',
'subtitle', 'subtitle',
'slugs', 'slugs',
'locale_to_description_is_active',
'is_published', 'is_published',
'duplication_date', 'duplication_date',
'must_of_the_week',
'publication_date',
'publication_time',
) )
extra_kwargs = { extra_kwargs = {
'backoffice_title': {'allow_null': False},
'duplication_date': {'read_only': True}, 'duplication_date': {'read_only': True},
'locale_to_description_is_active': {'allow_null': False},
'must_of_the_week': {'read_only': True},
} }
def create(self, validated_data): def create(self, validated_data):
@ -209,6 +215,20 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
return super().update(instance, validated_data) return super().update(instance, validated_data)
class NewsBackOfficeDuplicationInfoSerializer(serializers.ModelSerializer):
"""Duplication info for news detail."""
country = CountrySimpleSerializer(read_only=True)
class Meta:
model = models.News
fields = (
'id',
'duplication_date',
'country',
)
class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
NewsDetailSerializer): NewsDetailSerializer):
"""News detail serializer for back-office users.""" """News detail serializer for back-office users."""
@ -224,6 +244,7 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
queryset=SiteSettings.objects.all()) queryset=SiteSettings.objects.all())
template_display = serializers.CharField(source='get_template_display', template_display = serializers.CharField(source='get_template_display',
read_only=True) read_only=True)
duplicates = NewsBackOfficeDuplicationInfoSerializer(many=True, allow_null=True, read_only=True)
class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta): class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta):
"""Meta class.""" """Meta class."""
@ -237,6 +258,7 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
'template', 'template',
'template_display', 'template_display',
'is_international', 'is_international',
'duplicates',
) )
@ -252,13 +274,14 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
'is_main', 'is_main',
] ]
def get_request_kwargs(self): @property
def request_kwargs(self):
"""Get url kwargs from request.""" """Get url kwargs from request."""
return self.context.get('request').parser_context.get('kwargs') return self.context.get('request').parser_context.get('kwargs')
def create(self, validated_data): def create(self, validated_data):
news_pk = self.get_request_kwargs().get('pk') news_pk = self.request_kwargs.get('pk')
image_id = self.get_request_kwargs().get('image_id') image_id = self.request_kwargs.get('image_id')
qs = models.NewsGallery.objects.filter(image_id=image_id, news_id=news_pk) qs = models.NewsGallery.objects.filter(image_id=image_id, news_id=news_pk)
instance = qs.first() instance = qs.first()
if instance: if instance:
@ -268,8 +291,8 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
def validate(self, attrs): def validate(self, attrs):
"""Override validate method.""" """Override validate method."""
news_pk = self.get_request_kwargs().get('pk') news_pk = self.request_kwargs.get('pk')
image_id = self.get_request_kwargs().get('image_id') image_id = self.request_kwargs.get('image_id')
news_qs = models.News.objects.filter(pk=news_pk) news_qs = models.News.objects.filter(pk=news_pk)
image_qs = Image.objects.filter(id=image_id) image_qs = Image.objects.filter(id=image_id)
@ -337,7 +360,12 @@ class NewsCarouselCreateSerializer(CarouselCreateSerializer):
def create(self, validated_data, *args, **kwargs): def create(self, validated_data, *args, **kwargs):
validated_data.update({ validated_data.update({
'content_object': validated_data.pop('news') 'country': validated_data['news'].country
})
validated_data.update({
'content_object': validated_data.pop('news'),
'is_parse': True,
'active': True,
}) })
return super().create(validated_data) return super().create(validated_data)
@ -347,9 +375,11 @@ class NewsCloneCreateSerializer(NewsBackOfficeBaseSerializer,
"""Serializer for creating news clone.""" """Serializer for creating news clone."""
template_display = serializers.CharField(source='get_template_display', template_display = serializers.CharField(source='get_template_display',
read_only=True) read_only=True)
duplicates = NewsBackOfficeDuplicationInfoSerializer(many=True, allow_null=True, read_only=True)
class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta): class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta):
fields = NewsBackOfficeBaseSerializer.Meta.fields + NewsDetailSerializer.Meta.fields + ( fields = NewsBackOfficeBaseSerializer.Meta.fields + NewsDetailSerializer.Meta.fields + (
'template_display', 'template_display',
'duplicates',
) )
read_only_fields = fields read_only_fields = fields
@ -359,5 +389,5 @@ class NewsCloneCreateSerializer(NewsBackOfficeBaseSerializer,
new_country = get_object_or_404(location_models.Country, code=kwargs['country_code']) new_country = get_object_or_404(location_models.Country, code=kwargs['country_code'])
view_count_model = rating_models.ViewCount.objects.create(count=0) view_count_model = rating_models.ViewCount.objects.create(count=0)
instance.create_duplicate(new_country, view_count_model) instance.create_duplicate(new_country, view_count_model)
return instance return get_object_or_404(models.News, pk=kwargs['pk'])

View File

@ -14,5 +14,5 @@ urlpatterns = [
path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(), path('<int:pk>/gallery/<int:image_id>/', views.NewsBackOfficeGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'), name='gallery-create-destroy'),
path('<int:pk>/carousels/', views.NewsCarouselCreateDestroyView.as_view(), name='create-destroy-carousels'), path('<int:pk>/carousels/', views.NewsCarouselCreateDestroyView.as_view(), name='create-destroy-carousels'),
path('<int:pk>/clone/<str:country_code>', views.NewsCloneView.as_view(), name='create-destroy-carousels'), path('<int:pk>/clone/<str:country_code>', views.NewsCloneView.as_view(), name='clone-news-item'),
] ]

View File

@ -1,7 +1,8 @@
"""News app views.""" """News app views."""
from django.conf import settings from django.conf import settings
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions from django.utils import translation
from rest_framework import generics, permissions, response
from news import filters, models, serializers from news import filters, models, serializers
from rating.tasks import add_rating from rating.tasks import add_rating
@ -21,7 +22,7 @@ class NewsMixinView:
qs = models.News.objects.published() \ qs = models.News.objects.published() \
.with_base_related() \ .with_base_related() \
.annotate_in_favorites(self.request.user) \ .annotate_in_favorites(self.request.user) \
.order_by('-is_highlighted', '-start') .order_by('-is_highlighted', '-publication_date', '-publication_time')
country_code = self.request.country_code country_code = self.request.country_code
if country_code: if country_code:
@ -29,6 +30,11 @@ class NewsMixinView:
qs = qs.international_news() qs = qs.international_news()
else: else:
qs = qs.by_country_code(country_code) qs = qs.by_country_code(country_code)
# locale = kwargs.get('locale')
# if locale:
# qs = qs.by_locale(locale)
return qs return qs
def get_object(self): def get_object(self):
@ -43,8 +49,13 @@ class NewsListView(NewsMixinView, generics.ListAPIView):
filter_class = filters.NewsListFilterSet filter_class = filters.NewsListFilterSet
def get_queryset(self, *args, **kwargs): def get_queryset(self, *args, **kwargs):
kwargs.update({'international_preferred': True}) locale = translation.get_language()
return super().get_queryset(*args, **kwargs) kwargs.update({
'international_preferred': True,
'locale': locale,
})
return super().get_queryset(*args, **kwargs)\
.filter(locale_to_description_is_active__values__contains=['True'])
class NewsDetailView(NewsMixinView, generics.RetrieveAPIView): class NewsDetailView(NewsMixinView, generics.RetrieveAPIView):
@ -99,7 +110,10 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView,
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method.""" """Override get_queryset method."""
return super().get_queryset().with_extended_related() qs = super().get_queryset().with_extended_related()
if self.request.country_code:
qs = qs.by_country_code(self.request.country_code)
return qs
class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
@ -107,6 +121,13 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
"""Resource for a create gallery for news for back-office users.""" """Resource for a create gallery for news for back-office users."""
serializer_class = serializers.NewsBackOfficeGallerySerializer serializer_class = serializers.NewsBackOfficeGallerySerializer
def create(self, request, *args, **kwargs):
_ = super().create(request, *args, **kwargs)
news_qs = self.filter_queryset(self.get_queryset())
return response.Response(
data=serializers.NewsDetailSerializer(get_object_or_404(news_qs, pk=kwargs.get('pk'))).data
)
def get_object(self): def get_object(self):
""" """
Returns the object the view is displaying. Returns the object the view is displaying.
@ -171,6 +192,6 @@ class NewsCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
class NewsCloneView(generics.CreateAPIView): class NewsCloneView(generics.CreateAPIView):
"""View for creating clone News""" """View for creating clone News"""
permission_classes = (permissions.AllowAny, ) permission_classes = (permissions.AllowAny,)
serializer_class = serializers.NewsCloneCreateSerializer serializer_class = serializers.NewsCloneCreateSerializer
queryset = models.News.objects.all() queryset = models.News.objects.all()

View File

@ -1,13 +1,17 @@
"""Product app models.""" """Product app models."""
from django.conf import settings
from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes import fields as generic
from django.contrib.gis.db import models as gis_models from django.contrib.gis.db import models as gis_models
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import Case, When from django.db.models import Case, When, F
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from location.models import WineOriginAddressMixin from location.models import WineOriginAddressMixin
from review.models import Review
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin, from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
TranslatedFieldsMixin, TJSONField, FavoritesMixin, TranslatedFieldsMixin, TJSONField, FavoritesMixin,
GalleryModelMixin, IntermediateGalleryModelMixin) GalleryModelMixin, IntermediateGalleryModelMixin)
@ -136,6 +140,60 @@ class ProductQuerySet(models.QuerySet):
) )
) )
def annotate_distance(self, point: Point = None):
"""
Return QuerySet with annotated field - distance
Description:
"""
return self.annotate(distance=Distance('establishment__address__coordinates',
point,
srid=settings.GEO_DEFAULT_SRID))
def has_location(self):
"""Return objects with geo location."""
return self.filter(establishment__address__coordinates__isnull=False)
def same_subtype(self, product):
"""Annotate flag same subtype."""
return self.annotate(same_subtype=Case(
models.When(
subtypes__in=product.subtypes.all(),
then=True
),
default=False,
output_field=models.BooleanField(default=False)
))
def similar_base(self, product):
"""Return QuerySet filtered by base filters for Product model."""
filters = {
'reviews__status': Review.READY,
'product_type': product.product_type,
}
if product.subtypes.exists():
filters.update(
{'subtypes__in': product.subtypes.all()})
return self.exclude(id=product.id) \
.filter(**filters) \
.annotate_distance(point=product.establishment.location)
def similar(self, slug):
"""
Return QuerySet with objects that similar to Product.
:param slug: str product slug
"""
product_qs = self.filter(slug=slug)
if product_qs.exists():
product = product_qs.first()
return self.similar_base(product) \
.same_subtype(product) \
.order_by(F('same_subtype').desc(),
F('distance').asc()) \
.distinct('same_subtype', 'distance', 'id')
else:
return self.none()
class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes, class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
HasTagsMixin, FavoritesMixin): HasTagsMixin, FavoritesMixin):
@ -219,8 +277,8 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
awards = generic.GenericRelation(to='main.Award', related_query_name='product') awards = generic.GenericRelation(to='main.Award', related_query_name='product')
serial_number = models.CharField(max_length=255, serial_number = models.CharField(max_length=255,
default=None, null=True, default=None, null=True,
verbose_name=_('Serial number')) verbose_name=_('Serial number'))
objects = ProductManager.from_queryset(ProductQuerySet)() objects = ProductManager.from_queryset(ProductQuerySet)()

View File

@ -22,14 +22,15 @@ class ProductBackOfficeGallerySerializer(serializers.ModelSerializer):
'is_main', 'is_main',
] ]
def get_request_kwargs(self): @property
def request_kwargs(self):
"""Get url kwargs from request.""" """Get url kwargs from request."""
return self.context.get('request').parser_context.get('kwargs') return self.context.get('request').parser_context.get('kwargs')
def validate(self, attrs): def validate(self, attrs):
"""Override validate method.""" """Override validate method."""
product_pk = self.get_request_kwargs().get('pk') product_pk = self.request_kwargs.get('pk')
image_id = self.get_request_kwargs().get('image_id') image_id = self.request_kwargs.get('image_id')
product_qs = models.Product.objects.filter(pk=product_pk) product_qs = models.Product.objects.filter(pk=product_pk)
image_qs = Image.objects.filter(id=image_id) image_qs = Image.objects.filter(id=image_id)

View File

@ -218,4 +218,4 @@ class ProductCommentCreateSerializer(CommentSerializer):
'user': self.context.get('request').user, 'user': self.context.get('request').user,
'content_object': validated_data.pop('product') 'content_object': validated_data.pop('product')
}) })
return super().create(validated_data) return super().create(validated_data)

View File

@ -16,4 +16,13 @@ urlpatterns = [
name='create-comment'), name='create-comment'),
path('slug/<slug:slug>/comments/<int:comment_id>/', views.ProductCommentRUDView.as_view(), path('slug/<slug:slug>/comments/<int:comment_id>/', views.ProductCommentRUDView.as_view(),
name='rud-comment'), name='rud-comment'),
# similar products by type/subtype
# temporary uses single mechanism, bec. description in process
path('slug/<slug:slug>/similar/wines/', views.SimilarListView.as_view(),
name='similar-wine'),
path('slug/<slug:slug>/similar/liquors/', views.SimilarListView.as_view(),
name='similar-liquor'),
path('slug/<slug:slug>/similar/food/', views.SimilarListView.as_view(),
name='similar-food'),
] ]

View File

@ -6,6 +6,7 @@ from comment.models import Comment
from product import filters, serializers from product import filters, serializers
from comment.serializers import CommentRUDSerializer from comment.serializers import CommentRUDSerializer
from utils.views import FavoritesCreateDestroyMixinView from utils.views import FavoritesCreateDestroyMixinView
from utils.pagination import PortionPagination
class ProductBaseView(generics.GenericAPIView): class ProductBaseView(generics.GenericAPIView):
@ -31,6 +32,12 @@ class ProductListView(ProductBaseView, generics.ListAPIView):
return qs return qs
class ProductSimilarView(ProductListView):
"""Resource for getting a list of similar product."""
serializer_class = serializers.ProductBaseSerializer
pagination_class = PortionPagination
class ProductDetailView(ProductBaseView, generics.RetrieveAPIView): class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
"""Detail view fro model Product.""" """Detail view fro model Product."""
lookup_field = 'slug' lookup_field = 'slug'
@ -81,3 +88,14 @@ class ProductCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
self.check_object_permissions(self.request, comment_obj) self.check_object_permissions(self.request, comment_obj)
return comment_obj return comment_obj
class SimilarListView(ProductSimilarView):
"""Return similar products."""
def get_queryset(self):
"""Overridden get_queryset method."""
return super().get_queryset() \
.has_location() \
.similar(slug=self.kwargs.get('slug'))

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-16 06:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recipe', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='recipe',
name='old_id',
field=models.PositiveIntegerField(blank=True, default=None, null=True, verbose_name='old id'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-16 13:51
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recipe', '0002_recipe_old_id'),
]
operations = [
migrations.AddField(
model_name='recipe',
name='slug',
field=models.SlugField(max_length=255, null=True, unique=True, verbose_name='Slug'),
),
]

View File

@ -25,6 +25,9 @@ class RecipeQuerySet(models.QuerySet):
default=False, default=False,
output_field=models.BooleanField(default=False))) output_field=models.BooleanField(default=False)))
def by_locale(self, locale):
return self.filter(title__icontains=locale)
class Recipe(TranslatedFieldsMixin, ImageMixin, BaseAttributes): class Recipe(TranslatedFieldsMixin, ImageMixin, BaseAttributes):
"""Recipe model.""" """Recipe model."""
@ -43,22 +46,19 @@ class Recipe(TranslatedFieldsMixin, ImageMixin, BaseAttributes):
STR_FIELD_NAME = 'title' STR_FIELD_NAME = 'title'
title = TJSONField(blank=True, null=True, default=None, verbose_name=_('Title'), title = TJSONField(blank=True, null=True, default=None, verbose_name=_('Title'), help_text='{"en-GB": "some text"}')
help_text='{"en-GB": "some text"}')
subtitle = TJSONField(blank=True, null=True, default=None, verbose_name=_('Subtitle'), subtitle = TJSONField(blank=True, null=True, default=None, verbose_name=_('Subtitle'),
help_text='{"en-GB": "some text"}') help_text='{"en-GB": "some text"}')
description = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'), description = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
help_text='{"en-GB": "some text"}') help_text='{"en-GB": "some text"}')
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES, state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES, verbose_name=_('State'))
verbose_name=_('State')) author = models.CharField(max_length=255, blank=True, null=True, default=None, verbose_name=_('Author'))
author = models.CharField(max_length=255, blank=True, null=True, default=None, published_at = models.DateTimeField(verbose_name=_('Published at'), blank=True, default=None, null=True,
verbose_name=_('Author'))
published_at = models.DateTimeField(verbose_name=_('Published at'),
blank=True, default=None, null=True,
help_text=_('Published at')) help_text=_('Published at'))
published_scheduled_at = models.DateTimeField(verbose_name=_('Published scheduled at'), published_scheduled_at = models.DateTimeField(verbose_name=_('Published scheduled at'), blank=True, default=None,
blank=True, default=None, null=True, null=True, help_text=_('Published scheduled at'))
help_text=_('Published scheduled at')) old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
slug = models.SlugField(unique=True, max_length=255, null=True, verbose_name=_('Slug'))
objects = RecipeQuerySet.as_manager() objects = RecipeQuerySet.as_manager()

View File

@ -14,8 +14,15 @@ class RecipeListSerializer(serializers.ModelSerializer):
"""Meta class.""" """Meta class."""
model = models.Recipe model = models.Recipe
fields = ('id', 'title_translated', 'subtitle_translated', 'author', fields = (
'published_at', 'in_favorites') 'id',
'title_translated',
'subtitle_translated',
'author',
'created_by',
'published_at',
'in_favorites',
)
read_only_fields = fields read_only_fields = fields

View File

@ -1,19 +1,49 @@
from django.db.models import Value, IntegerField, F
from pprint import pprint from pprint import pprint
from django.db.models import Count
from recipe.models import Recipe
from transfer.models import PageTexts from transfer.models import PageTexts
from transfer.serializers.recipe import RecipeSerializer from transfer.serializers.recipe import RecipeSerializer
def transfer_recipe(): def transfer_recipe():
queryset = PageTexts.objects.filter(page__type="Recipe") queryset = PageTexts.objects.filter(
page__type='Recipe',
).values(
'id',
'title',
'summary',
'body',
'locale',
'state',
'slug',
'created_at',
'page__attachment_suffix_url',
'page__account_id',
)
serialized_data = RecipeSerializer(data=list(queryset.values()), many=True) serialized_data = RecipeSerializer(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"News serializer errors: {serialized_data.errors}") pprint(f'Recipe serializer errors: {serialized_data.errors}')
return
# Удаление дубликатов рецептов по одинаковым description
duplicate_descriptions = Recipe.objects.values(
'description'
).annotate(
description_count=Count('description')
).filter(
description_count__gt=1
)
for data in duplicate_descriptions:
description = data['description']
_list = list(Recipe.objects.filter(description=description).values_list('pk', flat=True)[1:])
Recipe.objects.filter(id__in=_list).delete()
data_types = { data_types = {
"recipe": [transfer_recipe] 'recipe': [transfer_recipe]
} }

View File

@ -1,5 +1,7 @@
"""Recipe app common views.""" """Recipe app common views."""
from django.utils import translation
from rest_framework import generics, permissions from rest_framework import generics, permissions
from recipe import models from recipe import models
from recipe.serializers import common as serializers from recipe.serializers import common as serializers
@ -10,9 +12,14 @@ class RecipeViewMixin(generics.GenericAPIView):
pagination_class = None pagination_class = None
permission_classes = (permissions.AllowAny,) permission_classes = (permissions.AllowAny,)
def get_queryset(self): def get_queryset(self, *args, **kwargs):
user = self.request.user user = self.request.user
qs = models.Recipe.objects.published().annotate_in_favorites(user) qs = models.Recipe.objects.published().annotate_in_favorites(user)
locale = kwargs.get('locale')
if locale:
qs = qs.by_locale(locale)
return qs return qs
@ -21,6 +28,11 @@ class RecipeListView(RecipeViewMixin, generics.ListAPIView):
serializer_class = serializers.RecipeListSerializer serializer_class = serializers.RecipeListSerializer
def get_queryset(self, *args, **kwargs):
locale = translation.get_language()
kwargs.update({'locale': locale})
return super().get_queryset(*args, **kwargs)
class RecipeDetailView(RecipeViewMixin, generics.RetrieveAPIView): class RecipeDetailView(RecipeViewMixin, generics.RetrieveAPIView):
"""Resource for detailed recipe information.""" """Resource for detailed recipe information."""

View File

@ -127,8 +127,10 @@ def transfer_product_reviews():
data_types = { data_types = {
"languages": [
transfer_languages,
],
"overlook": [ "overlook": [
# transfer_languages,
transfer_reviews, transfer_reviews,
transfer_text_review, transfer_text_review,
make_en_text_review, make_en_text_review,

View File

@ -3,6 +3,7 @@ from django.conf import settings
from django_elasticsearch_dsl import Document, Index, fields from django_elasticsearch_dsl import Document, Index, fields
from search_indexes.utils import OBJECT_FIELD_PROPERTIES from search_indexes.utils import OBJECT_FIELD_PROPERTIES
from news import models from news import models
from json import dumps
NewsIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'news')) NewsIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'news'))
@ -17,7 +18,7 @@ class NewsDocument(Document):
'name': fields.KeywordField()}) 'name': fields.KeywordField()})
title = fields.ObjectField(attr='title_indexing', title = fields.ObjectField(attr='title_indexing',
properties=OBJECT_FIELD_PROPERTIES) properties=OBJECT_FIELD_PROPERTIES)
slugs = fields.ObjectField(properties=OBJECT_FIELD_PROPERTIES) slugs = fields.KeywordField()
backoffice_title = fields.TextField(analyzer='english') backoffice_title = fields.TextField(analyzer='english')
subtitle = fields.ObjectField(attr='subtitle_indexing', subtitle = fields.ObjectField(attr='subtitle_indexing',
properties=OBJECT_FIELD_PROPERTIES) properties=OBJECT_FIELD_PROPERTIES)
@ -44,10 +45,11 @@ class NewsDocument(Document):
}, },
multi=True) multi=True)
favorites_for_users = fields.ListField(field=fields.IntegerField()) favorites_for_users = fields.ListField(field=fields.IntegerField())
start = fields.DateField(attr='start') start = fields.DateField(attr='publication_datetime')
has_any_desc_active = fields.BooleanField()
def prepare_slugs(self, instance): def prepare_slugs(self, instance):
return {locale: instance.slugs.get(locale) for locale in OBJECT_FIELD_PROPERTIES} return dumps(instance.slugs or {})
class Django: class Django:

View File

@ -6,6 +6,7 @@ from news.serializers import NewsTypeSerializer
from search_indexes.documents import EstablishmentDocument, NewsDocument from search_indexes.documents import EstablishmentDocument, NewsDocument
from search_indexes.documents.product import ProductDocument from search_indexes.documents.product import ProductDocument
from search_indexes.utils import get_translated_value from search_indexes.utils import get_translated_value
from json import loads
class TagsDocumentSerializer(serializers.Serializer): class TagsDocumentSerializer(serializers.Serializer):
@ -243,7 +244,7 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
@staticmethod @staticmethod
def get_slug(obj): def get_slug(obj):
return get_translated_value(obj.slugs) return get_translated_value(loads(obj.slugs))
@staticmethod @staticmethod
def get_title_translated(obj): def get_title_translated(obj):

View File

@ -1,13 +1,15 @@
"""Search indexes app views.""" """Search indexes app views."""
from rest_framework import permissions
from django_elasticsearch_dsl_drf import constants from django_elasticsearch_dsl_drf import constants
from django_elasticsearch_dsl_drf.filter_backends import ( from django_elasticsearch_dsl_drf.filter_backends import (
FilteringFilterBackend, FilteringFilterBackend,
GeoSpatialOrderingFilterBackend, GeoSpatialOrderingFilterBackend,
OrderingFilterBackend, OrderingFilterBackend,
) )
from elasticsearch_dsl import TermsFacet
from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet
from elasticsearch_dsl import TermsFacet
from rest_framework import permissions
from product.models import Product
from search_indexes import serializers, filters, utils from search_indexes import serializers, filters, utils
from search_indexes.documents import EstablishmentDocument, NewsDocument from search_indexes.documents import EstablishmentDocument, NewsDocument
from search_indexes.documents.product import ProductDocument from search_indexes.documents.product import ProductDocument
@ -17,6 +19,11 @@ from utils.pagination import ESDocumentPagination
class NewsDocumentViewSet(BaseDocumentViewSet): class NewsDocumentViewSet(BaseDocumentViewSet):
"""News document ViewSet.""" """News document ViewSet."""
def get_queryset(self):
qs = super(NewsDocumentViewSet, self).get_queryset()
qs = qs.filter('match', has_any_desc_active=True)
return qs
document = NewsDocument document = NewsDocument
lookup_field = 'slug' lookup_field = 'slug'
pagination_class = ESDocumentPagination pagination_class = ESDocumentPagination
@ -61,11 +68,18 @@ class NewsDocumentViewSet(BaseDocumentViewSet):
) )
filter_fields = { filter_fields = {
'tags_id': {
'field': 'tags.id',
'lookups': [
constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE,
],
},
'tag': { 'tag': {
'field': 'tags.id', 'field': 'tags.id',
'lookups': [ 'lookups': [
constants.LOOKUP_QUERY_IN, constants.LOOKUP_QUERY_IN,
constants.LOOKUP_QUERY_EXCLUDE constants.LOOKUP_QUERY_EXCLUDE,
] ]
}, },
'tag_value': { 'tag_value': {
@ -334,6 +348,12 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
# GeoSpatialOrderingFilterBackend, # GeoSpatialOrderingFilterBackend,
] ]
def get_queryset(self):
qs = super(ProductDocumentViewSet, self).get_queryset()
qs = qs.filter('match', state=Product.PUBLISHED)
return qs
ordering_fields = { ordering_fields = {
'created': { 'created': {
'field': 'created', 'field': 'created',
@ -390,7 +410,7 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
'lookups': [constants.LOOKUP_QUERY_IN], 'lookups': [constants.LOOKUP_QUERY_IN],
}, },
'country': { 'country': {
'field': 'establishment.address.city.country.code', 'field': 'establishment.city.country.code',
}, },
'wine_colors_id': { 'wine_colors_id': {
'field': 'wine_colors.id', 'field': 'wine_colors.id',

View File

@ -63,9 +63,20 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
"""ViewSet for TagCategory model.""" """ViewSet for TagCategory model."""
serializer_class = serializers.FiltersTagCategoryBaseSerializer serializer_class = serializers.FiltersTagCategoryBaseSerializer
index_name_to_order = {
'open_now': 9,
'works_noon': 8,
'works_evening': 7,
'pop': 6,
'category': 5,
'toque_number': 4,
'cuisine': 3,
'moment': 2,
'service': 1,
}
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset()) queryset = self.filter_queryset(self.get_queryset().exclude(public=False))
serializer = self.get_serializer(queryset, many=True) serializer = self.get_serializer(queryset, many=True)
result_list = serializer.data result_list = serializer.data
@ -77,7 +88,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
elif query_params.get('product_type'): elif query_params.get('product_type'):
params_type = query_params.get('product_type') params_type = query_params.get('product_type')
week_days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday") week_days = tuple(map(_, ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")))
flags = ('toque_number', 'wine_region', 'works_noon', 'works_evening', 'works_now', 'works_at_weekday') flags = ('toque_number', 'wine_region', 'works_noon', 'works_evening', 'works_now', 'works_at_weekday')
filter_flags = {flag_name: False for flag_name in flags} filter_flags = {flag_name: False for flag_name in flags}
additional_flags = [] additional_flags = []
@ -94,19 +105,6 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
for flag_name in additional_flags: for flag_name in additional_flags:
filter_flags[flag_name] = True filter_flags[flag_name] = True
if filter_flags['toque_number']:
toques = {
"index_name": "toque_number",
"label_translated": "Toques",
"param_name": "toque_number__in",
"filters": [{
"id": toque_id,
"index_name": "toque_%d" % toque_id,
"label_translated": "Toque %d" % toque_id
} for toque_id in range(6)]
}
result_list.append(toques)
if request.query_params.get('product_type') == ProductType.WINE: if request.query_params.get('product_type') == ProductType.WINE:
wine_region_id = query_params.get('wine_region_id__in') wine_region_id = query_params.get('wine_region_id__in')
@ -129,11 +127,30 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
result_list.append(wine_regions) result_list.append(wine_regions)
for item in result_list:
if 'filters' in item:
item['filters'].sort(key=lambda x: x.get('label_translated'))
if filter_flags['toque_number']:
toques = {
"index_name": "toque_number",
"label_translated": "Toques",
"param_name": "toque_number__in",
'type': 'toque',
"filters": [{
"id": toque_id,
"index_name": "toque_%d" % toque_id,
"label_translated": "Toque %d" % toque_id
} for toque_id in range(6)]
}
result_list.append(toques)
if filter_flags['works_noon']: if filter_flags['works_noon']:
works_noon = { works_noon = {
"index_name": "works_noon", "index_name": "works_noon",
"label_translated": "Open noon", "label_translated": "Open noon",
"param_name": "works_noon__in", "param_name": "works_noon__in",
'type': 'weekday',
"filters": [{ "filters": [{
"id": weekday, "id": weekday,
"index_name": week_days[weekday].lower(), "index_name": week_days[weekday].lower(),
@ -148,6 +165,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
"index_name": "works_evening", "index_name": "works_evening",
"label_translated": "Open evening", "label_translated": "Open evening",
"param_name": "works_evening__in", "param_name": "works_evening__in",
'type': 'weekday',
"filters": [{ "filters": [{
"id": weekday, "id": weekday,
"index_name": week_days[weekday].lower(), "index_name": week_days[weekday].lower(),
@ -161,7 +179,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
"index_name": "open_now", "index_name": "open_now",
"label_translated": "Open now", "label_translated": "Open now",
"param_name": "open_now", "param_name": "open_now",
"type": True "type": 'bool',
} }
result_list.append(works_now) result_list.append(works_now)
@ -170,6 +188,7 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
"index_name": "works_at_weekday", "index_name": "works_at_weekday",
"label_translated": "Works at weekday", "label_translated": "Works at weekday",
"param_name": "works_at_weekday__in", "param_name": "works_at_weekday__in",
'type': 'weekday',
"filters": [{ "filters": [{
"id": weekday, "id": weekday,
"index_name": week_days[weekday].lower(), "index_name": week_days[weekday].lower(),
@ -180,7 +199,17 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
search_view_class = self.define_search_view_by_request(request) search_view_class = self.define_search_view_by_request(request)
facets = search_view_class.as_view({'get': 'list'})(self.mutate_request(self.request)).data['facets'] facets = search_view_class.as_view({'get': 'list'})(self.mutate_request(self.request)).data['facets']
return Response(self.remove_empty_filters(result_list, facets)) result_list = self.remove_empty_filters(result_list, facets)
tag_category = list(filter(lambda x: x.get('index_name') == 'tag', result_list))
result_list = [category for category in result_list if category.get('index_name') != 'tag']
if len(tag_category):
tag_category = list(filter(lambda x: x.get('index_name') == 'pop', tag_category[0]['filters']))
if len(tag_category): # we have Pop tag in our results
tag_category = tag_category[0]
tag_category['param_name'] = 'tags_id__in'
result_list.append(tag_category)
result_list.sort(key=lambda x: self.index_name_to_order.get(x.get('index_name'), 0), reverse=True)
return Response(result_list)
@staticmethod @staticmethod
def mutate_request(request): def mutate_request(request):
@ -217,9 +246,11 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
if facets.get('_filter_tag'): if facets.get('_filter_tag'):
tags_to_preserve = list(map(lambda el: el['key'], facets['_filter_tag']['tag']['buckets'])) tags_to_preserve = list(map(lambda el: el['key'], facets['_filter_tag']['tag']['buckets']))
if facets.get('_filter_wine_colors'): if facets.get('_filter_wine_colors'):
wine_colors_to_preserve = list(map(lambda el: el['key'], facets['_filter_wine_colors']['wine_colors']['buckets'])) wine_colors_to_preserve = list(
map(lambda el: el['key'], facets['_filter_wine_colors']['wine_colors']['buckets']))
if facets.get('_filter_wine_region_id'): if facets.get('_filter_wine_region_id'):
wine_regions_to_preserve = list(map(lambda el: el['key'], facets['_filter_wine_region_id']['wine_region_id']['buckets'])) wine_regions_to_preserve = list(
map(lambda el: el['key'], facets['_filter_wine_region_id']['wine_region_id']['buckets']))
if facets.get('_filter_toque_number'): if facets.get('_filter_toque_number'):
toque_numbers = list(map(lambda el: el['key'], facets['_filter_toque_number']['toque_number']['buckets'])) toque_numbers = list(map(lambda el: el['key'], facets['_filter_toque_number']['toque_number']['buckets']))
if facets.get('_filter_works_noon'): if facets.get('_filter_works_noon'):
@ -227,7 +258,8 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
if facets.get('_filter_works_evening'): if facets.get('_filter_works_evening'):
works_evening = list(map(lambda el: el['key'], facets['_filter_works_evening']['works_evening']['buckets'])) works_evening = list(map(lambda el: el['key'], facets['_filter_works_evening']['works_evening']['buckets']))
if facets.get('_filter_works_at_weekday'): if facets.get('_filter_works_at_weekday'):
works_at_weekday = list(map(lambda el: el['key'], facets['_filter_works_at_weekday']['works_at_weekday']['buckets'])) works_at_weekday = list(
map(lambda el: el['key'], facets['_filter_works_at_weekday']['works_at_weekday']['buckets']))
if facets.get('_filter_works_now'): if facets.get('_filter_works_now'):
works_now = list(map(lambda el: el['key'], facets['_filter_works_now']['works_now']['buckets'])) works_now = list(map(lambda el: el['key'], facets['_filter_works_now']['works_now']['buckets']))
@ -237,9 +269,11 @@ class FiltersTagCategoryViewSet(TagCategoryViewSet):
if param_name == 'tags_id__in': if param_name == 'tags_id__in':
category['filters'] = list(filter(lambda tag: tag['id'] in tags_to_preserve, category['filters'])) category['filters'] = list(filter(lambda tag: tag['id'] in tags_to_preserve, category['filters']))
elif param_name == 'wine_colors_id__in': elif param_name == 'wine_colors_id__in':
category['filters'] = list(filter(lambda tag: tag['id'] in wine_colors_to_preserve, category['filters'])) category['filters'] = list(
filter(lambda tag: tag['id'] in wine_colors_to_preserve, category['filters']))
elif param_name == 'wine_region_id__in': elif param_name == 'wine_region_id__in':
category['filters'] = list(filter(lambda tag: tag['id'] in wine_regions_to_preserve, category['filters'])) category['filters'] = list(
filter(lambda tag: tag['id'] in wine_regions_to_preserve, category['filters']))
elif param_name == 'toque_number__in': elif param_name == 'toque_number__in':
category['filters'] = list(filter(lambda tag: tag['id'] in toque_numbers, category['filters'])) category['filters'] = list(filter(lambda tag: tag['id'] in toque_numbers, category['filters']))
elif param_name == 'works_noon__in': elif param_name == 'works_noon__in':

View File

@ -13,7 +13,7 @@ class Command(BaseCommand):
'news', # перенос новостей (после №2) 'news', # перенос новостей (после №2)
'account', # №1 - перенос пользователей 'account', # №1 - перенос пользователей
'subscriber', 'subscriber',
'recipe', 'recipe', # №2 - рецепты
'partner', 'partner',
'establishment', # №3 - перенос заведений 'establishment', # №3 - перенос заведений
'gallery', 'gallery',
@ -48,7 +48,9 @@ class Command(BaseCommand):
'guide_element_types', 'guide_element_types',
'guide_elements_bulk', 'guide_elements_bulk',
'guide_element_advertorials', 'guide_element_advertorials',
'guide_element_label_photo',
'guide_complete', 'guide_complete',
'languages', # №4 - перенос языков
] ]
def handle(self, *args, **options): def handle(self, *args, **options):

View File

@ -343,7 +343,7 @@ class GuideAds(MigrateMixin):
nb_right_pages = models.IntegerField(blank=True, null=True) nb_right_pages = models.IntegerField(blank=True, null=True)
created_at = models.DateTimeField() created_at = models.DateTimeField()
updated_at = models.DateTimeField() updated_at = models.DateTimeField()
guide_ad_node_id = models.IntegerField(blank=True, null=True) guide_ad_node = models.ForeignKey('GuideElements', on_delete=models.DO_NOTHING, blank=True, null=True)
type = models.CharField(max_length=255, blank=True, null=True) type = models.CharField(max_length=255, blank=True, null=True)
class Meta: class Meta:
@ -1224,6 +1224,22 @@ class Footers(MigrateMixin):
db_table = 'footers' db_table = 'footers'
class LabelPhotos(MigrateMixin):
using = 'legacy'
guide_ad = models.ForeignKey(GuideAds, models.DO_NOTHING, blank=True, null=True)
attachment_file_name = models.CharField(max_length=255)
attachment_content_type = models.CharField(max_length=255)
attachment_file_size = models.IntegerField()
attachment_updated_at = models.DateTimeField()
attachment_suffix_url = models.CharField(max_length=255)
geometries = models.CharField(max_length=1024)
class Meta:
managed = False
db_table = 'label_photos'
class OwnershipAffs(MigrateMixin): class OwnershipAffs(MigrateMixin):
using = 'legacy' using = 'legacy'

View File

@ -77,7 +77,11 @@ class EstablishmentSerializer(serializers.ModelSerializer):
schedules = validated_data.pop('schedules') schedules = validated_data.pop('schedules')
subtypes = [validated_data.pop('subtype', None)] subtypes = [validated_data.pop('subtype', None)]
establishment = Establishment.objects.create(**validated_data) # establishment = Establishment.objects.create(**validated_data)
establishment, _ = Establishment.objects.update_or_create(
old_id=validated_data['old_id'],
defaults=validated_data,
)
if email: if email:
ContactEmail.objects.get_or_create( ContactEmail.objects.get_or_create(
email=email, email=email,

View File

@ -68,7 +68,7 @@ class GuideSerializer(TransferSerializerMixin):
class GuideFilterSerializer(TransferSerializerMixin): class GuideFilterSerializer(TransferSerializerMixin):
id = serializers.IntegerField() id = serializers.IntegerField()
year = serializers.CharField(allow_null=True) year = serializers.CharField(allow_null=True)
type = serializers.CharField(allow_null=True, source='establishment_type') establishment_type = serializers.CharField(allow_null=True)
countries = serializers.CharField(allow_null=True) countries = serializers.CharField(allow_null=True)
regions = serializers.CharField(allow_null=True) regions = serializers.CharField(allow_null=True)
subregions = serializers.CharField(allow_null=True) subregions = serializers.CharField(allow_null=True)
@ -86,7 +86,7 @@ class GuideFilterSerializer(TransferSerializerMixin):
fields = ( fields = (
'id', 'id',
'year', 'year',
'type', 'establishment_type',
'countries', 'countries',
'regions', 'regions',
'subregions', 'subregions',

View File

@ -1,55 +1,87 @@
from rest_framework import serializers from rest_framework import serializers
from account.models import User
from recipe.models import Recipe from recipe.models import Recipe
from utils.legacy_parser import parse_legacy_news_content from utils.legacy_parser import parse_legacy_news_content
class RecipeSerializer(serializers.ModelSerializer): class RecipeSerializer(serializers.Serializer):
locale = serializers.CharField() id = serializers.IntegerField()
title = serializers.CharField(allow_null=True)
summary = serializers.CharField(allow_null=True, allow_blank=True)
body = serializers.CharField(allow_null=True) body = serializers.CharField(allow_null=True)
title = serializers.CharField() locale = serializers.CharField(allow_null=True)
state = serializers.CharField() state = serializers.CharField(allow_null=True)
created_at = serializers.DateTimeField(source="published_at", format='%m-%d-%Y %H:%M:%S') slug = serializers.CharField(allow_null=True)
created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S')
class Meta: page__attachment_suffix_url = serializers.CharField(allow_null=True)
model = Recipe page__account_id = serializers.IntegerField(allow_null=True)
fields = (
"body",
"title",
"state",
"created_at",
'locale',
)
def validate(self, data): def validate(self, data):
data["state"] = self.get_state(data) data.update({
data["title"] = self.get_title(data) 'old_id': data.pop('id'),
data["description"] = self.get_description(data) 'title': self.get_title(data),
data.pop("body") 'subtitle': self.get_subtitle(data),
data.pop("locale") 'description': self.get_description(data),
'state': self.get_state(data),
'created': data.pop('created_at'),
'image': self.get_image(data),
'created_by': self.get_account(data),
'modified_by': self.get_account(data),
})
data.pop('page__account_id')
data.pop('page__attachment_suffix_url')
data.pop('summary')
data.pop('body')
data.pop('locale')
return data return data
def create(self, validated_data): def create(self, validated_data):
return Recipe.objects.create(**validated_data) obj, _ = Recipe.objects.update_or_create(
old_id=validated_data['old_id'],
defaults=validated_data,
)
return obj
def get_state(self, obj): @staticmethod
if obj["state"] == "published": def get_title(data):
return Recipe.PUBLISHED if data.get('title') and data.get('locale'):
elif obj["state"] == "hidden": return {data['locale']: data['title']}
return Recipe.HIDDEN return None
elif obj["state"] == "published_exclusive":
return Recipe.PUBLISHED_EXCLUSIVE
else:
return Recipe.WAITING
def get_title(self, obj): @staticmethod
# tit = obj.get("title") def get_subtitle(data):
# return {"en-GB": tit} if data.get('summary') and data.get('locale'):
return {obj['locale']: obj['title']} return {data['locale']: data['summary']}
return None
def get_description(self, obj): @staticmethod
# desc = obj.get("body") def get_description(data):
# return {"en-GB": desc} if data.get('body') and data.get('locale'):
content = None content = parse_legacy_news_content(data['body'])
if obj['body']: return {data['locale']: content}
content = parse_legacy_news_content(obj['body']) return None
return {obj['locale']: content}
@staticmethod
def get_state(data):
value = data.get('state')
states = {
'published': Recipe.PUBLISHED,
'hidden': Recipe.HIDDEN,
'published_exclusive': Recipe.PUBLISHED_EXCLUSIVE
}
return states.get(value, Recipe.WAITING)
@staticmethod
def get_image(data):
values = (None, 'default/missing.png')
if data.get('page__attachment_suffix_url') not in values:
return data['page__attachment_suffix_url']
return None
@staticmethod
def get_account(data):
if data.get('page__account_id'):
return User.objects.filter(old_id=data['page__account_id']).first()
return None

View File

@ -67,16 +67,23 @@ def get_default_locale():
settings.FALLBACK_LOCALE settings.FALLBACK_LOCALE
def translate_field(self, field_name): def translate_field(self, field_name, toggle_field_name=None):
def translate(self): def translate(self):
field = getattr(self, field_name) field = getattr(self, field_name)
toggler = getattr(self, toggle_field_name, None)
if isinstance(field, dict): if isinstance(field, dict):
if toggler:
field = {locale: v for locale, v in field.items() if toggler.get(locale) in [True, 'True', 'true']}
value = field.get(to_locale(get_language())) value = field.get(to_locale(get_language()))
# fallback # fallback
if value is None: if value is None:
value = field.get(get_default_locale()) value = field.get(get_default_locale())
if value is None: if value is None:
value = field.get(next(iter(field.keys()), None)) try:
value = next(iter(field.values()))
except StopIteration:
# field values are absent
return None
return value return value
return None return None
return translate return translate
@ -114,7 +121,7 @@ class TranslatedFieldsMixin:
field_name = field.name field_name = field.name
if isinstance(field, TJSONField): if isinstance(field, TJSONField):
setattr(cls, f'{field.name}_translated', setattr(cls, f'{field.name}_translated',
property(translate_field(self, field_name))) property(translate_field(self, field_name, f'locale_to_{field_name}_is_active')))
setattr(cls, f'{field_name}_indexing', setattr(cls, f'{field_name}_indexing',
property(index_field(self, field_name))) property(index_field(self, field_name)))
@ -361,6 +368,10 @@ class GMTokenGenerator(PasswordResetTokenGenerator):
class GalleryModelMixin(models.Model): class GalleryModelMixin(models.Model):
"""Mixin for models that has gallery.""" """Mixin for models that has gallery."""
class Meta:
"""Meta class."""
abstract = True
@property @property
def crop_gallery(self): def crop_gallery(self):
if hasattr(self, 'gallery'): if hasattr(self, 'gallery'):
@ -400,10 +411,6 @@ class GalleryModelMixin(models.Model):
) )
return image_property return image_property
class Meta:
"""Meta class."""
abstract = True
class IntermediateGalleryModelQuerySet(models.QuerySet): class IntermediateGalleryModelQuerySet(models.QuerySet):
"""Extended QuerySet.""" """Extended QuerySet."""

View File

@ -6,6 +6,7 @@ from django.conf import settings
from rest_framework.pagination import CursorPagination, PageNumberPagination from rest_framework.pagination import CursorPagination, PageNumberPagination
from django_elasticsearch_dsl_drf.pagination import PageNumberPagination as ESPagination from django_elasticsearch_dsl_drf.pagination import PageNumberPagination as ESPagination
class ProjectPageNumberPagination(PageNumberPagination): class ProjectPageNumberPagination(PageNumberPagination):
"""Customized pagination class.""" """Customized pagination class."""
@ -82,7 +83,7 @@ class ESDocumentPagination(ESPagination):
return page.facets._d_ return page.facets._d_
class EstablishmentPortionPagination(ProjectMobilePagination): class PortionPagination(ProjectMobilePagination):
""" """
Pagination for app establishments with limit page size equal to 12 Pagination for app establishments with limit page size equal to 12
""" """

View File

@ -11,7 +11,7 @@ urlpatterns = [
path('timetables/', include('timetable.urls.mobile')), path('timetables/', include('timetable.urls.mobile')),
# path('account/', include('account.urls.web')), # path('account/', include('account.urls.web')),
path('re_blocks/', include('advertisement.urls.mobile')), path('re_blocks/', include('advertisement.urls.mobile')),
# path('collection/', include('collection.urls.web')), # path('collection/', include('collection.urls.mobile')),
# path('establishments/', include('establishment.urls.web')), # path('establishments/', include('establishment.urls.web')),
path('news/', include('news.urls.mobile')), path('news/', include('news.urls.mobile')),
# path('partner/', include('partner.urls.web')), # path('partner/', include('partner.urls.web')),