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

# Conflicts:
#	apps/location/models.py
#	apps/transfer/management/commands/transfer.py
This commit is contained in:
littlewolf 2019-12-20 11:55:23 +03:00
commit 3dd76503a5
71 changed files with 1356 additions and 220 deletions

View File

@ -1,4 +1,5 @@
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.response import Response
@ -96,6 +97,13 @@ class CreatePendingBooking(generics.CreateAPIView):
permission_classes = (permissions.AllowAny,)
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):
data = request.data.copy()
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,)
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):
instance = self.get_object()
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,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

@ -11,6 +11,7 @@ from utils.models import (
URLImageMixin,
)
from utils.querysets import RelatedObjectsCountMixin
from utils.models import IntermediateGalleryModelMixin, GalleryModelMixin
# Mixins
@ -75,9 +76,9 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
block_size = JSONField(
_('collection block properties'), null=True, blank=True,
default=None, help_text='{"width": "250px", "height":"250px"}')
# description = TJSONField(
# _('description'), null=True, blank=True,
# default=None, help_text='{"en-GB":"some text"}')
description = TJSONField(
_('description'), null=True, blank=True,
default=None, help_text='{"en-GB":"some text"}')
slug = models.SlugField(max_length=50, unique=True,
verbose_name=_('Collection slug'), editable=True, null=True)
old_id = models.IntegerField(null=True, blank=True)
@ -112,19 +113,22 @@ class Collection(ProjectBaseMixin, CollectionDateMixin,
@property
def related_object_names(self) -> list:
"""Return related object names."""
raw_object_names = {}
for related_object in [(related_object.id, related_object.name) for related_object in self._related_objects]:
instances = getattr(self, f'{related_object[1]}')
raw_objects = []
for related_object in [related_object.name for related_object in self._related_objects]:
instances = getattr(self, f'{related_object}')
if instances.exists():
for instance in instances.all():
raw_object_names[related_object[0]] = 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
related_objects = []
object_names = set()
re_pattern = r'[\w]+'
for object_id in raw_object_names:
result = re.findall(re_pattern, raw_object_names[object_id])
for object_id, raw_name, in raw_objects:
result = re.findall(re_pattern, raw_name)
if result:
name = ' '.join(result).capitalize()
if name not in object_names:
@ -207,6 +211,17 @@ class Guide(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
"""String method."""
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):
"""QuerySet for model Advertorial."""
@ -377,6 +392,9 @@ class GuideElement(ProjectBaseMixin, MPTTModel):
parent = TreeForeignKey('self', on_delete=models.CASCADE,
null=True, blank=True,
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,
verbose_name=_('old id'))

View File

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

View File

@ -35,7 +35,7 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer):
'country',
'country_id',
# 'block_size',
# 'description',
'description',
'slug',
# 'start',
# 'end',

View File

