From 4ee6306efb49ede993cd3995dd4099d314808da8 Mon Sep 17 00:00:00 2001 From: Kuroshini Date: Tue, 17 Sep 2019 13:47:41 +0300 Subject: [PATCH 1/4] Add transportation text field to establishment model --- .../0019_establishment_transportation.py | 18 ++++++++++++++++++ apps/establishment/models.py | 2 ++ apps/establishment/serializers/common.py | 1 + 3 files changed, 21 insertions(+) create mode 100644 apps/establishment/migrations/0019_establishment_transportation.py diff --git a/apps/establishment/migrations/0019_establishment_transportation.py b/apps/establishment/migrations/0019_establishment_transportation.py new file mode 100644 index 00000000..ba4f4f5e --- /dev/null +++ b/apps/establishment/migrations/0019_establishment_transportation.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-09-17 10:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0018_socialnetwork'), + ] + + operations = [ + migrations.AddField( + model_name='establishment', + name='transportation', + field=models.TextField(blank=True, default=None, null=True, verbose_name='Transportation'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 2d16cfe6..e25f89d9 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -157,6 +157,8 @@ class Establishment(ProjectBaseMixin, ImageMixin, TranslatedFieldsMixin): tags = generic.GenericRelation(to='main.MetaDataContent') reviews = generic.GenericRelation(to='review.Review') comments = generic.GenericRelation(to='comment.Comment') + transportation = models.TextField(blank=True, null=True, default=None, + verbose_name=_('Transportation')) objects = EstablishmentQuerySet.as_manager() diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index 65331ad7..740a5f93 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -241,6 +241,7 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): 'menu', 'best_price_menu', 'best_price_carte', + 'transportation', ] def get_review(self, obj): From 27a1998dbcd13dbff715dcd71241c32d3fe806cf Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 17 Sep 2019 15:36:33 +0300 Subject: [PATCH 2/4] =?UTF-8?q?GM-73:=20=D0=9B=D0=BE=D0=B3=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D1=85=D0=BE=D0=B6=D0=B8=D1=85=20=D1=80?= =?UTF-8?q?=D0=B5=D1=81=D1=82=D0=BE=D1=80=D0=B0=D0=BD=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/0008_auto_20190916_1158.py | 50 ++++++++ apps/collection/models.py | 30 ++--- apps/collection/serializers/common.py | 6 - .../0019_establishment_is_publish.py | 18 +++ apps/establishment/models.py | 108 ++++++++++++++++++ apps/establishment/urls/common.py | 1 + apps/establishment/views/common.py | 3 +- apps/establishment/views/web.py | 13 ++- apps/location/serializers.py | 2 - apps/review/admin.py | 11 +- 10 files changed, 216 insertions(+), 26 deletions(-) create mode 100644 apps/collection/migrations/0008_auto_20190916_1158.py create mode 100644 apps/establishment/migrations/0019_establishment_is_publish.py diff --git a/apps/collection/migrations/0008_auto_20190916_1158.py b/apps/collection/migrations/0008_auto_20190916_1158.py new file mode 100644 index 00000000..1f3f8045 --- /dev/null +++ b/apps/collection/migrations/0008_auto_20190916_1158.py @@ -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), + ), + ] diff --git a/apps/collection/models.py b/apps/collection/models.py index 6b82e799..3ef4d0bf 100644 --- a/apps/collection/models.py +++ b/apps/collection/models.py @@ -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() diff --git a/apps/collection/serializers/common.py b/apps/collection/serializers/common.py index 69319f74..4fd692a6 100644 --- a/apps/collection/serializers/common.py +++ b/apps/collection/serializers/common.py @@ -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', ] diff --git a/apps/establishment/migrations/0019_establishment_is_publish.py b/apps/establishment/migrations/0019_establishment_is_publish.py new file mode 100644 index 00000000..869a41ac --- /dev/null +++ b/apps/establishment/migrations/0019_establishment_is_publish.py @@ -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'), + ), + ] diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 5a187f89..2e9b27ed 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -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() diff --git a/apps/establishment/urls/common.py b/apps/establishment/urls/common.py index dc96c542..3fdd46d3 100644 --- a/apps/establishment/urls/common.py +++ b/apps/establishment/urls/common.py @@ -9,6 +9,7 @@ urlpatterns = [ path('', views.EstablishmentListView.as_view(), name='list'), path('tags/', views.EstablishmentTagListView.as_view(), name='tags'), path('/', views.EstablishmentRetrieveView.as_view(), name='detail'), + path('/similar/', views.EstablishmentSimilarListView.as_view(), name='similar'), path('/comments/', views.EstablishmentCommentListView.as_view(), name='list-comments'), path('/comments/create/', views.EstablishmentCommentCreateView.as_view(), name='create-comment'), diff --git a/apps/establishment/views/common.py b/apps/establishment/views/common.py index 454ef1d0..b57d6ef6 100644 --- a/apps/establishment/views/common.py +++ b/apps/establishment/views/common.py @@ -12,4 +12,5 @@ class EstablishmentMixin: def get_queryset(self): """Overrided method 'get_queryset'.""" - return models.Establishment.objects.all().prefetch_actual_employees() \ No newline at end of file + return models.Establishment.objects.published() \ + .prefetch_actual_employees() diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 354b12a0..b4b4b587 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -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 diff --git a/apps/location/serializers.py b/apps/location/serializers.py index b7f2ebbb..34d92cc3 100644 --- a/apps/location/serializers.py +++ b/apps/location/serializers.py @@ -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', ] diff --git a/apps/review/admin.py b/apps/review/admin.py index cbd0bf94..5820f557 100644 --- a/apps/review/admin.py +++ b/apps/review/admin.py @@ -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.""" From 69f1a5cf481816a55535493a56b2c4e296ac7878 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Tue, 17 Sep 2019 17:16:21 +0300 Subject: [PATCH 3/4] GM-73 added ordering --- apps/establishment/models.py | 10 ++++++---- apps/establishment/views/web.py | 6 +++--- project/settings/development.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 84fc3e02..c07de769 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -129,7 +129,7 @@ class EstablishmentQuerySet(models.QuerySet): then=10 ), default='public_mark', - output_field=models.PositiveSmallIntegerField())) + output_field=models.FloatField())) def annotate_additional_mark(self, public_mark: float): """ @@ -159,10 +159,12 @@ class EstablishmentQuerySet(models.QuerySet): Required fields: distance_mark, additional_mark. Fields Description: - Annotated field is obtained by adding the distance and additional marks. + Annotated field is obtained by formula: + (distance + additional marks) * intermediate_public_mark. """ - return self.annotate(total_mark=models.F('distance_mark') + - models.F('additional_mark')) + return self.annotate( + total_mark=(models.F('distance_mark') + models.F('additional_mark')) * + models.F('intermediate_public_mark')) def similar(self, establishment_pk: int): """ diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index b4b4b587..cc0d1c56 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -11,7 +11,7 @@ from utils.views import JWTGenericViewMixin from establishment.views import EstablishmentMixin -class EstablishmentListView(EstablishmentMixin, JWTGenericViewMixin, generics.ListAPIView): +class EstablishmentListView(EstablishmentMixin, generics.ListAPIView): """Resource for getting a list of establishments.""" serializer_class = serializers.EstablishmentListSerializer filter_class = filters.EstablishmentFilter @@ -29,9 +29,9 @@ class EstablishmentSimilarListView(EstablishmentListView): def get_queryset(self): """Override get_queryset method""" - qs = super(EstablishmentListView, self).get_queryset() + qs = super().get_queryset() return qs.similar(establishment_pk=self.kwargs.get('pk'))\ - .order_by('?')[:13] + .order_by('-total_mark')[:13] class EstablishmentRetrieveView(EstablishmentMixin, JWTGenericViewMixin, generics.RetrieveAPIView): diff --git a/project/settings/development.py b/project/settings/development.py index 7c47ff5b..a7c8d9fd 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -32,4 +32,4 @@ ELASTICSEARCH_INDEX_NAMES = { sentry_sdk.init( dsn="https://35d9bb789677410ab84a822831c6314f@sentry.io/1729093", integrations=[DjangoIntegration()] -) \ No newline at end of file +) From c3717ecfd934eab0bbb5c32565019a13ba1f2f4b Mon Sep 17 00:00:00 2001 From: Dmitriy Kuzmenko Date: Tue, 17 Sep 2019 17:16:35 +0300 Subject: [PATCH 4/4] merge transportation --- .../migrations/0020_merge_20190917_1415.py | 14 ++++++++++++++ apps/location/admin.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 apps/establishment/migrations/0020_merge_20190917_1415.py diff --git a/apps/establishment/migrations/0020_merge_20190917_1415.py b/apps/establishment/migrations/0020_merge_20190917_1415.py new file mode 100644 index 00000000..15846929 --- /dev/null +++ b/apps/establishment/migrations/0020_merge_20190917_1415.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.4 on 2019-09-17 14:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('establishment', '0019_establishment_is_publish'), + ('establishment', '0019_establishment_transportation'), + ] + + operations = [ + ] diff --git a/apps/location/admin.py b/apps/location/admin.py index 146a8b43..a7610a65 100644 --- a/apps/location/admin.py +++ b/apps/location/admin.py @@ -23,7 +23,7 @@ class CityAdmin(admin.ModelAdmin): class AddressAdmin(admin.OSMGeoAdmin): """Address admin.""" list_display = ('__str__', 'geo_lon', 'geo_lat') - readonly_fields = ['geo_lon', 'geo_lat',] + readonly_fields = ['geo_lon', 'geo_lat', ] fieldsets = ( (_('Main'), { 'fields': ['street_name_1', 'street_name_2', 'number']