diff --git a/apps/account/serializers/common.py b/apps/account/serializers/common.py index e8d6ba30..e6076117 100644 --- a/apps/account/serializers/common.py +++ b/apps/account/serializers/common.py @@ -77,6 +77,23 @@ class UserSerializer(serializers.ModelSerializer): return instance +class UserBaseSerializer(serializers.ModelSerializer): + """Serializer is used to display brief information about the user.""" + + fullname = serializers.CharField(source='get_full_name', read_only=True) + + class Meta: + """Meta class.""" + + model = models.User + fields = ( + 'fullname', + 'cropped_image_url', + 'image_url', + ) + read_only_fields = fields + + class ChangePasswordSerializer(serializers.ModelSerializer): """Serializer for model User.""" diff --git a/apps/collection/serializers/common.py b/apps/collection/serializers/common.py index f7319561..78612a55 100644 --- a/apps/collection/serializers/common.py +++ b/apps/collection/serializers/common.py @@ -4,11 +4,24 @@ from collection import models from location import models as location_models -class CollectionSerializer(serializers.ModelSerializer): - """Collection serializer""" +class CollectionBaseSerializer(serializers.ModelSerializer): + """Collection base serializer""" # RESPONSE description_translated = serializers.CharField(read_only=True, allow_null=True) + class Meta: + model = models.Collection + fields = [ + 'id', + 'name', + 'description_translated', + 'image_url', + 'slug', + ] + + +class CollectionSerializer(CollectionBaseSerializer): + """Collection serializer""" # COMMON block_size = serializers.JSONField() is_publish = serializers.BooleanField() @@ -24,18 +37,13 @@ class CollectionSerializer(serializers.ModelSerializer): class Meta: model = models.Collection - fields = [ - 'id', - 'name', - 'description_translated', + fields = CollectionBaseSerializer.Meta.fields + [ 'start', 'end', - 'image_url', 'is_publish', 'on_top', 'country', 'block_size', - 'slug', ] diff --git a/apps/collection/urls/common.py b/apps/collection/urls/common.py index 7ffa50cf..36801ac5 100644 --- a/apps/collection/urls/common.py +++ b/apps/collection/urls/common.py @@ -7,6 +7,7 @@ app_name = 'collection' urlpatterns = [ path('', views.CollectionHomePageView.as_view(), name='list'), + path('/', views.CollectionDetailView.as_view(), name='detail'), path('/establishments/', views.CollectionEstablishmentListView.as_view(), name='detail'), diff --git a/apps/collection/views/common.py b/apps/collection/views/common.py index fd2a4584..16a42f58 100644 --- a/apps/collection/views/common.py +++ b/apps/collection/views/common.py @@ -12,7 +12,14 @@ from collection.serializers import common as serializers class CollectionViewMixin(generics.GenericAPIView): """Mixin for Collection view""" model = models.Collection - queryset = models.Collection.objects.all() + permission_classes = (permissions.AllowAny,) + + def get_queryset(self): + """Override get_queryset method.""" + return models.Collection.objects.published() \ + .by_country_code(code=self.request.country_code) \ + .filter_all_related_gt(3) \ + .order_by('-on_top', '-modified') class GuideViewMixin(generics.GenericAPIView): @@ -23,41 +30,22 @@ class GuideViewMixin(generics.GenericAPIView): # Views # Collections -class CollectionListView(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)\ - .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 CollectionDetailView(CollectionViewMixin, generics.RetrieveAPIView): + """Retrieve detail of Collection instance.""" + lookup_field = 'slug' + serializer_class = serializers.CollectionBaseSerializer -class CollectionEstablishmentListView(CollectionListView): +class CollectionEstablishmentListView(CollectionHomePageView): """Retrieve list of establishment for collection.""" - permission_classes = (permissions.AllowAny,) + lookup_field = 'slug' pagination_class = ProjectPageNumberPagination serializer_class = EstablishmentBaseSerializer - lookup_field = 'slug' def get_queryset(self): """ diff --git a/apps/news/migrations/0020_remove_news_author.py b/apps/news/migrations/0020_remove_news_author.py new file mode 100644 index 00000000..95f376c9 --- /dev/null +++ b/apps/news/migrations/0020_remove_news_author.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.4 on 2019-10-02 09:18 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0019_news_author'), + ] + + operations = [ + migrations.RemoveField( + model_name='news', + name='author', + ), + ] diff --git a/apps/news/models.py b/apps/news/models.py index c128b8ed..efa0b566 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -1,11 +1,9 @@ """News app models.""" -from django.contrib.contenttypes import fields as generic from django.db import models +from django.contrib.contenttypes import fields as generic from django.utils import timezone from django.utils.translation import gettext_lazy as _ from rest_framework.reverse import reverse -from sorl.thumbnail import delete - from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin from random import sample as random_sample @@ -29,10 +27,21 @@ class NewsType(models.Model): class NewsQuerySet(models.QuerySet): """QuerySet for model News""" + def with_base_related(self): + """Return qs with related objects.""" + return self.select_related('news_type', 'country').prefetch_related('tags') + + def with_extended_related(self): + """Return qs with related objects.""" + return self.select_related('created_by') + def by_type(self, news_type): """Filter News by type""" return self.filter(news_type__name=news_type) + def by_tags(self, tags): + return self.filter(tags__in=tags) + def by_country_code(self, code): """Filter collection by country code.""" return self.filter(country__code=code) @@ -44,9 +53,16 @@ class NewsQuerySet(models.QuerySet): models.Q(end__isnull=True)), state__in=self.model.PUBLISHED_STATES, start__lte=now) - def with_related(self): - """Return qs with related objects.""" - return self.select_related('news_type', 'country').prefetch_related('tags') + # todo: filter by best score + # todo: filter by country? + def should_read(self, news): + return self.model.objects.exclude(pk=news.pk).published().\ + with_base_related().by_type(news.news_type).distinct().order_by('?') + + def same_theme(self, news): + return self.model.objects.exclude(pk=news.pk).published().\ + with_base_related().by_type(news.news_type).\ + by_tags(news.tags.all()).distinct().order_by('-start') class News(BaseAttributes, TranslatedFieldsMixin): @@ -97,19 +113,16 @@ class News(BaseAttributes, TranslatedFieldsMixin): slug = models.SlugField(unique=True, max_length=50, verbose_name=_('News slug')) playlist = models.IntegerField(_('playlist')) - - # 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 + image_url = models.URLField(blank=True, null=True, default=None, + verbose_name=_('Image URL path')) + preview_image_url = models.URLField(blank=True, null=True, default=None, + verbose_name=_('Preview image URL path')) template = models.PositiveIntegerField(choices=TEMPLATE_CHOICES, default=NEWSPAPER) address = models.ForeignKey('location.Address', blank=True, null=True, default=None, verbose_name=_('address'), @@ -119,8 +132,6 @@ class News(BaseAttributes, TranslatedFieldsMixin): verbose_name=_('country')) tags = generic.GenericRelation(to='main.MetaDataContent') - gallery = models.ManyToManyField('gallery.Image', through='news.NewsGallery') - objects = NewsQuerySet.as_manager() class Meta: @@ -136,51 +147,15 @@ class News(BaseAttributes, TranslatedFieldsMixin): 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}) @property - def original_images(self): - return self.gallery.originals() + def should_read(self): + return self.__class__.objects.should_read(self)[:3] + @property + def same_theme(self): + return self.__class__.objects.same_theme(self)[:3] -class NewsGalleryQuerySet(models.QuerySet): - """QuerySet for model News""" - - -class NewsGallery(models.Model): - - news = models.ForeignKey(News, null=True, - related_name='news_gallery', - on_delete=models.SET_NULL, - verbose_name=_('news')) - image = models.ForeignKey('gallery.Image', null=True, - related_name='news_gallery', - on_delete=models.SET_NULL, - verbose_name=_('gallery')) - - objects = NewsGalleryQuerySet.as_manager() - - class Meta: - """NewsGallery meta class.""" - verbose_name = _('news gallery') - verbose_name_plural = _('news galleries') diff --git a/apps/news/serializers.py b/apps/news/serializers.py index e4b8c1c7..c473be1d 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -1,10 +1,6 @@ """News app common serializers.""" -from django.utils.translation import gettext_lazy as _ from rest_framework import serializers - -from account.serializers.common import UserSerializer -from gallery.models import Image -from gallery.serializers import ImageSerializer +from account.serializers.common import UserBaseSerializer from location import models as location_models from location.serializers import CountrySimpleSerializer from main.serializers import MetaDataContentSerializer @@ -12,53 +8,6 @@ from news import models from utils.serializers import TranslatedField, ProjectModelSerializer -class NewsCropImageSerializer(ImageSerializer): - """Serializer for returning crop images of news image.""" - orientation_display = serializers.CharField(source='get_orientation_display', - read_only=True) - web_url = serializers.SerializerMethodField() - mobile_url = serializers.SerializerMethodField() - - class Meta: - model = Image - fields = [ - 'id', - 'title', - 'orientation_display', - 'web_url', - 'mobile_url', - ] - extra_kwargs = { - 'orientation': {'write_only': True} - } - - def get_web_url(self, obj): - """Return URL of cropped image by thumbnail.""" - return obj.get_image_url('news_promo_horizontal_web') - - def get_mobile_url(self, obj): - """Return URL of cropped image by thumbnail.""" - return obj.get_image_url('news_promo_horizontal_mobile') - - -class NewsImageSerializer(ImageSerializer): - """News images""" - url = serializers.URLField(source='image.url', read_only=True) - crops = NewsCropImageSerializer(source='childs', allow_null=True, many=True) - - class Meta: - model = Image - fields = [ - 'id', - 'title', - 'url', - 'crops', - ] - extra_kwargs = { - 'orientation': {'write_only': True} - } - - class NewsTypeSerializer(serializers.ModelSerializer): """News type serializer.""" @@ -79,7 +28,6 @@ class NewsBaseSerializer(ProjectModelSerializer): # related fields news_type = NewsTypeSerializer(read_only=True) tags = MetaDataContentSerializer(read_only=True, many=True) - gallery = NewsImageSerializer(source='original_images', read_only=True, many=True) class Meta: """Meta class.""" @@ -90,10 +38,11 @@ class NewsBaseSerializer(ProjectModelSerializer): 'title_translated', 'subtitle_translated', 'is_highlighted', + 'image_url', + 'preview_image_url', 'news_type', 'tags', 'slug', - 'gallery', ) @@ -102,8 +51,7 @@ class NewsDetailSerializer(NewsBaseSerializer): description_translated = TranslatedField() country = CountrySimpleSerializer(read_only=True) - # todo: check the data redundancy - author = UserSerializer(source='created_by', read_only=True) + author = UserBaseSerializer(source='created_by', read_only=True) state_display = serializers.CharField(source='get_state_display', read_only=True) @@ -120,7 +68,21 @@ class NewsDetailSerializer(NewsBaseSerializer): 'state_display', 'author', 'country', - 'list_also_like_news', + ) + + +class NewsDetailWebSerializer(NewsDetailSerializer): + """News detail serializer for web users..""" + + same_theme = NewsBaseSerializer(many=True, read_only=True) + should_read = NewsBaseSerializer(many=True, read_only=True) + + class Meta(NewsDetailSerializer.Meta): + """Meta class.""" + + fields = NewsDetailSerializer.Meta.fields + ( + 'same_theme', + 'should_read', ) @@ -161,40 +123,3 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, 'template_display', ) - -class NewsBackOfficeGallerySerializer(serializers.ModelSerializer): - """Serializer class for model NewsGallery.""" - class Meta: - """Meta class""" - model = models.NewsGallery - fields = [ - 'id', - ] - - def get_request_kwargs(self): - """Get url kwargs from request.""" - return self.context.get('request').parser_context.get('kwargs') - - def validate(self, attrs): - """Override validate method.""" - news_pk = self.get_request_kwargs().get('pk') - image_id = self.get_request_kwargs().get('image_id') - - news_qs = models.News.objects.filter(pk=news_pk) - image_qs = Image.objects.filter(id=image_id) - - if not news_qs.exists(): - raise serializers.ValidationError({'detail': _('News not found')}) - if not image_qs.exists(): - raise serializers.ValidationError({'detail': _('Image not found')}) - - news = news_qs.first() - image = image_qs.first() - - if news.news_gallery.filter(image=image).exists(): - raise serializers.ValidationError({'detail': _('Image is already added')}) - - attrs['news'] = news - attrs['image'] = image - - return attrs diff --git a/apps/news/views.py b/apps/news/views.py index ba998c11..d398f8f8 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -15,7 +15,7 @@ class NewsMixinView: def get_queryset(self, *args, **kwargs): """Override get_queryset method.""" - qs = models.News.objects.with_related().published()\ + qs = models.News.objects.published().with_base_related()\ .order_by('-is_highlighted', '-created') if self.request.country_code: qs = qs.by_country_code(self.request.country_code) @@ -32,7 +32,11 @@ class NewsDetailView(NewsMixinView, generics.RetrieveAPIView): """News detail view.""" lookup_field = 'slug' - serializer_class = serializers.NewsDetailSerializer + serializer_class = serializers.NewsDetailWebSerializer + + def get_queryset(self): + """Override get_queryset method.""" + return super().get_queryset().with_extended_related() class NewsTypeListView(generics.ListAPIView): @@ -48,7 +52,7 @@ class NewsBackOfficeMixinView: """News back office mixin view.""" permission_classes = (permissions.IsAuthenticated,) - queryset = models.News.objects.with_related() \ + queryset = models.News.objects.with_base_related() \ .order_by('-is_highlighted', '-created') @@ -65,6 +69,10 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView, return self.create_serializers_class return super().get_serializer_class() + def get_queryset(self): + """Override get_queryset method.""" + return super().get_queryset().with_extended_related() + class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, generics.CreateAPIView,