@ -2,6 +2,7 @@ from rest_framework import serializers
from collection import models
from location import models as location_models
from main.serializers import SiteShortSerializer
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"""
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:
model = models.Guide
fields = [
@ -56,4 +77,60 @@ class GuideSerializer(serializers.ModelSerializer):
'name',
'start',
'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 tqdm import tqdm
from collection.models import GuideElementSection, GuideElementSectionCategory, \
GuideWineColorSection, GuideElementType, GuideElement, \
Guide, Advertorial
from establishment.models import Establishment
from review.models import Review
from gallery.models import Image
from location.models import WineRegion, City
from product.models import Product
from review.models import Review
from transfer.models import Guides, GuideFilters, GuideSections, GuideElements, \
GuideAds
GuideAds, LabelPhotos
from transfer.serializers.guide import GuideSerializer, GuideFilterSerializer
from collection.models import GuideElementSection, GuideElementSectionCategory, \
GuideWineColorSection, GuideElementType, GuideElement, \
Guide, Advertorial
from django.db.models import Subquery
def transfer_guide():
@ -252,7 +256,7 @@ def transfer_guide_element_advertorials():
qs = GuideElement.objects.filter(old_id=old_id)
legacy_qs = GuideElements.objects.exclude(guide__isnull=True) \
.exclude(guide__title__icontains='test') \
.filter(id=guide_ad_node_id)
.filter(id=old_id)
if qs.exists() and legacy_qs.exists():
return qs.first()
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)}')
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 = {
'guides': [
transfer_guide,
@ -305,7 +358,10 @@ data_types = {
transfer_guide_elements_bulk,
],
'guide_element_advertorials': [
transfer_guide_element_advertorials
transfer_guide_element_advertorials,
],
'guide_element_label_photo': [
transfer_guide_element_label_photo,
],
'guide_complete': [
transfer_guide, # transfer guides from Guides
@ -315,5 +371,6 @@ data_types = {
transfer_guide_element_type, # partial transfer section types 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_label_photo, # transfer guide element label photos
]
}

View File

@ -1,12 +1,17 @@
"""Collection common urlpaths."""
from django.urls import path
from rest_framework.routers import SimpleRouter
from django.urls import path
from collection.views import back as views
app_name = 'collection'
router = SimpleRouter()
router.register(r'', views.CollectionBackOfficeViewSet)
urlpatterns = router.urls
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 = [
path('', views.CollectionHomePageView.as_view(), name='list'),
path('<slug:slug>/', views.CollectionDetailView.as_view(), name='detail'),
path('<slug:slug>/establishments/', views.CollectionEstablishmentListView.as_view(),
path('slug/<slug:slug>/', views.CollectionDetailView.as_view(), name='detail'),
path('slug/<slug:slug>/establishments/', views.CollectionEstablishmentListView.as_view(),
name='detail'),
path('guides/', views.GuideListView.as_view(), name='guides-list'),
path('guides/<int:pk>/', views.GuideRetrieveView.as_view(), name='guides-detail'),
]

View File

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

View File

@ -1,9 +1,11 @@
from django_filters.rest_framework import DjangoFilterBackend
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.serializers import back as serializers
from collection import models, serializers
from utils.views import BindObjectMixin
@ -22,6 +24,22 @@ class CollectionViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
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,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
@ -58,3 +76,19 @@ class CollectionBackOfficeViewSet(mixins.CreateModelMixin,
collection.establishments.remove(related_object)
elif obj_type == self.bind_object_serializer_class.PRODUCT:
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):
"""Override get_queryset method."""
return models.Collection.objects.published() \
.by_country_code(code=self.request.country_code) \
.order_by('-on_top', '-modified')
.by_country_code(code=self.request.country_code) \
.order_by('-on_top', '-created')
class GuideViewMixin(generics.GenericAPIView):
@ -39,7 +39,7 @@ class CollectionHomePageView(CollectionListView):
def get_queryset(self):
"""Override get_queryset."""
return super(CollectionHomePageView, self).get_queryset() \
.filter_all_related_gt(3)
.filter_all_related_gt(3)
class CollectionDetailView(CollectionViewMixin, generics.RetrieveAPIView):
@ -72,10 +72,10 @@ class CollectionEstablishmentListView(CollectionListView):
class GuideListView(GuideViewMixin, generics.ListAPIView):
"""List Guide view"""
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.GuideSerializer
serializer_class = serializers.GuideBaseSerializer
class GuideRetrieveView(GuideViewMixin, generics.RetrieveAPIView):
"""Retrieve Guide view"""
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 tqdm import tqdm
from establishment.models import Establishment
from transfer.models import Reviews, ReviewTexts
@ -22,7 +22,7 @@ class Command(BaseCommand):
'updated_at',
)
for r_id, establishment_id, new_date in queryset:
for r_id, establishment_id, new_date in tqdm(queryset):
try:
review_id, date = valid_reviews[establishment_id]
except KeyError:
@ -41,7 +41,7 @@ class Command(BaseCommand):
'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()
if establishment:
description = establishment.description
@ -53,7 +53,7 @@ class Command(BaseCommand):
count += 1
# Если нет 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
if len(description) and 'en-GB' not in description:
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)
))
def has_location(self):
"""Return objects with geo location."""
return self.filter(address__coordinates__isnull=False)
def similar_base(self, establishment):
"""
Return filtered QuerySet by base filters.
@ -267,25 +271,30 @@ class EstablishmentQuerySet(models.QuerySet):
else:
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.
:param slug: str artisan slug
Return QuerySet with objects that similar to Artisan/Producer(s).
:param slug: str artisan/producer slug
"""
artisan_qs = self.filter(slug=slug)
if artisan_qs.exists():
artisan = artisan_qs.first()
ids_by_subquery = self.similar_base_subquery(
establishment=artisan,
filters={
'public_mark__gte': 10,
}
)
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')
establishment_qs = self.filter(slug=slug)
if establishment_qs.exists():
establishment = establishment_qs.first()
return self.similar_base(establishment) \
.same_subtype(establishment) \
.order_by(F('same_subtype').desc(),
F('distance').asc()) \
.distinct('same_subtype', 'distance', 'id')
else:
return self.none()

View File

@ -225,18 +225,19 @@ class EstablishmentBackOfficeGallerySerializer(serializers.ModelSerializer):
'is_main',
]
def get_request_kwargs(self):
@property
def request_kwargs(self):
"""Get url kwargs from request."""
return self.context.get('request').parser_context.get('kwargs')
def validate(self, attrs):
"""Override validate method."""
establishment_pk = self.get_request_kwargs().get('pk')
establishment_slug = self.get_request_kwargs().get('slug')
establishment_pk = self.request_kwargs.get('pk')
establishment_slug = self.request_kwargs.get('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)
image_qs = Image.objects.filter(id=image_id)

View File

@ -17,12 +17,14 @@ urlpatterns = [
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
name='create-destroy-favorites'),
# similar establishments
# similar establishments by type/subtype
path('slug/<slug:slug>/similar/', views.RestaurantSimilarListView.as_view(),
name='similar-restaurants'),
path('slug/<slug:slug>/similar/wineries/', views.WinerySimilarListView.as_view(),
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'),
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 establishment import filters, models, serializers
from main import methods
from utils.pagination import EstablishmentPortionPagination
from utils.pagination import PortionPagination
from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
@ -41,6 +41,12 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
.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):
"""Resource for getting a establishment."""
@ -61,7 +67,7 @@ class EstablishmentMobileRetrieveView(EstablishmentRetrieveView):
class EstablishmentRecentReviewListView(EstablishmentListView):
"""List view for last reviewed establishments."""
pagination_class = EstablishmentPortionPagination
pagination_class = PortionPagination
def get_queryset(self):
"""Overridden method 'get_queryset'."""
@ -77,37 +83,34 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
return qs.last_reviewed(point=point)
class EstablishmentSimilarList(EstablishmentListView):
"""Resource for getting a list of similar establishments."""
serializer_class = serializers.EstablishmentSimilarSerializer
pagination_class = EstablishmentPortionPagination
class RestaurantSimilarListView(EstablishmentSimilarList):
class RestaurantSimilarListView(EstablishmentSimilarView):
"""Resource for getting a list of similar restaurants."""
def get_queryset(self):
"""Overridden get_queryset method"""
return EstablishmentMixinView.get_queryset(self) \
.has_location() \
.similar_restaurants(slug=self.kwargs.get('slug'))
class WinerySimilarListView(EstablishmentSimilarList):
class WinerySimilarListView(EstablishmentSimilarView):
"""Resource for getting a list of similar wineries."""
def get_queryset(self):
"""Overridden get_queryset method"""
return EstablishmentMixinView.get_queryset(self) \
.has_location() \
.similar_wineries(slug=self.kwargs.get('slug'))
class ArtisanSimilarListView(EstablishmentSimilarList):
"""Resource for getting a list of similar artisans."""
class ArtisanProducerSimilarListView(EstablishmentSimilarView):
"""Resource for getting a list of similar artisan/producer(s)."""
def get_queryset(self):
"""Overridden get_queryset method"""
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):

View File

@ -22,3 +22,34 @@ class CityBackFilter(filters.FilterSet):
if value not in EMPTY_VALUES:
return queryset.search_by_name(value)
return queryset
class RegionFilter(filters.FilterSet):
"""Region filter set."""
country_id = filters.CharFilter()
sub_regions_by_region_id = filters.CharFilter(method='by_region')
without_parent_region = filters.BooleanFilter(method='by_parent_region')
class Meta:
"""Meta class."""
model = models.Region
fields = (
'country_id',
'sub_regions_by_region_id',
'without_parent_region',
)
def by_region(self, queryset, name, value):
"""Search regions by sub region id."""
if value not in EMPTY_VALUES:
return queryset.sub_regions_by_region_id(value)
def by_parent_region(self, queryset, name, value):
"""
Search if region instance has a parent region..
If True then show only Regions
Otherwise show only Sub regions.
"""
if value not in EMPTY_VALUES:
return queryset.without_parent_region(value)

View File

@ -76,7 +76,28 @@ class Country(RelatedInstanceMixin, TranslatedFieldsMixin,
return self.id
class Region(RelatedInstanceMixin, models.Model):
class RegionQuerySet(models.QuerySet):
"""QuerySet for model Region."""
def without_parent_region(self, switcher: bool = True):
"""Filter regions by parent region."""
return self.filter(parent_region__isnull=switcher)
def by_region_id(self, region_id):
"""Filter regions by region id."""
return self.filter(id=region_id)
def by_sub_region_id(self, sub_region_id):
"""Filter sub regions by sub region id."""
return self.filter(parent_region_id=sub_region_id)
def sub_regions_by_region_id(self, region_id):
"""Filter regions by sub region id."""
return self.filter(parent_region_id=region_id)
class Region(models.Model):
"""Region model."""
name = models.CharField(_('name'), max_length=250)
@ -89,6 +110,8 @@ class Region(RelatedInstanceMixin, models.Model):
old_id = models.IntegerField(null=True, blank=True, default=None)
mysql_ids = ArrayField(models.IntegerField(), blank=True, null=True)
objects = RegionQuerySet.as_manager()
class Meta:
"""Meta class."""

View File

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

View File

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

View File

@ -8,9 +8,11 @@ from utils.views import CreateDestroyGalleryViewMixin
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from django.shortcuts import get_object_or_404
from utils.serializers import ImageBaseSerializer
from location.filters import RegionFilter
from location import filters
# Address
@ -18,29 +20,36 @@ class AddressListCreateView(common.AddressViewMixin, generics.ListCreateAPIView)
"""Create view for model Address."""
serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
class AddressRUDView(common.AddressViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Address."""
serializer_class = serializers.AddressDetailSerializer
queryset = models.Address.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
# City
class CityListCreateView(common.CityViewMixin, generics.ListCreateAPIView):
"""Create view for model City."""
serializer_class = serializers.CitySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
queryset = models.City.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
# queryset = models.City.objects.all()
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):
"""Create view for model City."""
serializer_class = serializers.CitySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
queryset = models.City.objects.all()
filter_class = filters.CityBackFilter
pagination_class = None
@ -49,14 +58,14 @@ class CityListSearchView(common.CityViewMixin, generics.ListCreateAPIView):
class CityRUDView(common.CityViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model City."""
serializer_class = serializers.CitySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
class CityGalleryCreateDestroyView(common.CityViewMixin,
CreateDestroyGalleryViewMixin):
"""Resource for a create gallery for product for back-office users."""
serializer_class = serializers.CityGallerySerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
def get_object(self):
"""
@ -77,7 +86,7 @@ class CityGalleryListView(common.CityViewMixin,
generics.ListAPIView):
"""Resource for returning gallery for product for back-office users."""
serializer_class = ImageBaseSerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
def get_object(self):
"""Override get_object method."""
@ -99,13 +108,15 @@ class RegionListCreateView(common.RegionViewMixin, generics.ListCreateAPIView):
"""Create view for model Region"""
pagination_class = None
serializer_class = serializers.RegionSerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
ordering_fields = '__all__'
filter_class = RegionFilter
class RegionRUDView(common.RegionViewMixin, generics.RetrieveUpdateDestroyAPIView):
"""Retrieve view for model Region"""
serializer_class = serializers.RegionSerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
# Country
@ -114,11 +125,11 @@ class CountryListCreateView(generics.ListCreateAPIView):
queryset = models.Country.objects.all()
serializer_class = serializers.CountryBackSerializer
pagination_class = None
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
class CountryRUDView(generics.RetrieveUpdateDestroyAPIView):
"""RUD view for model Country."""
serializer_class = serializers.CountryBackSerializer
permission_classes = [IsAuthenticatedOrReadOnly|IsCountryAdmin]
queryset = models.Country.objects.all()
permission_classes = [IsAuthenticatedOrReadOnly | IsCountryAdmin]
queryset = models.Country.objects.all()

View File

@ -11,7 +11,7 @@ class SiteSettingsInline(admin.TabularInline):
@admin.register(models.SiteSettings)
class SiteSettingsAdmin(admin.ModelAdmin):
"""Site settings admin conf."""
inlines = [SiteSettingsInline,]
inlines = [SiteSettingsInline, ]
@admin.register(models.Feature)
@ -54,3 +54,23 @@ class PageAdmin(admin.ModelAdmin):
list_display = ('id', '__str__', 'advertisement')
list_filter = ('advertisement__url', 'source')
date_hierarchy = 'created'
@admin.register(models.Footer)
class FooterAdmin(admin.ModelAdmin):
"""Footer admin."""
list_display = ('id', 'site', )
@admin.register(models.FooterLink)
class FooterLinkAdmin(admin.ModelAdmin):
"""FooterLink admin."""
@admin.register(models.Panel)
class PanelAdmin(admin.ModelAdmin):
"""Panel admin."""
list_display = ('id', 'name', 'user', 'created', )
raw_id_fields = ('user', )
list_display_links = ('id', 'name', )

View File

@ -37,6 +37,13 @@ def determine_country_code(request):
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):
META = request.META
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

