GM-73: Логика похожих ресторанов
This commit is contained in:
parent
0043b1a8c1
commit
27a1998dbc
50
apps/collection/migrations/0008_auto_20190916_1158.py
Normal file
50
apps/collection/migrations/0008_auto_20190916_1158.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Generated by Django 2.2.4 on 2019-09-16 11:58
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
('collection', '0007_collection_image'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='collection',
|
||||
name='filters',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='collection',
|
||||
name='selectors',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='collection',
|
||||
name='targets',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='collectionitem',
|
||||
name='item_ids',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='collectionitem',
|
||||
name='item_type',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='collection',
|
||||
name='collection_type',
|
||||
field=models.PositiveSmallIntegerField(choices=[(0, 'Ordinary'), (1, 'Pop')], default=0, verbose_name='Collection type'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='collectionitem',
|
||||
name='content_type',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='collectionitem',
|
||||
name='object_id',
|
||||
field=models.PositiveIntegerField(blank=True, default=None, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
from django.contrib.postgres.fields import JSONField
|
||||
from django.contrib.contenttypes.fields import ContentType
|
||||
from django.contrib.contenttypes import fields as generic
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
|
@ -40,6 +42,17 @@ class CollectionQuerySet(models.QuerySet):
|
|||
|
||||
class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
|
||||
"""Collection model."""
|
||||
ORDINARY = 0 # Ordinary collection
|
||||
POP = 1 # POP collection
|
||||
|
||||
COLLECTION_TYPES = (
|
||||
(ORDINARY, _('Ordinary')),
|
||||
(POP, _('Pop')),
|
||||
)
|
||||
|
||||
collection_type = models.PositiveSmallIntegerField(choices=COLLECTION_TYPES,
|
||||
default=ORDINARY,
|
||||
verbose_name=_('Collection type'))
|
||||
image = models.ForeignKey(
|
||||
'gallery.Image', null=True, blank=True, default=None,
|
||||
verbose_name=_('Collection image'), on_delete=models.CASCADE)
|
||||
|
|
@ -47,15 +60,6 @@ class Collection(ProjectBaseMixin, CollectionNameMixin, CollectionDateMixin):
|
|||
default=False, verbose_name=_('Publish status'))
|
||||
on_top = models.BooleanField(
|
||||
default=False, verbose_name=_('Position on top'))
|
||||
filters = JSONField(
|
||||
_('filters'), null=True, blank=True,
|
||||
default=None, help_text='{"key":"value"}')
|
||||
selectors = JSONField(
|
||||
_('selectors'), null=True, blank=True,
|
||||
default=None, help_text='{"key":"value"}')
|
||||
targets = JSONField(
|
||||
_('targets'), null=True, blank=True,
|
||||
default=None, help_text='{"key":"value"}')
|
||||
country = models.ForeignKey(
|
||||
'location.Country', verbose_name=_('country'), on_delete=models.CASCADE)
|
||||
block_size = JSONField(
|
||||
|
|
@ -86,10 +90,10 @@ class CollectionItem(ProjectBaseMixin):
|
|||
"""CollectionItem model."""
|
||||
collection = models.ForeignKey(
|
||||
Collection, verbose_name=_('collection'), on_delete=models.CASCADE)
|
||||
item_type = models.IntegerField(verbose_name=_('item type identifier'))
|
||||
item_ids = JSONField(
|
||||
_('item_ids'), null=True, blank=True,
|
||||
default=None, help_text='{"key":"value"}')
|
||||
content_type = models.ForeignKey(ContentType, default=None,
|
||||
null=True, blank=True, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveIntegerField(default=None, null=True, blank=True)
|
||||
content_object = generic.GenericForeignKey('content_type', 'object_id')
|
||||
|
||||
objects = CollectionItemQuerySet.as_manager()
|
||||
|
||||
|
|
|
|||
|
|
@ -18,9 +18,6 @@ class CollectionSerializer(serializers.ModelSerializer):
|
|||
# REQUEST
|
||||
start = serializers.DateTimeField(write_only=True)
|
||||
end = serializers.DateTimeField(write_only=True)
|
||||
filters = serializers.JSONField(write_only=True)
|
||||
selectors = serializers.JSONField(write_only=True)
|
||||
targets = serializers.JSONField(write_only=True)
|
||||
country = serializers.PrimaryKeyRelatedField(
|
||||
queryset=location_models.Country.objects.all(),
|
||||
write_only=True)
|
||||
|
|
@ -39,9 +36,6 @@ class CollectionSerializer(serializers.ModelSerializer):
|
|||
'image_url',
|
||||
'is_publish',
|
||||
'on_top',
|
||||
'filters',
|
||||
'selectors',
|
||||
'targets',
|
||||
'country',
|
||||
'block_size',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.4 on 2019-09-16 11:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('establishment', '0018_socialnetwork'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='establishment',
|
||||
name='is_publish',
|
||||
field=models.BooleanField(default=False, verbose_name='Publish status'),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
"""Establishment models."""
|
||||
from functools import reduce
|
||||
from django.contrib.gis.db.models.functions import Distance
|
||||
|
||||
from django.contrib.gis.measure import Distance as DistanceMeasure
|
||||
from django.contrib.gis.geos import Point
|
||||
from django.contrib.contenttypes import fields as generic
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
|
|
@ -9,6 +12,8 @@ from django.utils.translation import gettext_lazy as _
|
|||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
from location.models import Address
|
||||
from collection.models import Collection
|
||||
from review.models import Review
|
||||
from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField,
|
||||
TranslatedFieldsMixin, BaseAttributes)
|
||||
|
||||
|
|
@ -80,6 +85,107 @@ class EstablishmentQuerySet(models.QuerySet):
|
|||
"""Return establishments by country code"""
|
||||
return self.filter(address__city__country__code=code)
|
||||
|
||||
def published(self):
|
||||
"""
|
||||
Return QuerySet with published establishments.
|
||||
"""
|
||||
return self.filter(is_publish=True)
|
||||
|
||||
def annotate_distance(self, point: Point):
|
||||
"""
|
||||
Return QuerySet with annotated field - distance
|
||||
Description:
|
||||
|
||||
"""
|
||||
return self.annotate(distance=models.Value(
|
||||
DistanceMeasure(Distance('address__coordinates', point, srid=4236)).m,
|
||||
output_field=models.FloatField()))
|
||||
|
||||
def annotate_distance_mark(self):
|
||||
"""
|
||||
Return QuerySet with annotated field - distance_mark.
|
||||
Required fields: distance.
|
||||
Description:
|
||||
If the radius of the establishments in QuerySet does not exceed 500 meters,
|
||||
then distance_mark is set to 0.6, otherwise 0.
|
||||
"""
|
||||
return self.annotate(distance_mark=models.Case(
|
||||
models.When(distance__lte=500,
|
||||
then=0.6),
|
||||
default=0,
|
||||
output_field=models.FloatField()))
|
||||
|
||||
def annotate_intermediate_public_mark(self):
|
||||
"""
|
||||
Return QuerySet with annotated field - intermediate_public_mark.
|
||||
Description:
|
||||
If establishments in collection POP and its mark is null, then
|
||||
intermediate_mark is set to 10;
|
||||
"""
|
||||
return self.annotate(intermediate_public_mark=models.Case(
|
||||
models.When(
|
||||
collections__collection__collection_type=Collection.POP,
|
||||
public_mark__isnull=True,
|
||||
then=10
|
||||
),
|
||||
default='public_mark',
|
||||
output_field=models.PositiveSmallIntegerField()))
|
||||
|
||||
def annotate_additional_mark(self, public_mark: float):
|
||||
"""
|
||||
Return QuerySet with annotated field - additional_mark.
|
||||
Required fields: intermediate_public_mark
|
||||
Description:
|
||||
IF
|
||||
establishments public_mark + 3 > compared establishment public_mark
|
||||
OR
|
||||
establishments public_mark - 3 > compared establishment public_mark,
|
||||
THEN
|
||||
additional_mark is set to 0.4,
|
||||
ELSE
|
||||
set to 0.
|
||||
"""
|
||||
return self.annotate(additional_mark=models.Case(
|
||||
models.When(
|
||||
models.Q(intermediate_public_mark__lte=public_mark + 3) |
|
||||
models.Q(intermediate_public_mark__lte=public_mark - 3),
|
||||
then=0.4),
|
||||
default=0,
|
||||
output_field=models.FloatField()))
|
||||
|
||||
def annotate_total_mark(self):
|
||||
"""
|
||||
Return QuerySet with annotated field - total_mark.
|
||||
Required fields: distance_mark, additional_mark.
|
||||
Fields
|
||||
Description:
|
||||
Annotated field is obtained by adding the distance and additional marks.
|
||||
"""
|
||||
return self.annotate(total_mark=models.F('distance_mark') +
|
||||
models.F('additional_mark'))
|
||||
|
||||
def similar(self, establishment_pk: int):
|
||||
"""
|
||||
Return QuerySet with objects that similar to Establishment.
|
||||
:param establishment_pk: integer
|
||||
"""
|
||||
establishment_qs = Establishment.objects.filter(pk=establishment_pk)
|
||||
if establishment_qs.exists():
|
||||
establishment = establishment_qs.first()
|
||||
return self.exclude(pk=establishment_pk) \
|
||||
.filter(is_publish=True,
|
||||
image__isnull=False,
|
||||
reviews__isnull=False,
|
||||
reviews__status=Review.READY,
|
||||
public_mark__gte=10) \
|
||||
.annotate_distance(point=establishment.address.coordinates) \
|
||||
.annotate_distance_mark() \
|
||||
.annotate_intermediate_public_mark() \
|
||||
.annotate_additional_mark(public_mark=establishment.public_mark) \
|
||||
.annotate_total_mark()
|
||||
else:
|
||||
return self.none()
|
||||
|
||||
def prefetch_actual_employees(self):
|
||||
"""Prefetch actual employees."""
|
||||
return self.prefetch_related(
|
||||
|
|
@ -153,10 +259,12 @@ class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin):
|
|||
verbose_name=_('Lafourchette URL'))
|
||||
booking = models.URLField(blank=True, null=True, default=None,
|
||||
verbose_name=_('Booking URL'))
|
||||
is_publish = models.BooleanField(default=False, verbose_name=_('Publish status'))
|
||||
awards = generic.GenericRelation(to='main.Award')
|
||||
tags = generic.GenericRelation(to='main.MetaDataContent')
|
||||
reviews = generic.GenericRelation(to='review.Review')
|
||||
comments = generic.GenericRelation(to='comment.Comment')
|
||||
collections = generic.GenericRelation(to='collection.CollectionItem')
|
||||
|
||||
objects = EstablishmentQuerySet.as_manager()
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ urlpatterns = [
|
|||
path('', views.EstablishmentListView.as_view(), name='list'),
|
||||
path('tags/', views.EstablishmentTagListView.as_view(), name='tags'),
|
||||
path('<int:pk>/', views.EstablishmentRetrieveView.as_view(), name='detail'),
|
||||
path('<int:pk>/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'),
|
||||
path('<int:pk>/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'),
|
||||
path('<int:pk>/comments/create/', views.EstablishmentCommentCreateView.as_view(),
|
||||
name='create-comment'),
|
||||
|
|
|
|||
|
|
@ -12,4 +12,5 @@ class EstablishmentMixin:
|
|||
|
||||
def get_queryset(self):
|
||||
"""Overrided method 'get_queryset'."""
|
||||
return models.Establishment.objects.all().prefetch_actual_employees()
|
||||
return models.Establishment.objects.published() \
|
||||
.prefetch_actual_employees()
|
||||
|
|
|
|||
|
|
@ -19,10 +19,21 @@ class EstablishmentListView(EstablishmentMixin, JWTGenericViewMixin, generics.Li
|
|||
def get_queryset(self):
|
||||
"""Overridden method 'get_queryset'."""
|
||||
qs = super(EstablishmentListView, self).get_queryset()
|
||||
return qs.by_country_code(code=self.request.country_code)\
|
||||
return qs.by_country_code(code=self.request.country_code) \
|
||||
.annotate_in_favorites(user=self.request.user)
|
||||
|
||||
|
||||
class EstablishmentSimilarListView(EstablishmentListView):
|
||||
"""Resource for getting a list of establishments."""
|
||||
serializer_class = serializers.EstablishmentListSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""Override get_queryset method"""
|
||||
qs = super(EstablishmentListView, self).get_queryset()
|
||||
return qs.similar(establishment_pk=self.kwargs.get('pk'))\
|
||||
.order_by('?')[:13]
|
||||
|
||||
|
||||
class EstablishmentRetrieveView(EstablishmentMixin, JWTGenericViewMixin, generics.RetrieveAPIView):
|
||||
"""Resource for getting a establishment."""
|
||||
serializer_class = serializers.EstablishmentDetailSerializer
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ class RegionSerializer(serializers.ModelSerializer):
|
|||
|
||||
class CitySerializer(serializers.ModelSerializer):
|
||||
"""City serializer."""
|
||||
country = CountrySerializer()
|
||||
region = RegionSerializer()
|
||||
|
||||
class Meta:
|
||||
|
|
@ -48,7 +47,6 @@ class CitySerializer(serializers.ModelSerializer):
|
|||
'name',
|
||||
'code',
|
||||
'region',
|
||||
'country',
|
||||
'postal_code',
|
||||
'is_island',
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
# @admin.register(models.Review)
|
||||
# class ReviewAdminModel(admin.ModelAdmin):
|
||||
# """Admin model for model Review."""
|
||||
"""Admin page for app Review"""
|
||||
from . import models
|
||||
from django.contrib import admin
|
||||
|
||||
|
||||
@admin.register(models.Review)
|
||||
class ReviewAdminModel(admin.ModelAdmin):
|
||||
"""Admin model for model Review."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user