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:
commit
3dd76503a5
|
|
@ -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()
|
||||
|
|
|
|||
34
apps/collection/migrations/0024_auto_20191213_0859.py
Normal file
34
apps/collection/migrations/0024_auto_20191213_0859.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
20
apps/collection/migrations/0025_collection_description.py
Normal file
20
apps/collection/migrations/0025_collection_description.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
14
apps/collection/migrations/0026_merge_20191217_1151.py
Normal file
14
apps/collection/migrations/0026_merge_20191217_1151.py
Normal 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 = [
|
||||
]
|
||||
27
apps/collection/migrations/0027_auto_20191218_0753.py
Normal file
27
apps/collection/migrations/0027_auto_20191218_0753.py
Normal 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',
|
||||
),
|
||||
]
|
||||
|
|
@ -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'))
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
from .back import *
|
||||
from .web import *
|
||||
from .common import *
|
||||
|
|
@ -35,7 +35,7 @@ class CollectionBackOfficeSerializer(CollectionBaseSerializer):
|
|||
'country',
|
||||
'country_id',
|
||||
# 'block_size',
|
||||
# 'description',
|
||||
'description',
|
||||
'slug',
|
||||
# 'start',
|
||||
# 'end',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
from collection.urls.common import urlpatterns as common_url_patterns
|
||||
|
||||
|
||||
app_name = 'web'
|
||||
|
||||
urlpatterns_api = []
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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', )
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
32
apps/main/migrations/0043_auto_20191217_1120.py
Normal file
32
apps/main/migrations/0043_auto_20191217_1120.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
17
apps/main/migrations/0044_auto_20191217_1125.py
Normal file
17
apps/main/migrations/0044_auto_20191217_1125.py
Normal 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',
|
||||
),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}')
|
||||
|
|
|
|||
37
apps/news/migrations/0043_auto_20191216_1920.py
Normal file
37
apps/news/migrations/0043_auto_20191216_1920.py
Normal 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),
|
||||
]
|
||||
18
apps/news/migrations/0044_auto_20191216_2044.py
Normal file
18
apps/news/migrations/0044_auto_20191216_2044.py
Normal 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',
|
||||
),
|
||||
]
|
||||
18
apps/news/migrations/0045_news_must_of_the_week.py
Normal file
18
apps/news/migrations/0045_news_must_of_the_week.py
Normal 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),
|
||||
),
|
||||
]
|
||||
38
apps/news/migrations/0046_auto_20191218_1437.py
Normal file
38
apps/news/migrations/0046_auto_20191218_1437.py
Normal 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),
|
||||
]
|
||||
17
apps/news/migrations/0047_remove_news_start.py
Normal file
17
apps/news/migrations/0047_remove_news_start.py
Normal 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',
|
||||
),
|
||||
]
|
||||
17
apps/news/migrations/0048_remove_news_must_of_the_week.py
Normal file
17
apps/news/migrations/0048_remove_news_must_of_the_week.py
Normal 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',
|
||||
),
|
||||
]
|
||||
|
|
@ -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'], ]
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
||||
|
|
|
|||
18
apps/recipe/migrations/0002_recipe_old_id.py
Normal file
18
apps/recipe/migrations/0002_recipe_old_id.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
18
apps/recipe/migrations/0003_recipe_slug.py
Normal file
18
apps/recipe/migrations/0003_recipe_slug.py
Normal 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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
]
|
||||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user