@ -6,14 +6,18 @@ from django.contrib.contenttypes import fields as generic
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import JSONField
from django.core.validators import EMPTY_VALUES
from django.db import connections, connection
from django.db import models
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from rest_framework import exceptions
from configuration.models import TranslationSettings
from location.models import Country
from main import methods
from review.models import Review
from utils.exceptions import UnprocessableEntityError
from utils.methods import dictfetchall
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, PlatformMixin)
@ -279,6 +283,11 @@ class Carousel(models.Model):
@property
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'):
return self.content_object.slug
@ -354,6 +363,11 @@ class PageType(ProjectBaseMixin):
return self.name
class FooterLink(ProjectBaseMixin):
link = models.URLField(_('link'))
title = models.CharField(_('title'), max_length=255)
class Footer(ProjectBaseMixin):
site = models.ForeignKey(
'main.SiteSettings', related_name='footers', verbose_name=_('footer'),
@ -361,6 +375,7 @@ class Footer(ProjectBaseMixin):
)
about_us = models.TextField(_('about_us'))
copyright = models.TextField(_('copyright'))
links = models.ManyToManyField(FooterLink, verbose_name=_('links'), related_name='link_footer')
class PanelQuerySet(models.QuerySet):
@ -402,5 +417,85 @@ class Panel(ProjectBaseMixin):
def __str__(self):
return self.name
def execute_query(self):
pass
def execute_query(self, request):
"""Execute query"""
raw = self.query
page = int(request.query_params.get('page', 0))
page_size = int(request.query_params.get('page_size', 10))
if raw:
data = {
"count": 0,
"next": 2,
"previous": None,
"columns": None,
"results": []
}
with connections['default'].cursor() as cursor:
count = self._raw_count(raw)
start = page*page_size
cursor.execute(*self.set_limits(start, page_size))
data["count"] = count
data["next"] = self.get_next_page(count, page, page_size)
data["previous"] = self.get_previous_page(count, page)
data["results"] = dictfetchall(cursor)
data["columns"] = self._raw_columns(cursor)
return data
def get_next_page(self, count, page, page_size):
max_page = count/page_size-1
if not 0 <= page <= max_page:
raise exceptions.NotFound('Invalid page.')
if max_page > page:
return page + 1
return None
def get_previous_page(self, count, page):
if page > 0:
return page - 1
return None
@staticmethod
def _raw_execute(row):
with connections['default'].cursor() as cursor:
try:
cursor.execute(row)
return cursor.execute(row)
except Exception as er:
# TODO: log
raise UnprocessableEntityError()
def _raw_count(self, subquery):
if ';' in subquery:
subquery = subquery.replace(';', '')
_count_query = f"""SELECT count(*) from ({subquery}) as t;"""
# cursor = self._raw_execute(_count_query)
with connections['default'].cursor() as cursor:
cursor.execute(_count_query)
row = cursor.fetchone()
return row[0]
@staticmethod
def _raw_columns(cursor):
columns = [col[0] for col in cursor.description]
return columns
def _raw_page(self, raw, request):
page = request.query_params.get('page', 0)
page_size = request.query_params.get('page_size', 0)
raw = f"""{raw} LIMIT {page_size} OFFSET {page}"""
return raw
def set_limits(self, start, limit, params=tuple()):
limit_offset = ''
new_params = tuple()
if start > 0:
new_params += (start,)
limit_offset = ' OFFSET %s'
if limit is not None:
new_params = (limit,) + new_params
limit_offset = ' LIMIT %s' + limit_offset
params = params + new_params
query = self.query + limit_offset
return query, params

