merge with debelop

This commit is contained in:
Dmitriy Kuzmenko 2019-12-13 15:47:05 +03:00
commit 6e72c7c0e4
15 changed files with 275 additions and 80 deletions

View File

@ -213,7 +213,13 @@ class EstablishmentQuerySet(models.QuerySet):
))
def similar_base(self, establishment):
"""
Return filtered QuerySet by base filters.
Filters including:
1 Filter by type (and subtype) establishment.
2 Filter by published Review.
3 With annotated distance.
"""
filters = {
'reviews__status': Review.READY,
'establishment_type': establishment.establishment_type,
@ -224,27 +230,64 @@ class EstablishmentQuerySet(models.QuerySet):
.filter(**filters) \
.annotate_distance(point=establishment.location)
def similar_base_subquery(self, establishment, filters: dict) -> Subquery:
"""
Return filtered Subquery object by filters.
Filters including:
1 Filter by transmitted filters.
2 With ordering by distance.
"""
return Subquery(
self.similar_base(establishment)
.filter(**filters)
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
.values('id')
)
def similar_restaurants(self, slug):
"""
Return QuerySet with objects that similar to Restaurant.
:param restaurant_slug: str Establishment slug
:param slug: str restaurant slug
"""
restaurant_qs = self.filter(slug=slug,
public_mark__isnull=False)
restaurant_qs = self.filter(slug=slug)
if restaurant_qs.exists():
establishment = restaurant_qs.first()
subquery_filter_by_distance = Subquery(
self.similar_base(establishment)
.filter(public_mark__gte=10,
establishment_gallery__is_main=True)
.order_by('distance')[:settings.LIMITING_QUERY_OBJECTS]
.values('id')
restaurant = restaurant_qs.first()
ids_by_subquery = self.similar_base_subquery(
establishment=restaurant,
filters={
'public_mark__gte': 10,
'establishment_gallery__is_main': True,
}
)
return self.filter(id__in=subquery_filter_by_distance) \
return self.filter(id__in=ids_by_subquery) \
.annotate_intermediate_public_mark() \
.annotate_mark_similarity(mark=establishment.public_mark) \
.annotate_mark_similarity(mark=restaurant.public_mark) \
.order_by('mark_similarity') \
.distinct('mark_similarity', 'id')
else:
return self.none()
def similar_artisans(self, slug):
"""
Return QuerySet with objects that similar to Artisan.
:param slug: str artisan 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')
else:
return self.none()
def by_wine_region(self, wine_region):
"""

View File

@ -21,6 +21,8 @@ urlpatterns = [
path('slug/<slug:slug>/similar/', views.RestaurantSimilarListView.as_view(),
name='similar-restaurants'),
path('slug/<slug:slug>/similar/wineries/', views.WinerySimilarListView.as_view(),
name='similar-restaurants'),
name='similar-wineries'),
path('slug/<slug:slug>/similar/artisans/', views.ArtisanSimilarListView.as_view(),
name='similar-artisans'),
]

View File

@ -1,15 +1,17 @@
"""Establishment app views."""
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions
from rest_framework import status
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import generics, permissions, status
from account.models import User
from establishment import filters, models, serializers
from timetable.models import Timetable
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
from utils.permissions import IsCountryAdmin, IsEstablishmentManager, IsWineryReviewer
from utils.views import CreateDestroyGalleryViewMixin
from timetable.models import Timetable
from rest_framework import status
from rest_framework.response import Response
class EstablishmentMixinViews:
@ -41,7 +43,7 @@ class EstablishmentScheduleRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment schedule RUD view"""
lookup_field = 'slug'
serializer_class = ScheduleRUDSerializer
permission_classes = [IsWineryReviewer |IsEstablishmentManager]
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
def get_object(self):
"""
@ -75,6 +77,11 @@ class MenuListCreateView(generics.ListCreateAPIView):
serializer_class = serializers.MenuSerializers
queryset = models.Menu.objects.all()
permission_classes = [IsWineryReviewer | IsEstablishmentManager]
filter_backends = (DjangoFilterBackend,)
filterset_fields = (
'establishment',
'establishment__slug',
)
class MenuRUDView(generics.RetrieveUpdateDestroyAPIView):
@ -161,7 +168,7 @@ class EmailRUDView(generics.RetrieveUpdateDestroyAPIView):
class EmployeeListCreateView(generics.ListCreateAPIView):
"""Emplyoee list create view."""
permission_classes = (permissions.AllowAny, )
permission_classes = (permissions.AllowAny,)
filter_class = filters.EmployeeBackFilter
serializer_class = serializers.EmployeeBackSerializers
queryset = models.Employee.objects.all()
@ -170,7 +177,7 @@ class EmployeeListCreateView(generics.ListCreateAPIView):
class EstablishmentEmployeeListView(generics.ListCreateAPIView):
"""Establishment emplyoees list view."""
permission_classes = (permissions.AllowAny, )
permission_classes = (permissions.AllowAny,)
serializer_class = serializers.EstablishmentEmployeeBackSerializer
def get_queryset(self):
@ -352,8 +359,8 @@ class EstablishmentEmployeeCreateView(generics.CreateAPIView):
class EstablishmentEmployeeDeleteView(generics.DestroyAPIView):
def _get_object_to_delete(self, establishment_id, employee_id):
result_qs = models.EstablishmentEmployee\
.objects\
result_qs = models.EstablishmentEmployee \
.objects \
.filter(establishment_id=establishment_id, employee_id=employee_id)
if not result_qs.exists():
raise Http404
@ -371,7 +378,7 @@ class EstablishmentPositionListView(generics.ListAPIView):
"""Establishment positions list view."""
pagination_class = None
permission_classes = (permissions.AllowAny, )
permission_classes = (permissions.AllowAny,)
queryset = models.Position.objects.all()
serializer_class = serializers.PositionBackSerializer

View File

@ -87,7 +87,7 @@ class RestaurantSimilarListView(EstablishmentSimilarList):
"""Resource for getting a list of similar restaurants."""
def get_queryset(self):
"""Override get_queryset method"""
"""Overridden get_queryset method"""
return EstablishmentMixinView.get_queryset(self) \
.similar_restaurants(slug=self.kwargs.get('slug'))
@ -96,11 +96,20 @@ class WinerySimilarListView(EstablishmentSimilarList):
"""Resource for getting a list of similar wineries."""
def get_queryset(self):
"""Override get_queryset method"""
"""Overridden get_queryset method"""
return EstablishmentMixinView.get_queryset(self) \
.similar_wineries(slug=self.kwargs.get('slug'))
class ArtisanSimilarListView(EstablishmentSimilarList):
"""Resource for getting a list of similar artisans."""
def get_queryset(self):
"""Overridden get_queryset method"""
return EstablishmentMixinView.get_queryset(self) \
.similar_artisans(slug=self.kwargs.get('slug'))
class EstablishmentTypeListView(generics.ListAPIView):
"""Resource for getting a list of establishment types."""

View File

@ -0,0 +1,18 @@
# Generated by Django 2.2.7 on 2019-12-12 13:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('news', '0041_auto_20191211_1528'),
]
operations = [
migrations.AddField(
model_name='news',
name='duplication_date',
field=models.DateTimeField(blank=True, default=None, null=True, verbose_name='Duplication datetime'),
),
]

