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)
|
output_field=models.FloatField(default=0)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def has_location(self):
|
||||||
|
"""Return objects with geo location."""
|
||||||
|
return self.filter(address__coordinates__isnull=False)
|
||||||
|
|
||||||
def similar_base(self, establishment):
|
def similar_base(self, establishment):
|
||||||
"""
|
"""
|
||||||
Return filtered QuerySet by base filters.
|
Return filtered QuerySet by base filters.
|
||||||
|
|
@ -267,25 +271,30 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
else:
|
else:
|
||||||
return self.none()
|
return self.none()
|
||||||
|
|
||||||
def similar_artisans(self, slug):
|
def same_subtype(self, establishment):
|
||||||
|
"""Annotate flag same subtype."""
|
||||||
|
return self.annotate(same_subtype=Case(
|
||||||
|
models.When(
|
||||||
|
establishment_subtypes__in=establishment.establishment_subtypes.all(),
|
||||||
|
then=True
|
||||||
|
),
|
||||||
|
default=False,
|
||||||
|
output_field=models.BooleanField(default=False)
|
||||||
|
))
|
||||||
|
|
||||||
|
def similar_artisans_producers(self, slug):
|
||||||
"""
|
"""
|
||||||
Return QuerySet with objects that similar to Artisan.
|
Return QuerySet with objects that similar to Artisan/Producer(s).
|
||||||
:param slug: str artisan slug
|
:param slug: str artisan/producer slug
|
||||||
"""
|
"""
|
||||||
artisan_qs = self.filter(slug=slug)
|
establishment_qs = self.filter(slug=slug)
|
||||||
if artisan_qs.exists():
|
if establishment_qs.exists():
|
||||||
artisan = artisan_qs.first()
|
establishment = establishment_qs.first()
|
||||||
ids_by_subquery = self.similar_base_subquery(
|
return self.similar_base(establishment) \
|
||||||
establishment=artisan,
|
.same_subtype(establishment) \
|
||||||
filters={
|
.order_by(F('same_subtype').desc(),
|
||||||
'public_mark__gte': 10,
|
F('distance').asc()) \
|
||||||
}
|
.distinct('same_subtype', 'distance', 'id')
|
||||||
)
|
|
||||||
return self.filter(id__in=ids_by_subquery) \
|
|
||||||
.annotate_intermediate_public_mark() \
|
|
||||||
.annotate_mark_similarity(mark=artisan.public_mark) \
|
|
||||||
.order_by('mark_similarity') \
|
|
||||||
.distinct('mark_similarity', 'id')
|
|
||||||
else:
|
else:
|
||||||
return self.none()
|
return self.none()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,14 @@ urlpatterns = [
|
||||||
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
|
path('slug/<slug:slug>/favorites/', views.EstablishmentFavoritesCreateDestroyView.as_view(),
|
||||||
name='create-destroy-favorites'),
|
name='create-destroy-favorites'),
|
||||||
|
|
||||||
# similar establishments
|
# similar establishments by type/subtype
|
||||||
path('slug/<slug:slug>/similar/', views.RestaurantSimilarListView.as_view(),
|
path('slug/<slug:slug>/similar/', views.RestaurantSimilarListView.as_view(),
|
||||||
name='similar-restaurants'),
|
name='similar-restaurants'),
|
||||||
path('slug/<slug:slug>/similar/wineries/', views.WinerySimilarListView.as_view(),
|
path('slug/<slug:slug>/similar/wineries/', views.WinerySimilarListView.as_view(),
|
||||||
name='similar-wineries'),
|
name='similar-wineries'),
|
||||||
path('slug/<slug:slug>/similar/artisans/', views.ArtisanSimilarListView.as_view(),
|
# temporary uses single mechanism, bec. description in process
|
||||||
|
path('slug/<slug:slug>/similar/artisans/', views.ArtisanProducerSimilarListView.as_view(),
|
||||||
name='similar-artisans'),
|
name='similar-artisans'),
|
||||||
|
path('slug/<slug:slug>/similar/producers/', views.ArtisanProducerSimilarListView.as_view(),
|
||||||
|
name='similar-producers'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ from comment import models as comment_models
|
||||||
from comment.serializers import CommentRUDSerializer
|
from comment.serializers import CommentRUDSerializer
|
||||||
from establishment import filters, models, serializers
|
from establishment import filters, models, serializers
|
||||||
from main import methods
|
from main import methods
|
||||||
from utils.pagination import EstablishmentPortionPagination
|
from utils.pagination import PortionPagination
|
||||||
from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
|
from utils.views import FavoritesCreateDestroyMixinView, CarouselCreateDestroyMixinView
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -41,6 +41,12 @@ class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView):
|
||||||
.with_certain_tag_category_related('shop_category', 'artisan_category')
|
.with_certain_tag_category_related('shop_category', 'artisan_category')
|
||||||
|
|
||||||
|
|
||||||
|
class EstablishmentSimilarView(EstablishmentListView):
|
||||||
|
"""Resource for getting a list of similar establishments."""
|
||||||
|
serializer_class = serializers.EstablishmentSimilarSerializer
|
||||||
|
pagination_class = PortionPagination
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
|
class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView):
|
||||||
"""Resource for getting a establishment."""
|
"""Resource for getting a establishment."""
|
||||||
|
|
||||||
|
|
@ -61,7 +67,7 @@ class EstablishmentMobileRetrieveView(EstablishmentRetrieveView):
|
||||||
class EstablishmentRecentReviewListView(EstablishmentListView):
|
class EstablishmentRecentReviewListView(EstablishmentListView):
|
||||||
"""List view for last reviewed establishments."""
|
"""List view for last reviewed establishments."""
|
||||||
|
|
||||||
pagination_class = EstablishmentPortionPagination
|
pagination_class = PortionPagination
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden method 'get_queryset'."""
|
"""Overridden method 'get_queryset'."""
|
||||||
|
|
@ -77,37 +83,34 @@ class EstablishmentRecentReviewListView(EstablishmentListView):
|
||||||
return qs.last_reviewed(point=point)
|
return qs.last_reviewed(point=point)
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentSimilarList(EstablishmentListView):
|
class RestaurantSimilarListView(EstablishmentSimilarView):
|
||||||
"""Resource for getting a list of similar establishments."""
|
|
||||||
serializer_class = serializers.EstablishmentSimilarSerializer
|
|
||||||
pagination_class = EstablishmentPortionPagination
|
|
||||||
|
|
||||||
|
|
||||||
class RestaurantSimilarListView(EstablishmentSimilarList):
|
|
||||||
"""Resource for getting a list of similar restaurants."""
|
"""Resource for getting a list of similar restaurants."""
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden get_queryset method"""
|
"""Overridden get_queryset method"""
|
||||||
return EstablishmentMixinView.get_queryset(self) \
|
return EstablishmentMixinView.get_queryset(self) \
|
||||||
|
.has_location() \
|
||||||
.similar_restaurants(slug=self.kwargs.get('slug'))
|
.similar_restaurants(slug=self.kwargs.get('slug'))
|
||||||
|
|
||||||
|
|
||||||
class WinerySimilarListView(EstablishmentSimilarList):
|
class WinerySimilarListView(EstablishmentSimilarView):
|
||||||
"""Resource for getting a list of similar wineries."""
|
"""Resource for getting a list of similar wineries."""
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden get_queryset method"""
|
"""Overridden get_queryset method"""
|
||||||
return EstablishmentMixinView.get_queryset(self) \
|
return EstablishmentMixinView.get_queryset(self) \
|
||||||
|
.has_location() \
|
||||||
.similar_wineries(slug=self.kwargs.get('slug'))
|
.similar_wineries(slug=self.kwargs.get('slug'))
|
||||||
|
|
||||||
|
|
||||||
class ArtisanSimilarListView(EstablishmentSimilarList):
|
class ArtisanProducerSimilarListView(EstablishmentSimilarView):
|
||||||
"""Resource for getting a list of similar artisans."""
|
"""Resource for getting a list of similar artisan/producer(s)."""
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Overridden get_queryset method"""
|
"""Overridden get_queryset method"""
|
||||||
return EstablishmentMixinView.get_queryset(self) \
|
return EstablishmentMixinView.get_queryset(self) \
|
||||||
.similar_artisans(slug=self.kwargs.get('slug'))
|
.has_location() \
|
||||||
|
.similar_artisans_producers(slug=self.kwargs.get('slug'))
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentTypeListView(generics.ListAPIView):
|
class EstablishmentTypeListView(generics.ListAPIView):
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
"""Product app models."""
|
"""Product app models."""
|
||||||
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes import fields as generic
|
from django.contrib.contenttypes import fields as generic
|
||||||
from django.contrib.gis.db import models as gis_models
|
from django.contrib.gis.db import models as gis_models
|
||||||
|
from django.contrib.gis.db.models.functions import Distance
|
||||||
|
from django.contrib.gis.geos import Point
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Case, When
|
from django.db.models import Case, When, F
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from location.models import WineOriginAddressMixin
|
from location.models import WineOriginAddressMixin
|
||||||
|
from review.models import Review
|
||||||
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
|
from utils.models import (BaseAttributes, ProjectBaseMixin, HasTagsMixin,
|
||||||
TranslatedFieldsMixin, TJSONField, FavoritesMixin,
|
TranslatedFieldsMixin, TJSONField, FavoritesMixin,
|
||||||
GalleryModelMixin, IntermediateGalleryModelMixin)
|
GalleryModelMixin, IntermediateGalleryModelMixin)
|
||||||
|
|
@ -136,6 +140,60 @@ class ProductQuerySet(models.QuerySet):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def annotate_distance(self, point: Point = None):
|
||||||
|
"""
|
||||||
|
Return QuerySet with annotated field - distance
|
||||||
|
Description:
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.annotate(distance=Distance('establishment__address__coordinates',
|
||||||
|
point,
|
||||||
|
srid=settings.GEO_DEFAULT_SRID))
|
||||||
|
|
||||||
|
def has_location(self):
|
||||||
|
"""Return objects with geo location."""
|
||||||
|
return self.filter(establishment__address__coordinates__isnull=False)
|
||||||
|
|
||||||
|
def same_subtype(self, product):
|
||||||
|
"""Annotate flag same subtype."""
|
||||||
|
return self.annotate(same_subtype=Case(
|
||||||
|
models.When(
|
||||||
|
subtypes__in=product.subtypes.all(),
|
||||||
|
then=True
|
||||||
|
),
|
||||||
|
default=False,
|
||||||
|
output_field=models.BooleanField(default=False)
|
||||||
|
))
|
||||||
|
|
||||||
|
def similar_base(self, product):
|
||||||
|
"""Return QuerySet filtered by base filters for Product model."""
|
||||||
|
filters = {
|
||||||
|
'reviews__status': Review.READY,
|
||||||
|
'product_type': product.product_type,
|
||||||
|
}
|
||||||
|
if product.subtypes.exists():
|
||||||
|
filters.update(
|
||||||
|
{'subtypes__in': product.subtypes.all()})
|
||||||
|
return self.exclude(id=product.id) \
|
||||||
|
.filter(**filters) \
|
||||||
|
.annotate_distance(point=product.establishment.location)
|
||||||
|
|
||||||
|
def similar(self, slug):
|
||||||
|
"""
|
||||||
|
Return QuerySet with objects that similar to Product.
|
||||||
|
:param slug: str product slug
|
||||||
|
"""
|
||||||
|
product_qs = self.filter(slug=slug)
|
||||||
|
if product_qs.exists():
|
||||||
|
product = product_qs.first()
|
||||||
|
return self.similar_base(product) \
|
||||||
|
.same_subtype(product) \
|
||||||
|
.order_by(F('same_subtype').desc(),
|
||||||
|
F('distance').asc()) \
|
||||||
|
.distinct('same_subtype', 'distance', 'id')
|
||||||
|
else:
|
||||||
|
return self.none()
|
||||||
|
|
||||||
|
|
||||||
class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
|
class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
|
||||||
HasTagsMixin, FavoritesMixin):
|
HasTagsMixin, FavoritesMixin):
|
||||||
|
|
@ -219,8 +277,8 @@ class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes,
|
||||||
awards = generic.GenericRelation(to='main.Award', related_query_name='product')
|
awards = generic.GenericRelation(to='main.Award', related_query_name='product')
|
||||||
|
|
||||||
serial_number = models.CharField(max_length=255,
|
serial_number = models.CharField(max_length=255,
|
||||||
default=None, null=True,
|
default=None, null=True,
|
||||||
verbose_name=_('Serial number'))
|
verbose_name=_('Serial number'))
|
||||||
|
|
||||||
objects = ProductManager.from_queryset(ProductQuerySet)()
|
objects = ProductManager.from_queryset(ProductQuerySet)()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -218,4 +218,4 @@ class ProductCommentCreateSerializer(CommentSerializer):
|
||||||
'user': self.context.get('request').user,
|
'user': self.context.get('request').user,
|
||||||
'content_object': validated_data.pop('product')
|
'content_object': validated_data.pop('product')
|
||||||
})
|
})
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,13 @@ urlpatterns = [
|
||||||
name='create-comment'),
|
name='create-comment'),
|
||||||
path('slug/<slug:slug>/comments/<int:comment_id>/', views.ProductCommentRUDView.as_view(),
|
path('slug/<slug:slug>/comments/<int:comment_id>/', views.ProductCommentRUDView.as_view(),
|
||||||
name='rud-comment'),
|
name='rud-comment'),
|
||||||
|
|
||||||
|
# similar products by type/subtype
|
||||||
|
# temporary uses single mechanism, bec. description in process
|
||||||
|
path('slug/<slug:slug>/similar/wines/', views.SimilarListView.as_view(),
|
||||||
|
name='similar-wine'),
|
||||||
|
path('slug/<slug:slug>/similar/liquors/', views.SimilarListView.as_view(),
|
||||||
|
name='similar-liquor'),
|
||||||
|
path('slug/<slug:slug>/similar/food/', views.SimilarListView.as_view(),
|
||||||
|
name='similar-food'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from comment.models import Comment
|
||||||
from product import filters, serializers
|
from product import filters, serializers
|
||||||
from comment.serializers import CommentRUDSerializer
|
from comment.serializers import CommentRUDSerializer
|
||||||
from utils.views import FavoritesCreateDestroyMixinView
|
from utils.views import FavoritesCreateDestroyMixinView
|
||||||
|
from utils.pagination import PortionPagination
|
||||||
|
|
||||||
|
|
||||||
class ProductBaseView(generics.GenericAPIView):
|
class ProductBaseView(generics.GenericAPIView):
|
||||||
|
|
@ -31,6 +32,12 @@ class ProductListView(ProductBaseView, generics.ListAPIView):
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
|
class ProductSimilarView(ProductListView):
|
||||||
|
"""Resource for getting a list of similar product."""
|
||||||
|
serializer_class = serializers.ProductBaseSerializer
|
||||||
|
pagination_class = PortionPagination
|
||||||
|
|
||||||
|
|
||||||
class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
|
class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
|
||||||
"""Detail view fro model Product."""
|
"""Detail view fro model Product."""
|
||||||
lookup_field = 'slug'
|
lookup_field = 'slug'
|
||||||
|
|
@ -81,3 +88,14 @@ class ProductCommentRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
self.check_object_permissions(self.request, comment_obj)
|
self.check_object_permissions(self.request, comment_obj)
|
||||||
|
|
||||||
return comment_obj
|
return comment_obj
|
||||||
|
|
||||||
|
|
||||||
|
class SimilarListView(ProductSimilarView):
|
||||||
|
"""Return similar products."""
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
"""Overridden get_queryset method."""
|
||||||
|
return super().get_queryset() \
|
||||||
|
.has_location() \
|
||||||
|
.similar(slug=self.kwargs.get('slug'))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from django.conf import settings
|
||||||
from rest_framework.pagination import CursorPagination, PageNumberPagination
|
from rest_framework.pagination import CursorPagination, PageNumberPagination
|
||||||
from django_elasticsearch_dsl_drf.pagination import PageNumberPagination as ESPagination
|
from django_elasticsearch_dsl_drf.pagination import PageNumberPagination as ESPagination
|
||||||
|
|
||||||
|
|
||||||
class ProjectPageNumberPagination(PageNumberPagination):
|
class ProjectPageNumberPagination(PageNumberPagination):
|
||||||
"""Customized pagination class."""
|
"""Customized pagination class."""
|
||||||
|
|
||||||
|
|
@ -82,7 +83,7 @@ class ESDocumentPagination(ESPagination):
|
||||||
return page.facets._d_
|
return page.facets._d_
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentPortionPagination(ProjectMobilePagination):
|
class PortionPagination(ProjectMobilePagination):
|
||||||
"""
|
"""
|
||||||
Pagination for app establishments with limit page size equal to 12
|
Pagination for app establishments with limit page size equal to 12
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user