Refactored mechanism to find similar objects. Added endpoints to product app.
This commit is contained in:
parent
6a987532ba
commit
dbbb526902
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -219,8 +277,8 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
|
|||
awards = generic.GenericRelation(to='main.Award', related_query_name='product')
|
||||
|
||||
serial_number = models.CharField(max_length=255,
|
||||
default=None, null=True,
|
||||
verbose_name=_('Serial number'))
|
||||
default=None, null=True,
|
||||
verbose_name=_('Serial number'))
|
||||
|
||||
objects = ProductManager.from_queryset(ProductQuerySet)()
|
||||
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user