View File

@ -296,3 +296,20 @@ class PanelSerializer(serializers.ModelSerializer):
'user',
'user_id'
]
class PanelExecuteSerializer(serializers.ModelSerializer):
"""Panel execute serializer."""
class Meta:
model = models.Panel
fields = [
'id',
'name',
'display',
'description',
'query',
'created',
'modified',
'user',
'user_id'
]

View File

@ -23,8 +23,8 @@ urlpatterns = [
path('page-types/', views.PageTypeListCreateView.as_view(),
name='page-types-list-create'),
path('panels/', views.PanelsListCreateView.as_view(), name='panels'),
path('panels/<int:pk>/', views.PanelsListCreateView.as_view(), name='panels-rud'),
# path('panels/<int:pk>/execute/', views.PanelsView.as_view(), name='panels-execute')
path('panels/<int:pk>/', views.PanelsRUDView.as_view(), name='panels-rud'),
path('panels/<int:pk>/execute/', views.PanelsExecuteView.as_view(), name='panels-execute')
]

View File

@ -1,6 +1,8 @@
from django.contrib.contenttypes.models import ContentType
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions
from rest_framework.generics import get_object_or_404
from rest_framework.response import Response
from main import serializers
from main.filters import AwardFilter
@ -106,4 +108,16 @@ class PanelsRUDView(generics.RetrieveUpdateDestroyAPIView):
permissions.IsAdminUser,
)
serializer_class = serializers.PanelSerializer
queryset = Panel.objects.all()
queryset = Panel.objects.all()
class PanelsExecuteView(generics.ListAPIView):
"""Custom panels view."""
permission_classes = (
permissions.IsAdminUser,
)
queryset = Panel.objects.all()
def list(self, request, *args, **kwargs):
panel = get_object_or_404(Panel, id=self.kwargs['pk'])
return Response(panel.execute_query(request))

