diff --git a/apps/authorization/tests/tests_authorization.py b/apps/authorization/tests/tests_authorization.py index 4a5d2a2b..a6a49ea5 100644 --- a/apps/authorization/tests/tests_authorization.py +++ b/apps/authorization/tests/tests_authorization.py @@ -1,5 +1,6 @@ from rest_framework.test import APITestCase from account.models import User +from django.urls import reverse # Create your tests here. @@ -22,7 +23,6 @@ def get_tokens_for_user( class AuthorizationTests(APITestCase): def setUp(self): - print("Auth!") data = get_tokens_for_user() self.username = data["username"] self.password = data["password"] @@ -33,7 +33,7 @@ class AuthorizationTests(APITestCase): 'password': self.password, 'remember': True } - response = self.client.post('/api/auth/login/', data=data) + response = self.client.post(reverse('auth:authorization:login'), data=data) self.assertEqual(response.data['access_token'], self.tokens.get('access_token')) self.assertEqual(response.data['refresh_token'], self.tokens.get('refresh_token')) diff --git a/apps/collection/models.py b/apps/collection/models.py index beec9b08..25bf1ef9 100644 --- a/apps/collection/models.py +++ b/apps/collection/models.py @@ -8,6 +8,8 @@ from django.utils.translation import gettext_lazy as _ from utils.models import ProjectBaseMixin, URLImageMixin from utils.models import TranslatedFieldsMixin +from utils.querysets import RelatedObjectsCountMixin + # Mixins class CollectionNameMixin(models.Model): @@ -30,7 +32,7 @@ class CollectionDateMixin(models.Model): # Models -class CollectionQuerySet(models.QuerySet): +class CollectionQuerySet(RelatedObjectsCountMixin): """QuerySet for model Collection""" def by_country_code(self, code): diff --git a/apps/collection/tests.py b/apps/collection/tests.py index 6a985fc0..ea13fff9 100644 --- a/apps/collection/tests.py +++ b/apps/collection/tests.py @@ -8,6 +8,7 @@ from http.cookies import SimpleCookie from collection.models import Collection, Guide from location.models import Country +from establishment.models import Establishment, EstablishmentType # Create your tests here. @@ -20,7 +21,7 @@ class BaseTestCase(APITestCase): self.newsletter = True self.user = User.objects.create_user( username=self.username, email=self.email, password=self.password) - #get tokens + # get tokens tokens = User.create_jwt_tokens(self.user) self.client.cookies = SimpleCookie( {'access_token': tokens.get('access_token'), @@ -81,3 +82,27 @@ class CollectionGuideDetailTests(CollectionDetailTests): def test_guide_detail_Read(self): response = self.client.get(f'/api/web/collections/guides/{self.guide.id}/', format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class CollectionWebHomeTests(CollectionDetailTests): + def setUp(self): + super().setUp() + self.establishment_type = EstablishmentType.objects.create(name="Test establishment type") + + for i in range(1, 5): + setattr(self, f"establishment{i}", + Establishment.objects.create( + name=f"Test establishment {i}", + establishment_type_id=self.establishment_type.id, + is_publish=True, + slug=f"test-establishment-{i}" + ) + ) + + getattr(self, f"establishment{i}").collections.add(self.collection) + + def test_collection_list_filter(self): + response = self.client.get('/api/web/collections/?country_code=en', format='json') + data = response.json() + self.assertIn('count', data) + self.assertGreater(data['count'], 0) diff --git a/apps/collection/urls/common.py b/apps/collection/urls/common.py index 01385c3d..7ffa50cf 100644 --- a/apps/collection/urls/common.py +++ b/apps/collection/urls/common.py @@ -6,7 +6,7 @@ from collection.views import common as views app_name = 'collection' urlpatterns = [ - path('', views.CollectionListView.as_view(), name='list'), + path('', views.CollectionHomePageView.as_view(), name='list'), path('/establishments/', views.CollectionEstablishmentListView.as_view(), name='detail'), diff --git a/apps/collection/views/common.py b/apps/collection/views/common.py index d42bd851..148c5fab 100644 --- a/apps/collection/views/common.py +++ b/apps/collection/views/common.py @@ -30,9 +30,26 @@ class CollectionListView(CollectionViewMixin, generics.ListAPIView): def get_queryset(self): """Override get_queryset method""" - return models.Collection.objects.published()\ - .by_country_code(code=self.request.country_code)\ - .order_by('-on_top', '-created') + queryset = models.Collection.objects.published()\ + .by_country_code(code=self.request.country_code)\ + .order_by('-on_top', '-created') + + return queryset + + +class CollectionHomePageView(CollectionViewMixin, generics.ListAPIView): + """List Collection view""" + permission_classes = (permissions.AllowAny,) + serializer_class = serializers.CollectionSerializer + + def get_queryset(self): + """Override get_queryset method""" + queryset = models.Collection.objects.published()\ + .by_country_code(code=self.request.country_code)\ + .filter_all_related_gt(3)\ + .order_by('-on_top', '-modified') + + return queryset class CollectionEstablishmentListView(CollectionListView): diff --git a/apps/establishment/models.py b/apps/establishment/models.py index 365f9767..1d5be824 100644 --- a/apps/establishment/models.py +++ b/apps/establishment/models.py @@ -14,9 +14,10 @@ from django.utils.translation import gettext_lazy as _ from phonenumber_field.modelfields import PhoneNumberField from collection.models import Collection +from main.models import MetaDataContent from location.models import Address from review.models import Review -from utils.models import (ProjectBaseMixin, ImageMixin, TJSONField, URLImageMixin, +from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin, TranslatedFieldsMixin, BaseAttributes) @@ -72,6 +73,20 @@ class EstablishmentSubType(ProjectBaseMixin, TranslatedFieldsMixin): class EstablishmentQuerySet(models.QuerySet): """Extended queryset for Establishment model.""" + def with_base_related(self): + """Return qs with related objects.""" + return self.select_related('address').prefetch_related( + models.Prefetch('tags', + MetaDataContent.objects.select_related( + 'metadata__category')) + ) + + def with_extended_related(self): + return self.select_related('establishment_type').\ + prefetch_related('establishment_subtypes', 'awards', 'schedule', + 'phones').\ + prefetch_actual_employees() + def search(self, value, locale=None): """Search text in JSON fields.""" if locale is not None: @@ -298,12 +313,12 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin): return country.low_price, country.high_price # todo: make via prefetch - @property - def subtypes(self): - return EstablishmentSubType.objects.filter( - subtype_establishment=self, - establishment_type=self.establishment_type, - establishment_type__use_subtypes=True) + # @property + # def subtypes(self): + # return EstablishmentSubType.objects.filter( + # subtype_establishment=self, + # establishment_type=self.establishment_type, + # establishment_type__use_subtypes=True) def set_establishment_type(self, establishment_type): self.establishment_type = establishment_type diff --git a/apps/establishment/serializers/back.py b/apps/establishment/serializers/back.py index 96795711..5a15aafc 100644 --- a/apps/establishment/serializers/back.py +++ b/apps/establishment/serializers/back.py @@ -1,12 +1,9 @@ -import json from rest_framework import serializers - from establishment import models -from timetable.models import Timetable from establishment.serializers import ( EstablishmentBaseSerializer, PlateSerializer, ContactEmailsSerializer, - ContactPhonesSerializer, SocialNetworkRelatedSerializers, EstablishmentDetailSerializer -) + ContactPhonesSerializer, SocialNetworkRelatedSerializers, + EstablishmentTypeSerializer) from utils.decorators import with_base_attributes @@ -25,6 +22,7 @@ class EstablishmentListCreateSerializer(EstablishmentBaseSerializer): emails = ContactEmailsSerializer(read_only=True, many=True, ) socials = SocialNetworkRelatedSerializers(read_only=True, many=True, ) slug = serializers.SlugField(required=True, allow_blank=False, max_length=50) + type = EstablishmentTypeSerializer(source='establishment_type', read_only=True) class Meta: model = models.Establishment @@ -56,6 +54,7 @@ class EstablishmentRUDSerializer(EstablishmentBaseSerializer): phones = ContactPhonesSerializer(read_only=False, many=True, ) emails = ContactEmailsSerializer(read_only=False, many=True, ) socials = SocialNetworkRelatedSerializers(read_only=False, many=True, ) + type = EstablishmentTypeSerializer(source='establishment_type') class Meta: model = models.Establishment diff --git a/apps/establishment/serializers/common.py b/apps/establishment/serializers/common.py index c637acd8..0e33921c 100644 --- a/apps/establishment/serializers/common.py +++ b/apps/establishment/serializers/common.py @@ -1,19 +1,20 @@ """Establishment serializers.""" from rest_framework import serializers - +from django.utils.translation import ugettext_lazy as _ from comment import models as comment_models from comment.serializers import common as comment_serializers from establishment import models from favorites.models import Favorites -from location.serializers import AddressSerializer +from location.serializers import AddressSimpleSerializer, AddressSerializer from main.models import MetaDataContent from main.serializers import MetaDataContentSerializer, AwardSerializer, CurrencySerializer from review import models as review_models from timetable.serialziers import ScheduleRUDSerializer from utils import exceptions as utils_exceptions -from django.utils.translation import gettext_lazy as _ +from utils.serializers import TranslatedField from utils.serializers import TJSONSerializer + class ContactPhonesSerializer(serializers.ModelSerializer): """Contact phone serializer""" class Meta: @@ -142,11 +143,11 @@ class EstablishmentEmployeeSerializer(serializers.ModelSerializer): class EstablishmentBaseSerializer(serializers.ModelSerializer): """Base serializer for Establishment model.""" - type = EstablishmentTypeSerializer(source='establishment_type', read_only=True) - subtypes = EstablishmentSubTypeSerializer(many=True) + + preview_image = serializers.URLField(source='preview_image_url') + slug = serializers.SlugField(allow_blank=False, required=True, max_length=50) address = AddressSerializer() tags = MetaDataContentSerializer(many=True) - slug = serializers.SlugField(allow_blank=False, required=True, max_length=50) class Meta: """Meta class.""" @@ -159,60 +160,60 @@ class EstablishmentBaseSerializer(serializers.ModelSerializer): 'price_level', 'toque_number', 'public_mark', - 'type', - 'subtypes', + 'slug', + 'preview_image', + 'in_favorites', 'address', 'tags', - 'slug', ] class EstablishmentListSerializer(EstablishmentBaseSerializer): """Serializer for Establishment model.""" - # Annotated fields + in_favorites = serializers.BooleanField(allow_null=True) - preview_image = serializers.URLField(source='preview_image_url') - - class Meta: + class Meta(EstablishmentBaseSerializer.Meta): """Meta class.""" - model = models.Establishment fields = EstablishmentBaseSerializer.Meta.fields + [ 'in_favorites', - 'preview_image', ] +class EstablishmentAllListSerializer(EstablishmentListSerializer): + """ Serailizer for api/*/establishments """ + address = AddressSimpleSerializer() + + class Meta(EstablishmentListSerializer.Meta): + pass + class EstablishmentDetailSerializer(EstablishmentListSerializer): """Serializer for Establishment model.""" - description_translated = serializers.CharField(allow_null=True) + + description_translated = TranslatedField() + image = serializers.URLField(source='image_url') + type = EstablishmentTypeSerializer(source='establishment_type', read_only=True) + subtypes = EstablishmentSubTypeSerializer(many=True, source='establishment_subtypes') awards = AwardSerializer(many=True) schedule = ScheduleRUDSerializer(many=True, allow_null=True) - phones = ContactPhonesSerializer(read_only=True, many=True, ) - emails = ContactEmailsSerializer(read_only=True, many=True, ) + phones = ContactPhonesSerializer(read_only=True, many=True) + emails = ContactEmailsSerializer(read_only=True, many=True) review = ReviewSerializer(source='last_published_review', allow_null=True) employees = EstablishmentEmployeeSerializer(source='actual_establishment_employees', many=True) menu = MenuSerializers(source='menu_set', many=True, read_only=True) - best_price_menu = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True) best_price_carte = serializers.DecimalField(max_digits=14, decimal_places=2, read_only=True) - slug = serializers.SlugField(required=True, allow_blank=False, max_length=50) - - in_favorites = serializers.BooleanField() - - image = serializers.URLField(source='image_url') - - class Meta: + class Meta(EstablishmentListSerializer.Meta): """Meta class.""" - model = models.Establishment fields = EstablishmentListSerializer.Meta.fields + [ 'description_translated', - 'price_level', 'image', + 'subtypes', + 'type', 'awards', 'schedule', 'website', @@ -228,9 +229,18 @@ class EstablishmentDetailSerializer(EstablishmentListSerializer): 'best_price_menu', 'best_price_carte', 'transportation', - 'slug', ] + # def get_in_favorites(self, obj): + # """Get in_favorites status flag""" + # user = self.context.get('request').user + # if user.is_authenticated: + # return obj.id in user.favorites.by_content_type(app_label='establishment', + # model='establishment')\ + # .values_list('object_id', flat=True) + # else: + # return False + class EstablishmentCommentCreateSerializer(comment_serializers.CommentSerializer): """Create comment serializer""" diff --git a/apps/establishment/tests.py b/apps/establishment/tests.py index 16d224b8..39e28861 100644 --- a/apps/establishment/tests.py +++ b/apps/establishment/tests.py @@ -6,6 +6,7 @@ from http.cookies import SimpleCookie from main.models import Currency from establishment.models import Establishment, EstablishmentType, Menu # Create your tests here. +from translation.models import Language class BaseTestCase(APITestCase): @@ -25,6 +26,12 @@ class BaseTestCase(APITestCase): self.establishment_type = EstablishmentType.objects.create(name="Test establishment type") + # Create lang object + Language.objects.create( + title='English', + locale='en-GB' + ) + class EstablishmentBTests(BaseTestCase): def test_establishment_CRUD(self): diff --git a/apps/establishment/views/back.py b/apps/establishment/views/back.py index 974fca8a..5cba8255 100644 --- a/apps/establishment/views/back.py +++ b/apps/establishment/views/back.py @@ -4,10 +4,17 @@ from rest_framework import generics from establishment import models from establishment import serializers -from establishment.views.common import EstablishmentMixin -class EstablishmentListCreateView(EstablishmentMixin, generics.ListCreateAPIView): +class EstablishmentMixinViews: + """Establishment mixin.""" + + def get_queryset(self): + """Overrided method 'get_queryset'.""" + return models.Establishment.objects.published().with_base_related() + + +class EstablishmentListCreateView(EstablishmentMixinViews, generics.ListCreateAPIView): """Establishment list/create view.""" queryset = models.Establishment.objects.all() serializer_class = serializers.EstablishmentListCreateSerializer diff --git a/apps/establishment/views/common.py b/apps/establishment/views/common.py index 922882da..e69de29b 100644 --- a/apps/establishment/views/common.py +++ b/apps/establishment/views/common.py @@ -1,16 +0,0 @@ -"""Establishment app views.""" - -from rest_framework import permissions - -from establishment import models - - -class EstablishmentMixin: - """Establishment mixin.""" - - permission_classes = (permissions.AllowAny,) - - def get_queryset(self): - """Overridden method 'get_queryset'.""" - return models.Establishment.objects.published() \ - .prefetch_actual_employees() diff --git a/apps/establishment/views/web.py b/apps/establishment/views/web.py index 22ef3abc..f4558b71 100644 --- a/apps/establishment/views/web.py +++ b/apps/establishment/views/web.py @@ -1,5 +1,4 @@ """Establishment app views.""" - from django.conf import settings from django.contrib.gis.geos import Point from django.shortcuts import get_object_or_404 @@ -8,27 +7,50 @@ from rest_framework import generics, permissions from comment import models as comment_models from establishment import filters from establishment import models, serializers -from establishment.views import EstablishmentMixin from main import methods from main.models import MetaDataContent from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer from utils.pagination import EstablishmentPortionPagination -class EstablishmentListView(EstablishmentMixin, generics.ListAPIView): +class EstablishmentMixinView: + """Establishment mixin.""" + + permission_classes = (permissions.AllowAny,) + + def get_queryset(self): + """Overrided method 'get_queryset'.""" + return models.Establishment.objects.published().with_base_related().\ + annotate_in_favorites(self.request.user) + + +class EstablishmentListView(EstablishmentMixinView, generics.ListAPIView): """Resource for getting a list of establishments.""" - serializer_class = serializers.EstablishmentListSerializer + filter_class = filters.EstablishmentFilter + serializer_class = serializers.EstablishmentAllListSerializer def get_queryset(self): """Overridden method 'get_queryset'.""" qs = super(EstablishmentListView, self).get_queryset() - return qs.by_country_code(code=self.request.country_code) \ - .annotate_in_favorites(user=self.request.user) + if self.request.country_code: + qs = qs.by_country_code(self.request.country_code) + return qs + + +class EstablishmentRetrieveView(EstablishmentMixinView, generics.RetrieveAPIView): + """Resource for getting a establishment.""" + + lookup_field = 'slug' + serializer_class = serializers.EstablishmentDetailSerializer + + def get_queryset(self): + return super().get_queryset().with_extended_related() class EstablishmentRecentReviewListView(EstablishmentListView): """List view for last reviewed establishments.""" + pagination_class = EstablishmentPortionPagination def get_queryset(self): @@ -57,17 +79,6 @@ class EstablishmentSimilarListView(EstablishmentListView): return qs.similar(establishment_slug=self.kwargs.get('slug')) -class EstablishmentRetrieveView(EstablishmentMixin, generics.RetrieveAPIView): - """Resource for getting a establishment.""" - lookup_field = 'slug' - serializer_class = serializers.EstablishmentDetailSerializer - - def get_queryset(self): - """Override 'get_queryset' method.""" - return super(EstablishmentRetrieveView, self).get_queryset() \ - .annotate_in_favorites(self.request.user) - - class EstablishmentTypeListView(generics.ListAPIView): """Resource for getting a list of establishment types.""" diff --git a/apps/favorites/tests.py b/apps/favorites/tests.py index 208cf0db..99b01444 100644 --- a/apps/favorites/tests.py +++ b/apps/favorites/tests.py @@ -27,7 +27,7 @@ class BaseTestCase(APITestCase): news_type=self.test_news_type, description={"en-GB": "Description test news"}, playlist=1, start="2020-12-03 12:00:00", end="2020-12-13 12:00:00", - is_publish=True) + state=News.PUBLISHED, slug='test-news') self.test_content_type = ContentType.objects.get(app_label="news", model="news") diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index 555cf499..37d782de 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -1,7 +1,6 @@ """Location app common serializers.""" from django.contrib.gis.geos import Point from rest_framework import serializers - from location import models from utils.serializers import TranslatedField @@ -86,6 +85,7 @@ class CitySerializer(serializers.ModelSerializer): class AddressSerializer(serializers.ModelSerializer): """Address serializer.""" + city_id = serializers.PrimaryKeyRelatedField( source='city', queryset=models.City.objects.all()) @@ -104,7 +104,7 @@ class AddressSerializer(serializers.ModelSerializer): 'number', 'postal_code', 'geo_lon', - 'geo_lat' + 'geo_lat', ] def validate(self, attrs): @@ -128,3 +128,18 @@ class AddressSerializer(serializers.ModelSerializer): setattr(instance, 'geo_lon', float(0)) return super().to_representation(instance) + +class AddressSimpleSerializer(serializers.ModelSerializer): + """Serializer for address obj in related objects.""" + + class Meta: + """Meta class.""" + + model = models.Address + fields = ( + 'id', + 'street_name_1', + 'street_name_2', + 'number', + 'postal_code', + ) diff --git a/apps/news/migrations/0014_auto_20190927_0845.py b/apps/news/migrations/0014_auto_20190927_0845.py new file mode 100644 index 00000000..5a2b35fb --- /dev/null +++ b/apps/news/migrations/0014_auto_20190927_0845.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.4 on 2019-09-27 08:45 +from django.db import migrations +from django.core.validators import EMPTY_VALUES + + +def fill_slug(apps,schemaeditor): + News = apps.get_model('news', 'News') + for news in News.objects.all(): + if news.slug in EMPTY_VALUES: + news.slug = f'Slug_{news.id}' + news.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0013_auto_20190924_0806'), + ] + + operations = [ + migrations.RunPython(fill_slug, migrations.RunPython.noop) + ] diff --git a/apps/news/migrations/0015_auto_20190927_0853.py b/apps/news/migrations/0015_auto_20190927_0853.py new file mode 100644 index 00000000..e756e2e5 --- /dev/null +++ b/apps/news/migrations/0015_auto_20190927_0853.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-09-27 08:53 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0014_auto_20190927_0845'), + ] + + operations = [ + migrations.AlterField( + model_name='news', + name='slug', + field=models.SlugField(unique=True, verbose_name='News slug'), + ), + ] diff --git a/apps/news/migrations/0016_news_template.py b/apps/news/migrations/0016_news_template.py new file mode 100644 index 00000000..f85959ba --- /dev/null +++ b/apps/news/migrations/0016_news_template.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-09-27 13:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0015_auto_20190927_0853'), + ] + + operations = [ + migrations.AddField( + model_name='news', + name='template', + field=models.PositiveIntegerField(choices=[(0, 'newspaper'), (1, 'main.pdf.erb'), (2, 'main')], default=0), + ), + ] diff --git a/apps/news/migrations/0016_remove_news_author.py b/apps/news/migrations/0016_remove_news_author.py new file mode 100644 index 00000000..31ad12bb --- /dev/null +++ b/apps/news/migrations/0016_remove_news_author.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.4 on 2019-09-27 13:49 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0015_auto_20190927_0853'), + ] + + operations = [ + migrations.RemoveField( + model_name='news', + name='author', + ), + ] diff --git a/apps/news/migrations/0017_auto_20190927_1403.py b/apps/news/migrations/0017_auto_20190927_1403.py new file mode 100644 index 00000000..1886dcec --- /dev/null +++ b/apps/news/migrations/0017_auto_20190927_1403.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.4 on 2019-09-27 14:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0016_news_template'), + ] + + operations = [ + migrations.RemoveField( + model_name='news', + name='is_publish', + ), + migrations.AddField( + model_name='news', + name='state', + field=models.PositiveSmallIntegerField(choices=[(0, 'Waiting'), (1, 'Hidden'), (2, 'Published'), (3, 'Published exclusive')], default=0, verbose_name='State'), + ), + ] diff --git a/apps/news/migrations/0018_merge_20190927_1432.py b/apps/news/migrations/0018_merge_20190927_1432.py new file mode 100644 index 00000000..c4654943 --- /dev/null +++ b/apps/news/migrations/0018_merge_20190927_1432.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.4 on 2019-09-27 14:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0017_auto_20190927_1403'), + ('news', '0016_remove_news_author'), + ] + + operations = [ + ] diff --git a/apps/news/migrations/0019_news_author.py b/apps/news/migrations/0019_news_author.py new file mode 100644 index 00000000..41985255 --- /dev/null +++ b/apps/news/migrations/0019_news_author.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.4 on 2019-09-27 15:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0018_merge_20190927_1432'), + ] + + operations = [ + migrations.AddField( + model_name='news', + name='author', + field=models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='Author'), + ), + ] diff --git a/apps/news/models.py b/apps/news/models.py index 4b9176cd..ed40e09b 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework.reverse import reverse from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin +from random import sample as random_sample class NewsType(models.Model): @@ -40,7 +41,7 @@ class NewsQuerySet(models.QuerySet): now = timezone.now() return self.filter(models.Q(models.Q(end__gte=now) | models.Q(end__isnull=True)), - is_publish=True, start__lte=now) + state__in=self.model.PUBLISHED_STATES, start__lte=now) def with_related(self): """Return qs with related objects.""" @@ -50,6 +51,34 @@ class NewsQuerySet(models.QuerySet): class News(BaseAttributes, TranslatedFieldsMixin): """News model.""" + STR_FIELD_NAME = 'title' + + # TEMPLATE CHOICES + NEWSPAPER = 0 + MAIN_PDF_ERB = 1 + MAIN = 2 + + TEMPLATE_CHOICES = ( + (NEWSPAPER, 'newspaper'), + (MAIN_PDF_ERB, 'main.pdf.erb'), + (MAIN, 'main'), + ) + + # STATE CHOICES + WAITING = 0 + HIDDEN = 1 + PUBLISHED = 2 + PUBLISHED_EXCLUSIVE = 3 + + PUBLISHED_STATES = [PUBLISHED, PUBLISHED_EXCLUSIVE] + + STATE_CHOICES = ( + (WAITING, _('Waiting')), + (HIDDEN, _('Hidden')), + (PUBLISHED, _('Published')), + (PUBLISHED_EXCLUSIVE, _('Published exclusive')), + ) + news_type = models.ForeignKey(NewsType, on_delete=models.PROTECT, verbose_name=_('news type')) title = TJSONField(blank=True, null=True, default=None, @@ -64,17 +93,23 @@ class News(BaseAttributes, TranslatedFieldsMixin): start = models.DateTimeField(verbose_name=_('Start')) end = models.DateTimeField(blank=True, null=True, default=None, verbose_name=_('End')) - slug = models.SlugField(unique=True, max_length=50, null=True, - verbose_name=_('News slug'), editable=True,) + slug = models.SlugField(unique=True, max_length=50, + verbose_name=_('News slug')) playlist = models.IntegerField(_('playlist')) - is_publish = models.BooleanField(default=False, - verbose_name=_('Publish status')) + + # author = models.CharField(max_length=255, blank=True, null=True, + # default=None,verbose_name=_('Author')) + + state = models.PositiveSmallIntegerField(default=WAITING, choices=STATE_CHOICES, + verbose_name=_('State')) author = models.CharField(max_length=255, blank=True, null=True, default=None,verbose_name=_('Author')) + is_highlighted = models.BooleanField(default=False, verbose_name=_('Is highlighted')) # TODO: metadata_keys - описание ключей для динамического построения полей метаданных # TODO: metadata_values - Описание значений для динамических полей из MetadataKeys + template = models.PositiveIntegerField(choices=TEMPLATE_CHOICES, default=NEWSPAPER) address = models.ForeignKey('location.Address', blank=True, null=True, default=None, verbose_name=_('address'), on_delete=models.SET_NULL) @@ -96,6 +131,28 @@ class News(BaseAttributes, TranslatedFieldsMixin): def __str__(self): return f'news: {self.slug}' + @property + def is_publish(self): + return self.state in self.PUBLISHED_STATES + + @property + def list_also_like_news(self): + + # without "distinct" method the doubles are arising + like_news = News.objects.published().filter(news_type=self.news_type, tags__in=models.F("tags"))\ + .exclude(id=self.id).distinct() + + news_count = like_news.count() + + if news_count >= 6: + random_ids = random_sample(range(news_count), 6) + else: + random_ids = random_sample(range(news_count), news_count) + + news_list = [{"id": like_news[r].id, "slug": like_news[r].slug} for r in random_ids] + + return news_list + @property def web_url(self): return reverse('web:news:rud', kwargs={'slug': self.slug}) diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 8901b834..daee42b2 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -9,7 +9,7 @@ from location.serializers import CountrySimpleSerializer from main.serializers import MetaDataContentSerializer from news import models from utils.serializers import TranslatedField - +from account.serializers.common import UserSerializer class NewsTypeSerializer(serializers.ModelSerializer): """News type serializer.""" @@ -33,8 +33,6 @@ class NewsBaseSerializer(serializers.ModelSerializer): tags = MetaDataContentSerializer(read_only=True, many=True) gallery = ImageSerializer(read_only=True, many=True) - slug = serializers.SlugField(allow_blank=False, required=True, max_length=50) - class Meta: """Meta class.""" @@ -56,6 +54,9 @@ class NewsDetailSerializer(NewsBaseSerializer): description_translated = TranslatedField() country = CountrySimpleSerializer(read_only=True) + author = UserSerializer(source='created_by') + state_display = serializers.CharField(source='get_state_display', + read_only=True) class Meta(NewsBaseSerializer.Meta): """Meta class.""" @@ -66,8 +67,11 @@ class NewsDetailSerializer(NewsBaseSerializer): 'end', 'playlist', 'is_publish', + 'state', + 'state_display', 'author', 'country', + 'list_also_like_news', ) @@ -90,10 +94,11 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, news_type_id = serializers.PrimaryKeyRelatedField( source='news_type', write_only=True, queryset=models.NewsType.objects.all()) - country_id = serializers.PrimaryKeyRelatedField( source='country', write_only=True, queryset=location_models.Country.objects.all()) + template_display = serializers.CharField(source='get_template_display', + read_only=True) class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta): """Meta class.""" @@ -103,6 +108,8 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, 'description', 'news_type_id', 'country_id', + 'template', + 'template_display', ) diff --git a/apps/news/tests.py b/apps/news/tests.py index 7d6724d7..2e24ac45 100644 --- a/apps/news/tests.py +++ b/apps/news/tests.py @@ -26,7 +26,7 @@ class BaseTestCase(APITestCase): news_type=self.test_news_type, description={"en-GB": "Description test news"}, playlist=1, start=datetime.now() + timedelta(hours=-2), end=datetime.now() + timedelta(hours=2), - is_publish=True, slug='test-news-slug',) + state=News.PUBLISHED, slug='test-news-slug',) class NewsTestCase(BaseTestCase): diff --git a/apps/search_indexes/serializers.py b/apps/search_indexes/serializers.py index 480f509d..1d8e3ca3 100644 --- a/apps/search_indexes/serializers.py +++ b/apps/search_indexes/serializers.py @@ -46,6 +46,7 @@ class EstablishmentDocumentSerializer(DocumentSerializer): description_translated = serializers.SerializerMethodField(allow_null=True) + preview_image = serializers.URLField(source='preview_image_url') class Meta: """Meta class.""" @@ -53,7 +54,6 @@ class EstablishmentDocumentSerializer(DocumentSerializer): fields = ( 'id', 'name', - 'description', 'public_mark', 'toque_number', 'price_level', @@ -63,10 +63,34 @@ class EstablishmentDocumentSerializer(DocumentSerializer): 'collections', 'establishment_type', 'establishment_subtypes', - 'preview_image_url', + 'preview_image', 'slug', ) @staticmethod def get_description_translated(obj): return get_translated_value(obj.description) + + def to_representation(self, instance): + ret = super().to_representation(instance) + dict_merge = lambda a, b: a.update(b) or a + + ret['tags'] = map(lambda tag: dict_merge(tag, {'label_translated': get_translated_value(tag.pop('label'))}), + ret['tags']) + ret['establishment_subtypes'] = map( + lambda subtype: dict_merge(subtype, {'name_translated': get_translated_value(subtype.pop('name'))}), + ret['establishment_subtypes']) + if ret.get('establishment_type'): + ret['establishment_type']['name_translated'] = get_translated_value(ret['establishment_type'].pop('name')) + if ret.get('address'): + ret['address']['city']['country']['name_translated'] = get_translated_value( + ret['address']['city']['country'].pop('name')) + location = ret['address'].pop('location') + if location: + ret['address']['geo_lon'] = location['lon'] + ret['address']['geo_lat'] = location['lat'] + + ret['type'] = ret.pop('establishment_type') + ret['subtypes'] = ret.pop('establishment_subtypes') + + return ret \ No newline at end of file diff --git a/apps/search_indexes/urls.py b/apps/search_indexes/urls.py index 664e0b99..549e569d 100644 --- a/apps/search_indexes/urls.py +++ b/apps/search_indexes/urls.py @@ -6,5 +6,8 @@ from search_indexes import views router = routers.SimpleRouter() # router.register(r'news', views.NewsDocumentViewSet, basename='news') # temporarily disabled router.register(r'establishments', views.EstablishmentDocumentViewSet, basename='establishment') +router.register(r'mobile/establishments', views.EstablishmentDocumentViewSet, basename='establishment-mobile') +router.register(r'news', views.NewsDocumentViewSet, basename='news') urlpatterns = router.urls + diff --git a/apps/search_indexes/utils.py b/apps/search_indexes/utils.py index 93be9e81..0c1dd187 100644 --- a/apps/search_indexes/utils.py +++ b/apps/search_indexes/utils.py @@ -17,4 +17,6 @@ def get_translated_value(value): return None elif not isinstance(value, dict): field_dict = value.to_dict() + elif isinstance(value, dict): + field_dict = value return field_dict.get(get_current_language()) diff --git a/apps/search_indexes/views.py b/apps/search_indexes/views.py index 72cab583..a69caf1f 100644 --- a/apps/search_indexes/views.py +++ b/apps/search_indexes/views.py @@ -4,7 +4,8 @@ from django_elasticsearch_dsl_drf import constants from django_elasticsearch_dsl_drf.filter_backends import (FilteringFilterBackend, GeoSpatialFilteringFilterBackend) from django_elasticsearch_dsl_drf.viewsets import BaseDocumentViewSet -from django_elasticsearch_dsl_drf.pagination import PageNumberPagination + +from utils.pagination import ProjectPageNumberPagination from search_indexes import serializers, filters from search_indexes.documents import EstablishmentDocument, NewsDocument @@ -14,7 +15,7 @@ class NewsDocumentViewSet(BaseDocumentViewSet): document = NewsDocument lookup_field = 'slug' - pagination_class = PageNumberPagination + pagination_class = ProjectPageNumberPagination permission_classes = (permissions.AllowAny,) serializer_class = serializers.NewsDocumentSerializer ordering = ('id',) @@ -40,7 +41,7 @@ class EstablishmentDocumentViewSet(BaseDocumentViewSet): document = EstablishmentDocument lookup_field = 'slug' - pagination_class = PageNumberPagination + pagination_class = ProjectPageNumberPagination permission_classes = (permissions.AllowAny,) serializer_class = serializers.EstablishmentDocumentSerializer ordering = ('id',) diff --git a/apps/utils/middleware.py b/apps/utils/middleware.py index 096f8474..f3936169 100644 --- a/apps/utils/middleware.py +++ b/apps/utils/middleware.py @@ -19,6 +19,7 @@ def parse_cookies(get_response): cookie_dict = request.COOKIES # processing locale cookie locale = get_locale(cookie_dict) + # todo: don't use DB!!! Use cache if not Language.objects.filter(locale=locale).exists(): locale = TranslationSettings.get_solo().default_language translation.activate(locale) diff --git a/apps/utils/querysets.py b/apps/utils/querysets.py index f25507f7..bf2816f2 100644 --- a/apps/utils/querysets.py +++ b/apps/utils/querysets.py @@ -1,6 +1,8 @@ """Utils QuerySet Mixins""" from django.db import models - +from django.db.models import Q, Sum, F +from functools import reduce +from operator import add from utils.methods import get_contenttype @@ -15,3 +17,40 @@ class ContentTypeQuerySetMixin(models.QuerySet): """Filter QuerySet by ContentType.""" return self.filter(content_type=get_contenttype(app_label=app_label, model=model)) + + +class RelatedObjectsCountMixin(models.QuerySet): + """QuerySet for ManyToMany related count filter""" + + def _get_related_objects_names(self): + """Get all related objects (with reversed)""" + related_objects_names = [] + + for related_object in self.model._meta.related_objects: + if isinstance(related_object, models.ManyToManyRel): + related_objects_names.append(related_object.name) + + return related_objects_names + + def _annotate_related_objects_count(self): + """Annotate all related objects to queryset""" + annotations = {} + for related_object in self._get_related_objects_names(): + annotations[f"{related_object}_count"] = models.Count(f"{related_object}") + + return self.annotate(**annotations) + + def filter_related_gt(self, count): + """QuerySet filter by related objects count""" + q_objects = Q() + for related_object in self._get_related_objects_names(): + q_objects.add(Q(**{f"{related_object}_count__gt": count}), Q.OR) + + return self._annotate_related_objects_count().filter(q_objects) + + def filter_all_related_gt(self, count): + """Queryset filter by all related objects count""" + exp =reduce(add, [F(f"{related_object}_count") for related_object in self._get_related_objects_names()]) + return self._annotate_related_objects_count()\ + .annotate(all_related_count=exp)\ + .filter(all_related_count__gt=count) diff --git a/apps/utils/tests.py b/apps/utils/tests.py index 0ca77b6b..0eaf343d 100644 --- a/apps/utils/tests.py +++ b/apps/utils/tests.py @@ -1,5 +1,5 @@ import pytz -from datetime import datetime +from datetime import datetime, timedelta from rest_framework.test import APITestCase from rest_framework import status @@ -52,17 +52,18 @@ class TranslateFieldTests(BaseTestCase): }, description={"en-GB": "Test description"}, playlist=1, - start=datetime.now(pytz.utc), - end=datetime.now(pytz.utc), - is_publish=True, - news_type=self.news_type + start=datetime.now(pytz.utc) + timedelta(hours=-13), + end=datetime.now(pytz.utc) + timedelta(hours=13), + news_type=self.news_type, + slug='test', + state=News.PUBLISHED, ) def test_model_field(self): self.assertIsNotNone(getattr(self.news_item, "title_translated", None)) def test_read_locale(self): - response = self.client.get(f"/api/web/news/{self.news_item.id}/", format='json') + response = self.client.get(f"/api/web/news/slug/{self.news_item.slug}/", format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) news_data = response.json() @@ -94,6 +95,7 @@ class BaseAttributeTests(BaseTestCase): response_data = response.json() self.assertIn("id", response_data) + self.assertIsInstance(response_data['id'], int) employee = Employee.objects.get(id=response_data['id']) @@ -117,7 +119,7 @@ class BaseAttributeTests(BaseTestCase): 'name': 'Test new name' } - response = self.client.patch('/api/back/establishments/employees/1/', data=update_data) + response = self.client.patch(f'/api/back/establishments/employees/{employee.pk}/', data=update_data) self.assertEqual(response.status_code, status.HTTP_200_OK) employee.refresh_from_db()