View File

@ -211,6 +211,8 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
verbose_name=_('banner'))
site = models.ForeignKey('main.SiteSettings', blank=True, null=True,
on_delete=models.SET_NULL, verbose_name=_('site settings'))
duplication_date = models.DateTimeField(blank=True, null=True, default=None,
verbose_name=_('Duplication datetime'))
objects = NewsQuerySet.as_manager()
class Meta:
@ -220,7 +222,16 @@ class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin, HasTagsMixi
verbose_name_plural = _('news')
def __str__(self):
return f'news: {self.slug}'
return f'news: {next(iter(self.slugs.values()))}'
def create_duplicate(self, new_country, view_count_model):
self.pk = None
self.state = self.WAITING
self.slugs = {locale: f'{slug}-{new_country.code}' for locale, slug in self.slugs.items()}
self.country = new_country
self.views_count = view_count_model
self.duplication_date = timezone.now()
self.save()
@property
def is_publish(self):

View File

@ -13,6 +13,9 @@ from tag.serializers import TagBaseSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import (TranslatedField, ProjectModelSerializer,
FavoritesCreateSerializer, ImageBaseSerializer, CarouselCreateSerializer)
from rating import models as rating_models
from django.shortcuts import get_object_or_404
from utils.models import get_current_locale, get_default_locale
class AgendaSerializer(ProjectModelSerializer):
@ -68,6 +71,13 @@ class NewsBaseSerializer(ProjectModelSerializer):
tags = TagBaseSerializer(read_only=True, many=True, source='visible_tags')
in_favorites = serializers.BooleanField(allow_null=True, read_only=True)
view_counter = serializers.IntegerField(read_only=True)
slug = serializers.SerializerMethodField(read_only=True, allow_null=True)
def get_slug(self, obj):
if obj.slugs:
return obj.slugs.get(get_current_locale()) \
or obj.slugs.get(get_default_locale()) \
or next(iter(obj.slugs.values()))
class Meta:
"""Meta class."""
@ -75,12 +85,12 @@ class NewsBaseSerializer(ProjectModelSerializer):
model = models.News
fields = (
'id',
'slug',
'title_translated',
'subtitle_translated',
'is_highlighted',
'news_type',
'tags',
'slugs',
'view_counter',
)
@ -171,10 +181,13 @@ class NewsBackOfficeBaseSerializer(NewsBaseSerializer):
'title',
'backoffice_title',
'subtitle',
'slugs',
'is_published',
'duplication_date',
)
extra_kwargs = {
'backoffice_title': {'allow_null': False},
'duplication_date': {'read_only': True},
}
def create(self, validated_data):
@ -327,3 +340,24 @@ class NewsCarouselCreateSerializer(CarouselCreateSerializer):
'content_object': validated_data.pop('news')
})
return super().create(validated_data)
class NewsCloneCreateSerializer(NewsBackOfficeBaseSerializer,
NewsDetailSerializer):
"""Serializer for creating news clone."""
template_display = serializers.CharField(source='get_template_display',
read_only=True)
class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta):
fields = NewsBackOfficeBaseSerializer.Meta.fields + NewsDetailSerializer.Meta.fields + (
'template_display',
)
read_only_fields = fields
def create(self, validated_data):
kwargs = self.context.get('request').parser_context.get('kwargs')
instance = get_object_or_404(models.News, pk=kwargs['pk'])
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