View File

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

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."""
import uuid
from django.conf import settings
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.models import Case, When
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from rest_framework.reverse import reverse
from main.models import Carousel
from rating.models import Rating, ViewCount
from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin, HasTagsMixin,
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin,
FavoritesMixin)
from utils.querysets import TranslationQuerysetMixin
from django.conf import settings
from django.contrib.postgres.fields import HStoreField
from datetime import datetime
class Agenda(ProjectBaseMixin, TranslatedFieldsMixin):
@ -62,7 +67,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
def sort_by_start(self):
"""Return qs sorted by start DESC"""
return self.order_by('-start')
return self.order_by('-publication_date', '-publication_time')
def rating_value(self):
return self.annotate(rating=models.Count('ratings__ip', distinct=True))
@ -97,9 +102,13 @@ class NewsQuerySet(TranslationQuerysetMixin):
def published(self):
"""Return only published news"""
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)),
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 country?
@ -112,7 +121,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
return self.model.objects.exclude(pk=news.pk).published(). \
annotate_in_favorites(user). \
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):
"""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,
FavoritesMixin):
@ -170,20 +182,25 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
verbose_name=_('title'),
help_text='{"en-GB":"some text"}')
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,
verbose_name=_('subtitle'),
help_text='{"en-GB":"some text"}')
description = TJSONField(blank=True, null=True, default=None,
verbose_name=_('description'),
help_text='{"en-GB":"some text"}')
start = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('Start'))
locale_to_description_is_active = HStoreField(null=True, default=dict, blank=True,
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,
verbose_name=_('End'))
slugs = HStoreField(null=True, blank=True, default=None,
verbose_name=_('Slugs for current news obj'),
help_text='{"en-GB":"some slug"}')
slugs = HStoreField(null=True, blank=True, default=dict,
verbose_name=_('Slugs for current news obj'),
help_text='{"en-GB":"some slug"}')
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
verbose_name=_('State'))
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'))
duplication_date = models.DateTimeField(blank=True, null=True, default=None,
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()
class Meta:
@ -233,6 +252,34 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
self.duplication_date = timezone.now()
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
def is_publish(self):
return self.state in self.PUBLISHED_STATES
@ -317,7 +364,6 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
class NewsGallery(IntermediateGalleryModelMixin):
news = models.ForeignKey(News, null=True,
related_name='news_gallery',
on_delete=models.CASCADE,
@ -331,4 +377,4 @@ class NewsGallery(IntermediateGalleryModelMixin):
"""NewsGallery meta class."""
verbose_name = _('news gallery')
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 gallery.models import Image
from main.models import SiteSettings
from main.models import SiteSettings, Carousel
from location import models as location_models
from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
from news import models
@ -128,6 +128,7 @@ class NewsDetailSerializer(NewsBaseSerializer):
state_display = serializers.CharField(source='get_state_display',
read_only=True)
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
start = serializers.DateTimeField(source='publication_datetime', read_only=True)
class Meta(NewsBaseSerializer.Meta):
"""Meta class."""
@ -182,12 +183,17 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
'backoffice_title',
'subtitle',
'slugs',
'locale_to_description_is_active',
'is_published',
'duplication_date',
'must_of_the_week',
'publication_date',
'publication_time',
)
extra_kwargs = {
'backoffice_title': {'allow_null': False},
'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):
@ -209,6 +215,20 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
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,
NewsDetailSerializer):
"""News detail serializer for back-office users."""
@ -224,6 +244,7 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
queryset=SiteSettings.objects.all())
template_display = serializers.CharField(source='get_template_display',
read_only=True)
duplicates = NewsBackOfficeDuplicationInfoSerializer(many=True, allow_null=True, read_only=True)
class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta):
"""Meta class."""
@ -237,6 +258,7 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer,
'template',
'template_display',
'is_international',
'duplicates',
)
@ -252,13 +274,14 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
'is_main',
]
def get_request_kwargs(self):
@property
def request_kwargs(self):
"""Get url kwargs from request."""
return self.context.get('request').parser_context.get('kwargs')
def create(self, validated_data):
news_pk = self.get_request_kwargs().get('pk')
image_id = self.get_request_kwargs().get('image_id')
news_pk = self.request_kwargs.get('pk')
image_id = self.request_kwargs.get('image_id')
qs = models.NewsGallery.objects.filter(image_id=image_id, news_id=news_pk)
instance = qs.first()
if instance:
@ -268,8 +291,8 @@ class NewsBackOfficeGallerySerializer(serializers.ModelSerializer):
def validate(self, attrs):
"""Override validate method."""
news_pk = self.get_request_kwargs().get('pk')
image_id = self.get_request_kwargs().get('image_id')
news_pk = self.request_kwargs.get('pk')
image_id = self.request_kwargs.get('image_id')
news_qs = models.News.objects.filter(pk=news_pk)
image_qs = Image.objects.filter(id=image_id)
@ -337,7 +360,12 @@ class NewsCarouselCreateSerializer(CarouselCreateSerializer):
def create(self, validated_data, *args, **kwargs):
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)
@ -347,9 +375,11 @@ class NewsCloneCreateSerializer(NewsBackOfficeBaseSerializer,
"""Serializer for creating news clone."""
template_display = serializers.CharField(source='get_template_display',
read_only=True)
duplicates = NewsBackOfficeDuplicationInfoSerializer(many=True, allow_null=True, read_only=True)
class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta):
fields = NewsBackOfficeBaseSerializer.Meta.fields + NewsDetailSerializer.Meta.fields + (
'template_display',
'duplicates',
)
read_only_fields = fields
@ -359,5 +389,5 @@ class NewsCloneCreateSerializer(NewsBackOfficeBaseSerializer,
new_country = get_object_or_404(location_models.Country, code=kwargs['country_code'])
view_count_model = rating_models.ViewCount.objects.create(count=0)
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(),
name='gallery-create-destroy'),
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,6 +1,7 @@
"""News app views."""
from django.conf import settings
from django.shortcuts import get_object_or_404
from django.utils import translation
from rest_framework import generics, permissions, response
from news import filters, models, serializers
@ -21,7 +22,7 @@ class NewsMixinView:
qs = models.News.objects.published() \
.with_base_related() \
.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
if country_code:
@ -29,6 +30,11 @@ class NewsMixinView:
qs = qs.international_news()
else:
qs = qs.by_country_code(country_code)
# locale = kwargs.get('locale')
# if locale:
# qs = qs.by_locale(locale)
return qs
def get_object(self):
@ -43,8 +49,13 @@ class NewsListView(NewsMixinView, generics.ListAPIView):
filter_class = filters.NewsListFilterSet
def get_queryset(self, *args, **kwargs):
kwargs.update({'international_preferred': True})
return super().get_queryset(*args, **kwargs)
locale = translation.get_language()
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):
@ -181,6 +192,6 @@ class NewsCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
class NewsCloneView(generics.CreateAPIView):
"""View for creating clone News"""
permission_classes = (permissions.AllowAny, )
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.NewsCloneCreateSerializer
queryset = models.News.objects.all()

