GM-73: Логика похожих ресторанов

This commit is contained in:
Anatoly 2019-09-17 15:36:33 +03:00
parent 0043b1a8c1
commit 27a1998dbc
10 changed files with 216 additions and 26 deletions

View 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),
),
]

View File

@ -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()

View File

@ -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',
]

View File

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

View File

@ -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()

View File

@ -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'),

View File

@ -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()

View File

@ -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

View File

@ -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',
]

View File

@ -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."""