View File

@ -14,4 +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'),
]

View File

@ -7,7 +7,7 @@ from news import filters, models, serializers
from rating.tasks import add_rating
from utils.permissions import IsCountryAdmin, IsContentPageManager
from utils.views import CreateDestroyGalleryViewMixin, FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
from utils.serializers import ImageBaseSerializer
from utils.serializers import ImageBaseSerializer, EmptySerializer
class NewsMixinView:
@ -167,3 +167,10 @@ class NewsCarouselCreateDestroyView(CarouselCreateDestroyMixinView):
_model = models.News
serializer_class = serializers.NewsCarouselCreateSerializer
class NewsCloneView(generics.CreateAPIView):
"""View for creating clone News"""
permission_classes = (permissions.AllowAny, )
serializer_class = serializers.NewsCloneCreateSerializer
queryset = models.News.objects.all()

View File

@ -222,6 +222,7 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
subtitle_translated = serializers.SerializerMethodField(allow_null=True)
news_type = NewsTypeSerializer()
tags = TagsDocumentSerializer(many=True, source='visible_tags')
slug = serializers.SerializerMethodField(allow_null=True)
class Meta:
"""Meta class."""
@ -237,9 +238,13 @@ class NewsDocumentSerializer(InFavoritesMixin, DocumentSerializer):
'news_type',
'tags',
'start',
'slugs',
'slug',
)
@staticmethod
def get_slug(obj):
return get_translated_value(obj.slugs)
@staticmethod
def get_title_translated(obj):
return get_translated_value(obj.title)

View File