View File

@ -1,13 +1,17 @@
"""Product app models."""
from django.conf import settings
from django.contrib.contenttypes import fields as generic
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.validators import MaxValueValidator, MinValueValidator
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 location.models import WineOriginAddressMixin
from review.models import Review
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
TranslatedFieldsMixin, TJSONField, FavoritesMixin,
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,
HasTagsMixin, FavoritesMixin):

View File

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

View File

@ -218,4 +218,4 @@ class ProductCommentCreateSerializer(CommentSerializer):
'user': self.context.get('request').user,
'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'),
path('slug/<slug:slug>/comments/<int:comment_id>/', views.ProductCommentRUDView.as_view(),
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 comment.serializers import CommentRUDSerializer
from utils.views import FavoritesCreateDestroyMixinView
from utils.pagination import PortionPagination
class ProductBaseView(generics.GenericAPIView):
@ -31,6 +32,12 @@ class ProductListView(ProductBaseView, generics.ListAPIView):
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):
"""Detail view fro model Product."""
lookup_field = 'slug'
@ -81,3 +88,14 @@ class ProductCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
self.check_object_permissions(self.request, 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,
output_field=models.BooleanField(default=False)))
def by_locale(self, locale):
return self.filter(title__icontains=locale)
class Recipe(TranslatedFieldsMixin, ImageMixin, BaseAttributes):
"""Recipe model."""
@ -43,22 +46,19 @@ class Recipe(TranslatedFieldsMixin, ImageMixin, BaseAttributes):
STR_FIELD_NAME = 'title'
title = TJSONField(blank=True, null=True, default=None, verbose_name=_('Title'),
help_text='{"en-GB": "some text"}')
title = TJSONField(blank=True, null=True, default=None, verbose_name=_('Title'), help_text='{"en-GB": "some text"}')
subtitle = TJSONField(blank=True, null=True, default=None, verbose_name=_('Subtitle'),
help_text='{"en-GB": "some text"}')
description = TJSONField(blank=True, null=True, default=None, verbose_name=_('Description'),
help_text='{"en-GB": "some text"}')
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES,
verbose_name=_('State'))
author = models.CharField(max_length=255, blank=True, null=True, default=None,
verbose_name=_('Author'))
published_at = models.DateTimeField(verbose_name=_('Published at'),
blank=True, default=None, null=True,
state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES, verbose_name=_('State'))
author = models.CharField(max_length=255, blank=True, null=True, default=None, verbose_name=_('Author'))
published_at = models.DateTimeField(verbose_name=_('Published at'), blank=True, default=None, null=True,
help_text=_('Published at'))
published_scheduled_at = models.DateTimeField(verbose_name=_('Published scheduled at'),
blank=True, default=None, null=True,
help_text=_('Published scheduled at'))
published_scheduled_at = models.DateTimeField(verbose_name=_('Published scheduled at'), blank=True, default=None,
null=True, 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()

View File

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

View File

@ -1,19 +1,49 @@
from django.db.models import Value, IntegerField, F
from pprint import pprint
from django.db.models import Count
from recipe.models import Recipe
from transfer.models import PageTexts
from transfer.serializers.recipe import RecipeSerializer
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():
serialized_data.save()
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 = {
"recipe": [transfer_recipe]
'recipe': [transfer_recipe]
}

View File

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

View File

@ -127,8 +127,10 @@ def transfer_product_reviews():
data_types = {
"languages": [
transfer_languages,
],
"overlook": [
# transfer_languages,
transfer_reviews,
transfer_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 search_indexes.utils import OBJECT_FIELD_PROPERTIES
from news import models
from json import dumps
NewsIndex = Index(settings.ELASTICSEARCH_INDEX_NAMES.get(__name__, 'news'))
@ -17,7 +18,7 @@ class NewsDocument(Document):
'name': fields.KeywordField()})
title = fields.ObjectField(attr='title_indexing',
properties=OBJECT_FIELD_PROPERTIES)
slugs = fields.ObjectField(properties=OBJECT_FIELD_PROPERTIES)
slugs = fields.KeywordField()
backoffice_title = fields.TextField(analyzer='english')
subtitle = fields.ObjectField(attr='subtitle_indexing',
properties=OBJECT_FIELD_PROPERTIES)
@ -44,10 +45,11 @@ class NewsDocument(Document):
},
multi=True)
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):
return {locale: instance.slugs.get(locale) for locale in OBJECT_FIELD_PROPERTIES}
return dumps(instance.slugs or {})
class Django:

