diff --git a/apps/location/serializers/common.py b/apps/location/serializers/common.py index 63fe39e1..555cf499 100644 --- a/apps/location/serializers/common.py +++ b/apps/location/serializers/common.py @@ -1,7 +1,9 @@ +"""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 class CountrySerializer(serializers.ModelSerializer): @@ -20,6 +22,18 @@ class CountrySerializer(serializers.ModelSerializer): ] +class CountrySimpleSerializer(serializers.ModelSerializer): + """Simple country serializer.""" + + name_translated = TranslatedField() + + class Meta: + """Meta class.""" + + model = models.Country + fields = ('id', 'code', 'name_translated') + + class RegionSerializer(serializers.ModelSerializer): """Region serializer""" diff --git a/apps/main/serializers.py b/apps/main/serializers.py index f2beb768..10d75f2b 100644 --- a/apps/main/serializers.py +++ b/apps/main/serializers.py @@ -1,9 +1,9 @@ """Main app serializers.""" from rest_framework import serializers - from advertisement.serializers.web import AdvertisementSerializer from location.serializers import CountrySerializer from main import models +from utils.serializers import TranslatedField class FeatureSerializer(serializers.ModelSerializer): @@ -109,16 +109,16 @@ class AwardSerializer(AwardBaseSerializer): class MetaDataContentSerializer(serializers.ModelSerializer): - id = serializers.IntegerField(source='metadata.id', read_only=True, ) - label_translated = serializers.CharField( - source='metadata.label_translated', read_only=True, allow_null=True) + """MetaData content serializer.""" + + id = serializers.IntegerField(source='metadata.id', read_only=True) + label_translated = TranslatedField(source='metadata.label_translated') class Meta: + """Meta class.""" + model = models.MetaDataContent - fields = [ - 'id', - 'label_translated', - ] + fields = ('id', 'label_translated') class CurrencySerializer(serializers.ModelSerializer): diff --git a/apps/news/migrations/0010_auto_20190923_1131.py b/apps/news/migrations/0010_auto_20190923_1131.py new file mode 100644 index 00000000..0bd6fd24 --- /dev/null +++ b/apps/news/migrations/0010_auto_20190923_1131.py @@ -0,0 +1,54 @@ +# Generated by Django 2.2.4 on 2019-09-23 11:31 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0009_auto_20190901_1032'), + ] + + operations = [ + migrations.AddField( + model_name='news', + name='author', + field=models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='Author'), + ), + migrations.AddField( + model_name='news', + name='image_url', + field=models.URLField(blank=True, default=None, null=True, verbose_name='Image URL path'), + ), + migrations.AddField( + model_name='news', + name='preview_image_url', + field=models.URLField(blank=True, default=None, null=True, verbose_name='Preview image URL path'), + ), + migrations.AlterField( + model_name='news', + name='address', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.Address', verbose_name='address'), + ), + migrations.AlterField( + model_name='news', + name='country', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='location.Country', verbose_name='country'), + ), + migrations.AlterField( + model_name='news', + name='end', + field=models.DateTimeField(verbose_name='End'), + ), + migrations.AlterField( + model_name='news', + name='news_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='news.NewsType', verbose_name='news type'), + ), + migrations.AlterField( + model_name='news', + name='start', + field=models.DateTimeField(verbose_name='Start'), + ), + ] diff --git a/apps/news/migrations/0011_auto_20190923_1134.py b/apps/news/migrations/0011_auto_20190923_1134.py new file mode 100644 index 00000000..859cb636 --- /dev/null +++ b/apps/news/migrations/0011_auto_20190923_1134.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.4 on 2019-09-23 11:34 + +from django.db import migrations +from django.conf import settings + + +def copy_image_links(apps, schemaeditor): + News = apps.get_model('news', 'News') + for news in News.objects.all(): + if news.image: + news.image_url = f'{settings.SCHEMA_URI}://{settings.DOMAIN_URI}{news.image.image.url}' + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0010_auto_20190923_1131'), + ] + + operations = [ + migrations.RunPython(copy_image_links, migrations.RunPython.noop), + + migrations.RemoveField( + model_name='news', + name='image', + ), + ] diff --git a/apps/news/models.py b/apps/news/models.py index 3be34261..c132f6fc 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -1,25 +1,31 @@ """News app models.""" 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 utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin class NewsType(models.Model): """NewsType model.""" + name = models.CharField(_('name'), max_length=250) class Meta: + """Meta class.""" + verbose_name_plural = _('news types') verbose_name = _('news type') def __str__(self): + """Overrided __str__ method.""" return self.name class NewsQuerySet(models.QuerySet): """QuerySet for model News""" + def by_type(self, news_type): """Filter News by type""" return self.filter(news_type__name=news_type) @@ -30,48 +36,56 @@ class NewsQuerySet(models.QuerySet): def published(self): """Return only published news""" - return self.filter(is_publish=True) + now = timezone.now() + return self.filter(is_publish=True, start__lte=now, end__gte=now) + + def with_related(self): + """Return qs with related objects.""" + return self.select_related('news_type', 'country').prefetch_related('tags') class News(BaseAttributes, TranslatedFieldsMixin): """News model.""" - image = models.ForeignKey( - 'gallery.Image', null=True, blank=True, default=None, - verbose_name=_('News image'), on_delete=models.CASCADE) - news_type = models.ForeignKey( - NewsType, verbose_name=_('news type'), on_delete=models.CASCADE) - - title = TJSONField( - _('title'), null=True, blank=True, - default=None, help_text='{"en-GB":"some text"}') - subtitle = TJSONField( - _('subtitle'), null=True, blank=True, - default=None, help_text='{"en-GB":"some text"}' - ) - description = TJSONField( - _('description'), null=True, blank=True, - default=None, help_text='{"en-GB":"some text"}' - ) - start = models.DateTimeField(_('start')) - end = models.DateTimeField(_('end')) + news_type = models.ForeignKey(NewsType, on_delete=models.PROTECT, + verbose_name=_('news type')) + title = TJSONField(blank=True, null=True, default=None, + verbose_name=_('title'), + help_text='{"en-GB":"some text"}') + subtitle = TJSONField(blank=True, null=True, default=None, + verbose_name=_('subtitle'), + help_text='{"en-GB":"some text"}') + description = TJSONField(blank=True, null=True, default=None, + verbose_name=_('description'), + help_text='{"en-GB":"some text"}') + start = models.DateTimeField(verbose_name=_('Start')) + end = models.DateTimeField(verbose_name=_('End')) playlist = models.IntegerField(_('playlist')) - address = models.ForeignKey( - 'location.Address', verbose_name=_('address'), blank=True, - null=True, default=None, on_delete=models.CASCADE) - is_publish = models.BooleanField( - default=False, verbose_name=_('Publish status')) - country = models.ForeignKey( - 'location.Country', blank=True, null=True, - verbose_name=_('country'), on_delete=models.CASCADE) - is_highlighted = models.BooleanField( - default=False, verbose_name=_('Is highlighted')) + 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')) + 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')) + address = models.ForeignKey('location.Address', blank=True, null=True, + default=None, verbose_name=_('address'), + on_delete=models.SET_NULL) + country = models.ForeignKey('location.Country', blank=True, null=True, + on_delete=models.SET_NULL, + verbose_name=_('country')) + tags = generic.GenericRelation(to='main.MetaDataContent') objects = NewsQuerySet.as_manager() class Meta: + """Meta class.""" + verbose_name = _('news') verbose_name_plural = _('news') diff --git a/apps/news/serializers.py b/apps/news/serializers.py new file mode 100644 index 00000000..c0d424ad --- /dev/null +++ b/apps/news/serializers.py @@ -0,0 +1,101 @@ +"""News app common serializers.""" +from rest_framework import serializers +from location import models as location_models +from location.serializers import CountrySimpleSerializer +from main.serializers import MetaDataContentSerializer +from news import models +from utils.serializers import TranslatedField + + +class NewsTypeSerializer(serializers.ModelSerializer): + """News type serializer.""" + + class Meta: + """Meta class.""" + + model = models.NewsType + fields = ('id', 'name') + + +class NewsBaseSerializer(serializers.ModelSerializer): + """Base serializer for News model.""" + + # read only fields + title_translated = TranslatedField() + subtitle_translated = TranslatedField() + + # related fields + news_type = NewsTypeSerializer(read_only=True) + tags = MetaDataContentSerializer(read_only=True, many=True) + + class Meta: + """Meta class.""" + + model = models.News + fields = ( + 'id', + 'title_translated', + 'subtitle_translated', + 'image_url', + 'image_url', + 'preview_image_url', + 'news_type', + 'tags', + ) + + +class NewsDetailSerializer(NewsBaseSerializer): + """News detail serializer.""" + + description_translated = TranslatedField() + country = CountrySimpleSerializer(read_only=True) + + class Meta(NewsBaseSerializer.Meta): + """Meta class.""" + + fields = NewsBaseSerializer.Meta.fields + ( + 'description_translated', + 'start', + 'end', + 'playlist', + 'is_highlighted', + 'is_publish', + 'author', + 'country', + ) + + +class NewsBackOfficeBaseSerializer(NewsBaseSerializer): + """News back office base serializer.""" + + class Meta(NewsBaseSerializer.Meta): + """Meta class.""" + + fields = NewsBaseSerializer.Meta.fields + ( + 'title', + 'subtitle', + ) + + +class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, + NewsDetailSerializer): + """News detail serializer for back-office users.""" + + 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()) + + class Meta(NewsBackOfficeBaseSerializer.Meta, NewsDetailSerializer.Meta): + """Meta class.""" + + fields = NewsBackOfficeBaseSerializer.Meta.fields + \ + NewsDetailSerializer.Meta.fields + ( + 'description', + 'news_type_id', + 'country_id', + ) + diff --git a/apps/news/serializers/__init__.py b/apps/news/serializers/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/news/serializers/common.py b/apps/news/serializers/common.py deleted file mode 100644 index b64a7fc9..00000000 --- a/apps/news/serializers/common.py +++ /dev/null @@ -1,76 +0,0 @@ -"""News app common serializers.""" -from rest_framework import serializers -from gallery import models as gallery_models -from location.models import Address -from location.serializers import AddressSerializer -from news import models - - -class NewsTypeSerializer(serializers.ModelSerializer): - """News type serializer.""" - class Meta: - model = models.NewsType - fields = [ - 'id', - 'name' - ] - - -class NewsSerializer(serializers.ModelSerializer): - """News serializer.""" - - address = AddressSerializer() - title_translated = serializers.CharField(read_only=True, allow_null=True) - subtitle_translated = serializers.CharField(read_only=True, allow_null=True) - description_translated = serializers.CharField(read_only=True, allow_null=True) - image_url = serializers.ImageField(source='image.image', allow_null=True) - - class Meta: - model = models.News - fields = [ - 'id', - 'news_type', - 'start', - 'end', - 'playlist', - 'address', - 'is_highlighted', - 'image_url', - # Localized fields - 'title_translated', - 'subtitle_translated', - 'description_translated', - ] - - -class NewsCreateUpdateSerializer(NewsSerializer): - """News update serializer.""" - title = serializers.JSONField() - subtitle = serializers.JSONField() - description = serializers.JSONField() - image = serializers.PrimaryKeyRelatedField( - queryset=gallery_models.Image.objects.all(), required=True,) - news_type = serializers.PrimaryKeyRelatedField( - queryset=models.NewsType.objects.all(), write_only=True) - address = serializers.PrimaryKeyRelatedField( - queryset=Address.objects.all(), write_only=True) - - class Meta: - model = models.News - read_only_fields = [ - 'id' - ] - fields = [ - 'id', - 'news_type', - 'title', - 'subtitle', - 'description', - 'start', - 'end', - 'playlist', - 'address', - 'image', - 'is_publish', - 'country' - ] diff --git a/apps/news/serializers/web.py b/apps/news/serializers/web.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/news/urls/back.py b/apps/news/urls/back.py new file mode 100644 index 00000000..8522592e --- /dev/null +++ b/apps/news/urls/back.py @@ -0,0 +1,11 @@ +"""News app urlpatterns for backoffice""" +from django.urls import path +from news import views + +app_name = 'news' + +urlpatterns = [ + path('', views.NewsBackOfficeLCView.as_view(), name='list-create'), + path('/', views.NewsBackOfficeRUDView.as_view(), + name='retrieve-update-destroy'), +] \ No newline at end of file diff --git a/apps/news/urls/web.py b/apps/news/urls/web.py index 1dbcc0fe..9ec7e644 100644 --- a/apps/news/urls/web.py +++ b/apps/news/urls/web.py @@ -1,12 +1,11 @@ -"""Location app urlconf.""" +"""News app urlconf.""" from django.urls import path - -from news.views import common +from news import views app_name = 'news' urlpatterns = [ - path('', common.NewsListView.as_view(), name='list'), - path('/', common.NewsDetailView.as_view(), name='rud'), - path('type/', common.NewsTypeListView.as_view(), name='type'), + path('', views.NewsListView.as_view(), name='list'), + path('/', views.NewsDetailView.as_view(), name='rud'), + path('types/', views.NewsTypeListView.as_view(), name='type'), ] diff --git a/apps/news/views.py b/apps/news/views.py new file mode 100644 index 00000000..7d6da3f7 --- /dev/null +++ b/apps/news/views.py @@ -0,0 +1,68 @@ +"""News app views.""" +from rest_framework import generics, permissions +from news import filters, models, serializers + + +class NewsMixinView: + """News mixin.""" + + permission_classes = (permissions.AllowAny, ) + serializer_class = serializers.NewsBaseSerializer + + def get_queryset(self, *args, **kwargs): + """Override get_queryset method.""" + qs = models.News.objects.with_related().published()\ + .order_by('-is_highlighted', '-created') + if self.request.country_code: + qs = qs.by_country_code(self.request.country_code) + return qs + + +class NewsListView(NewsMixinView, generics.ListAPIView): + """News list view.""" + + filter_class = filters.NewsListFilterSet + + +class NewsDetailView(NewsMixinView, generics.RetrieveAPIView): + """News detail view.""" + + serializer_class = serializers.NewsDetailSerializer + + +class NewsTypeListView(generics.ListAPIView): + """NewsType list view.""" + + pagination_class = None + permission_classes = (permissions.AllowAny, ) + queryset = models.NewsType.objects.all() + serializer_class = serializers.NewsTypeSerializer + + +class NewsBackOfficeMixinView: + """News back office mixin view.""" + + permission_classes = (permissions.IsAuthenticated,) + queryset = models.News.objects.with_related() \ + .order_by('-is_highlighted', '-created') + + +class NewsBackOfficeLCView(NewsBackOfficeMixinView, + generics.ListCreateAPIView): + """Resource for a list of news for back-office users.""" + + serializer_class = serializers.NewsBackOfficeBaseSerializer + create_serializers_class = serializers.NewsBackOfficeDetailSerializer + + def get_serializer_class(self): + if self.request.method == 'POST': + return self.create_serializers_class + return super().get_serializer_class() + + +class NewsBackOfficeRUDView(NewsBackOfficeMixinView, + generics.RetrieveUpdateDestroyAPIView): + """Resource for detailed information about news for back-office users.""" + + serializer_class = serializers.NewsBackOfficeDetailSerializer + diff --git a/apps/news/views/__init__.py b/apps/news/views/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/news/views/common.py b/apps/news/views/common.py deleted file mode 100644 index cf3c7e29..00000000 --- a/apps/news/views/common.py +++ /dev/null @@ -1,38 +0,0 @@ -"""News app common app.""" -from rest_framework import generics, permissions - -from news import filters, models -from news.serializers import common as serializers -from utils.views import JWTGenericViewMixin - - -class NewsMixin: - """News mixin.""" - - permission_classes = (permissions.AllowAny, ) - serializer_class = serializers.NewsSerializer - - def get_queryset(self, *args, **kwargs): - """Override get_queryset method""" - return models.News.objects.published() \ - .by_country_code(code=self.request.country_code) \ - .order_by('-is_highlighted', '-created') - - -class NewsListView(NewsMixin, generics.ListAPIView): - """News list view.""" - - filter_class = filters.NewsListFilterSet - - -class NewsDetailView(NewsMixin, JWTGenericViewMixin, generics.RetrieveAPIView): - """News detail view.""" - - -class NewsTypeListView(generics.ListAPIView): - """NewsType list view.""" - - serializer_class = serializers.NewsTypeSerializer - permission_classes = (permissions.AllowAny, ) - pagination_class = None - queryset = models.NewsType.objects.all() diff --git a/apps/news/views/web.py b/apps/news/views/web.py deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/search_indexes/urls.py b/apps/search_indexes/urls.py index 60b05fb5..664e0b99 100644 --- a/apps/search_indexes/urls.py +++ b/apps/search_indexes/urls.py @@ -4,7 +4,7 @@ from search_indexes import views router = routers.SimpleRouter() -router.register(r'news', views.NewsDocumentViewSet, basename='news') +# router.register(r'news', views.NewsDocumentViewSet, basename='news') # temporarily disabled router.register(r'establishments', views.EstablishmentDocumentViewSet, basename='establishment') urlpatterns = router.urls diff --git a/apps/utils/serializers.py b/apps/utils/serializers.py index 33463ebe..1f4c6f96 100644 --- a/apps/utils/serializers.py +++ b/apps/utils/serializers.py @@ -1,6 +1,5 @@ """Utils app serializer.""" from rest_framework import serializers - from utils.models import PlatformMixin @@ -13,3 +12,12 @@ class SourceSerializerMixin(serializers.Serializer): source = serializers.ChoiceField(choices=PlatformMixin.SOURCES, default=PlatformMixin.WEB, write_only=True) + + +class TranslatedField(serializers.CharField): + """Translated field.""" + + def __init__(self, allow_null=True, required=False, read_only=True, + **kwargs): + super().__init__(allow_null=allow_null, required=required, + read_only=read_only, **kwargs) diff --git a/project/settings/development.py b/project/settings/development.py index a7c8d9fd..43a60935 100644 --- a/project/settings/development.py +++ b/project/settings/development.py @@ -24,7 +24,7 @@ ELASTICSEARCH_DSL = { ELASTICSEARCH_INDEX_NAMES = { - 'search_indexes.documents.news': 'development_news', + # 'search_indexes.documents.news': 'development_news', # temporarily disabled 'search_indexes.documents.establishment': 'development_establishment', } diff --git a/project/settings/local.py b/project/settings/local.py index febe4dd4..503ad191 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -64,6 +64,6 @@ ELASTICSEARCH_DSL = { ELASTICSEARCH_INDEX_NAMES = { - 'search_indexes.documents.news': 'local_news', + # 'search_indexes.documents.news': 'local_news', 'search_indexes.documents.establishment': 'local_establishment', } \ No newline at end of file diff --git a/project/settings/stage.py b/project/settings/stage.py index 59fc78ed..c0d6fdb1 100644 --- a/project/settings/stage.py +++ b/project/settings/stage.py @@ -22,6 +22,6 @@ ELASTICSEARCH_DSL = { ELASTICSEARCH_INDEX_NAMES = { - 'search_indexes.documents.news': 'stage_news', + # 'search_indexes.documents.news': 'stage_news', #temporarily disabled 'search_indexes.documents.establishment': 'stage_establishment', } diff --git a/project/urls/back.py b/project/urls/back.py index 5a64b955..7b4146eb 100644 --- a/project/urls/back.py +++ b/project/urls/back.py @@ -7,4 +7,5 @@ urlpatterns = [ namespace='gallery')), path('establishments/', include('establishment.urls.back')), path('location/', include('location.urls.back')), + path('news/', include('news.urls.back')) ] \ No newline at end of file