@ -3,7 +3,6 @@ from rest_framework import permissions
from django_elasticsearch_dsl_drf import constants
from django_elasticsearch_dsl_drf.filter_backends import (
FilteringFilterBackend,
GeoSpatialFilteringFilterBackend,
GeoSpatialOrderingFilterBackend,
OrderingFilterBackend,
)

View File

@ -123,19 +123,9 @@ class FiltersTagCategoryBaseSerializer(serializers.ModelSerializer):
return obj in ['open_now', ]
def get_param_name(self, obj):
if obj == 'service':
return 'tags_id__in'
elif obj == 'pop':
return 'tags_id__in'
elif obj == 'open_now':
return 'open_now'
elif obj == 'wine_region':
return 'wine_region_id__in'
return '%s__in' % obj.index_name
if obj.index_name == 'wine-color':
return 'wine_colors_id__in'
return 'tags_id__in'
def get_fields(self, *args, **kwargs):
fields = super(FiltersTagCategoryBaseSerializer, self).get_fields()

View File

@ -1,17 +1,15 @@
"""Tag views."""
from django.conf import settings
from rest_framework import generics
from rest_framework import mixins
from rest_framework import permissions
from rest_framework import status
from rest_framework import viewsets
from rest_framework import generics, mixins, permissions, status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
from django.utils.translation import gettext_lazy as _
from search_indexes import views as search_views
from location.models import WineRegion
from tag import filters
from tag import models
from tag import serializers
from product.models import ProductType
from tag import filters, models, serializers
class ChosenTagsView(generics.ListAPIView, viewsets.GenericViewSet):
@ -61,14 +59,9 @@ class TagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
# User`s views & viewsets
class FiltersTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
class FiltersTagCategoryViewSet(TagCategoryViewSet):
"""ViewSet for TagCategory model."""
filterset_class = filters.TagCategoryFilterSet
pagination_class = None
permission_classes = (permissions.AllowAny,)
queryset = models.TagCategory.objects.with_tags().with_base_related(). \
distinct()
serializer_class = serializers.FiltersTagCategoryBaseSerializer
def list(self, request, *args, **kwargs):
@ -92,7 +85,7 @@ class FiltersTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
if params_type == 'restaurant':
additional_flags += ['toque_number', 'works_noon', 'works_evening', 'works_now']
elif params_type == 'winery':
elif params_type in ['winery', 'wine']:
additional_flags += ['wine_region']
elif params_type == 'artisan':
@ -114,7 +107,7 @@ class FiltersTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
}
result_list.append(toques)
if filter_flags['wine_region']:
if request.query_params.get('product_type') == ProductType.WINE:
wine_region_id = query_params.get('wine_region_id__in')
if str(wine_region_id).isdigit():
@ -185,12 +178,77 @@ class FiltersTagCategoryViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
}
result_list.append(works_at_weekday)
if 'tags_id__in' in query_params:
# filtering by params_type and tags id
# todo: result_list.append( filtering_data )
pass
search_view_class = self.define_search_view_by_request(request)
facets = search_view_class.as_view({'get': 'list'})(self.mutate_request(self.request)).data['facets']
return Response(self.remove_empty_filters(result_list, facets))
return Response(result_list)
@staticmethod
def mutate_request(request):
"""Remove all filtering get params and remove s_ from the rest of them"""
request.GET._mutable = True
for name in request.query_params.copy().keys():
value = request.query_params.pop(name)
if name.startswith('s_'):
request.query_params[name[2:]] = value[0]
request.GET._mutable = False
return request._request
@staticmethod
def define_search_view_by_request(request):
request.GET._mutable = True
if request.query_params.get('items'):
items = request.query_params.pop('items')[0]
else:
raise ValidationError({'detail': _('Missing required "items" parameter')})
item_to_class = {
'news': search_views.NewsDocumentViewSet,
'establishments': search_views.EstablishmentDocumentViewSet,
'products': search_views.ProductDocumentViewSet,
}
klass = item_to_class.get(items)
if klass is None:
raise ValidationError({'detail': _('news/establishments/products')})
request.GET._mutable = False
return klass
@staticmethod
def remove_empty_filters(filters, facets):
# parse facets
if facets.get('_filter_tag'):
tags_to_preserve = list(map(lambda el: el['key'], facets['_filter_tag']['tag']['buckets']))
if facets.get('_filter_wine_colors'):
wine_colors_to_preserve = list(map(lambda el: el['key'], facets['_filter_wine_colors']['wine_colors']['buckets']))
if facets.get('_filter_wine_region_id'):
wine_regions_to_preserve = list(map(lambda el: el['key'], facets['_filter_wine_region_id']['wine_region_id']['buckets']))
if facets.get('_filter_toque_number'):
toque_numbers = list(map(lambda el: el['key'], facets['_filter_toque_number']['toque_number']['buckets']))
if facets.get('_filter_works_noon'):
works_noon = list(map(lambda el: el['key'], facets['_filter_works_noon']['works_noon']['buckets']))
if facets.get('_filter_works_evening'):
works_evening = list(map(lambda el: el['key'], facets['_filter_works_evening']['works_evening']['buckets']))
if facets.get('_filter_works_at_weekday'):
works_at_weekday = list(map(lambda el: el['key'], facets['_filter_works_at_weekday']['works_at_weekday']['buckets']))
if facets.get('_filter_works_now'):
works_now = list(map(lambda el: el['key'], facets['_filter_works_now']['works_now']['buckets']))
# remove empty filters
for category in filters:
param_name = category.get('param_name')
if param_name == 'tags_id__in':
category['filters'] = list(filter(lambda tag: tag['id'] in tags_to_preserve, category['filters']))
elif param_name == 'wine_colors_id__in':
category['filters'] = list(filter(lambda tag: tag['id'] in wine_colors_to_preserve, category['filters']))
elif param_name == 'wine_region_id__in':
category['filters'] = list(filter(lambda tag: tag['id'] in wine_regions_to_preserve, category['filters']))
elif param_name == 'toque_number__in':
category['filters'] = list(filter(lambda tag: tag['id'] in toque_numbers, category['filters']))
elif param_name == 'works_noon__in':
category['filters'] = list(filter(lambda tag: tag['id'] in works_noon, category['filters']))
elif param_name == 'works_evening__in':
category['filters'] = list(filter(lambda tag: tag['id'] in works_evening, category['filters']))
elif param_name == 'works_at_weekday__in':
category['filters'] = list(filter(lambda tag: tag['id'] in works_at_weekday, category['filters']))
return filters
# BackOffice user`s views & viewsets