View File

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

View File

@ -1,13 +1,15 @@
"""Search indexes app views."""
from rest_framework import permissions
from django_elasticsearch_dsl_drf import constants
from django_elasticsearch_dsl_drf.filter_backends import (
FilteringFilterBackend,
GeoSpatialOrderingFilterBackend,
OrderingFilterBackend,
)
from elasticsearch_dsl import TermsFacet
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.documents import EstablishmentDocument, NewsDocument
from search_indexes.documents.product import ProductDocument
@ -17,6 +19,11 @@ from utils.pagination import ESDocumentPagination
class NewsDocumentViewSet(BaseDocumentViewSet):
"""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
lookup_field = 'slug'
pagination_class = ESDocumentPagination
@ -341,6 +348,12 @@ class ProductDocumentViewSet(BaseDocumentViewSet):
# GeoSpatialOrderingFilterBackend,
]
def get_queryset(self):
qs = super(ProductDocumentViewSet, self).get_queryset()
qs = qs.filter('match', state=Product.PUBLISHED)
return qs
ordering_fields = {
'created': {
'field': 'created',

View File

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

View File

@ -343,7 +343,7 @@ class GuideAds(MigrateMixin):
nb_right_pages = models.IntegerField(blank=True, null=True)
created_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)
class Meta:
@ -1224,6 +1224,22 @@ class Footers(MigrateMixin):
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):
using = 'legacy'
@ -1239,6 +1255,7 @@ class OwnershipAffs(MigrateMixin):
managed = False
db_table = 'ownership_affs'
class Panels(MigrateMixin):
using = 'legacy'

View File

@ -77,7 +77,11 @@ class EstablishmentSerializer(serializers.ModelSerializer):
schedules = validated_data.pop('schedules')
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:
ContactEmail.objects.get_or_create(
email=email,

View File

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

View File

@ -1,55 +1,87 @@
from rest_framework import serializers
from account.models import User
from recipe.models import Recipe
from utils.legacy_parser import parse_legacy_news_content
class RecipeSerializer(serializers.ModelSerializer):
locale = serializers.CharField()
class RecipeSerializer(serializers.Serializer):
id = serializers.IntegerField()
title = serializers.CharField(allow_null=True)
summary = serializers.CharField(allow_null=True, allow_blank=True)
body = serializers.CharField(allow_null=True)
title = serializers.CharField()
state = serializers.CharField()
created_at = serializers.DateTimeField(source="published_at", format='%m-%d-%Y %H:%M:%S')
class Meta:
model = Recipe
fields = (
"body",
"title",
"state",
"created_at",
'locale',
)
locale = serializers.CharField(allow_null=True)
state = serializers.CharField(allow_null=True)
slug = serializers.CharField(allow_null=True)
created_at = serializers.DateTimeField(format='%m-%d-%Y %H:%M:%S')
page__attachment_suffix_url = serializers.CharField(allow_null=True)
page__account_id = serializers.IntegerField(allow_null=True)
def validate(self, data):
data["state"] = self.get_state(data)
data["title"] = self.get_title(data)
data["description"] = self.get_description(data)
data.pop("body")
data.pop("locale")
data.update({
'old_id': data.pop('id'),
'title': self.get_title(data),
'subtitle': self.get_subtitle(data),
'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
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):
if obj["state"] == "published":
return Recipe.PUBLISHED
elif obj["state"] == "hidden":
return Recipe.HIDDEN
elif obj["state"] == "published_exclusive":
return Recipe.PUBLISHED_EXCLUSIVE
else:
return Recipe.WAITING
@staticmethod
def get_title(data):
if data.get('title') and data.get('locale'):
return {data['locale']: data['title']}
return None
def get_title(self, obj):
# tit = obj.get("title")
# return {"en-GB": tit}
return {obj['locale']: obj['title']}
@staticmethod
def get_subtitle(data):
if data.get('summary') and data.get('locale'):
return {data['locale']: data['summary']}
return None
def get_description(self, obj):
# desc = obj.get("body")
# return {"en-GB": desc}
content = None
if obj['body']:
content = parse_legacy_news_content(obj['body'])
return {obj['locale']: content}
@staticmethod
def get_description(data):
if data.get('body') and data.get('locale'):
content = parse_legacy_news_content(data['body'])
return {data['locale']: content}
return None
@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

@ -171,3 +171,11 @@ class RemovedBindingObjectNotFound(serializers.ValidationError):
"""The exception must be thrown if the object not found."""
default_detail = _('Removed binding object not found.')
class UnprocessableEntityError(exceptions.APIException):
"""
The exception should be thrown when executing data on server rise error.
"""
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
default_detail = _('Unprocessable entity valid.')

View File

@ -132,3 +132,12 @@ def namedtuplefetchall(cursor):
desc = cursor.description
nt_result = namedtuple('Result', [col[0] for col in desc])
return [nt_result(*row) for row in cursor.fetchall()]
def dictfetchall(cursor):
"Return all rows from a cursor as a dict"
columns = [col[0] for col in cursor.description]
return [
dict(zip(columns, row))
for row in cursor.fetchall()
]

View File

@ -67,16 +67,23 @@ def get_default_locale():
settings.FALLBACK_LOCALE
def translate_field(self, field_name):
def translate_field(self, field_name, toggle_field_name=None):
def translate(self):
field = getattr(self, field_name)
toggler = getattr(self, toggle_field_name, None)
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()))
# fallback
if value is None:
value = field.get(get_default_locale())
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 None
return translate
@ -114,7 +121,7 @@ class TranslatedFieldsMixin:
field_name = field.name
if isinstance(field, TJSONField):
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',
property(index_field(self, field_name)))
@ -361,6 +368,10 @@ class GMTokenGenerator(PasswordResetTokenGenerator):
class GalleryModelMixin(models.Model):
"""Mixin for models that has gallery."""
class Meta:
"""Meta class."""
abstract = True
@property
def crop_gallery(self):
if hasattr(self, 'gallery'):
@ -400,10 +411,6 @@ class GalleryModelMixin(models.Model):
)
return image_property
class Meta:
"""Meta class."""
abstract = True
class IntermediateGalleryModelQuerySet(models.QuerySet):
"""Extended QuerySet."""

View File

@ -6,6 +6,7 @@ from django.conf import settings
from rest_framework.pagination import CursorPagination, PageNumberPagination
from django_elasticsearch_dsl_drf.pagination import PageNumberPagination as ESPagination
class ProjectPageNumberPagination(PageNumberPagination):
"""Customized pagination class."""
@ -82,7 +83,7 @@ class ESDocumentPagination(ESPagination):
return page.facets._d_
class EstablishmentPortionPagination(ProjectMobilePagination):
class PortionPagination(ProjectMobilePagination):
"""
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('account/', include('account.urls.web')),
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('news/', include('news.urls.mobile')),
# path('partner/', include('partner.urls.web')),