View File

@ -1224,6 +1224,21 @@ class Footers(MigrateMixin):
db_table = 'footers'
class OwnershipAffs(MigrateMixin):
using = 'legacy'
role = models.CharField(max_length=255, blank=True, null=True)
state = models.CharField(max_length=255, blank=True, null=True)
account_id = models.IntegerField(blank=True, null=True)
establishment_id = models.IntegerField(blank=True, null=True)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
requester_id = models.IntegerField(blank=True, null=True)
class Meta:
managed = False
db_table = 'ownership_affs'
class Panels(MigrateMixin):
using = 'legacy'
@ -1239,19 +1254,3 @@ class Panels(MigrateMixin):
class Meta:
managed = False
db_table = 'panels'
class OwnershipAffs(MigrateMixin):
using = 'legacy'
role = models.CharField(max_length=255, blank=True, null=True)
state = models.CharField(max_length=255, blank=True, null=True)
account_id = models.IntegerField(blank=True, null=True)
establishment_id = models.IntegerField(blank=True, null=True)
created_at = models.DateTimeField()
updated_at = models.DateTimeField()
requester_id = models.IntegerField(blank=True, null=True)
class Meta:
managed = False
db_table = 'ownership_affs'

12
db_migration_resolve.txt Normal file
View File

@ -0,0 +1,12 @@
В случае возникновения проблемы с применением миграции account 0027:
1 Удаляем unique together constrain для app - account
ALTER TABLE account_userrole
DROP CONSTRAINT account_userrole_user_id_role_id_26fa14c4_uniq;
2 Правим миграцию 0023
migrations.AlterUniqueTogether(
name='userrole',
unique_together=set(),
),
3 Применяем account 0027
4 Возвращаем миграцию account 0023, в исходное состояние