From 501b8833edcc612003be3269d1715c0ea8527b40 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Mon, 30 Sep 2019 15:40:28 +0300 Subject: [PATCH] gm-148: finish --- .../migrations/0002_auto_20190930_0714.py | 41 +++++++++++++++++ apps/gallery/serializers.py | 2 + apps/gallery/urls.py | 2 +- apps/gallery/views.py | 13 +++++- apps/news/admin.py | 6 +++ .../migrations/0014_auto_20190926_1156.py | 21 +++++++++ apps/news/migrations/0015_newsgallery.py | 27 +++++++++++ apps/news/migrations/0016_news_gallery.py | 19 ++++++++ apps/news/models.py | 12 ++--- apps/news/serializers.py | 45 +++++++++++++++++++ apps/news/urls/back.py | 7 ++- apps/news/views.py | 43 ++++++++++++++++++ 12 files changed, 229 insertions(+), 9 deletions(-) create mode 100644 apps/gallery/migrations/0002_auto_20190930_0714.py create mode 100644 apps/news/migrations/0014_auto_20190926_1156.py create mode 100644 apps/news/migrations/0015_newsgallery.py create mode 100644 apps/news/migrations/0016_news_gallery.py diff --git a/apps/gallery/migrations/0002_auto_20190930_0714.py b/apps/gallery/migrations/0002_auto_20190930_0714.py new file mode 100644 index 00000000..8423910f --- /dev/null +++ b/apps/gallery/migrations/0002_auto_20190930_0714.py @@ -0,0 +1,41 @@ +# Generated by Django 2.2.4 on 2019-09-30 07:14 + +from django.db import migrations, models +import django.db.models.deletion +import easy_thumbnails.fields +import utils.methods + + +class Migration(migrations.Migration): + + dependencies = [ + ('gallery', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='image', + name='orientation', + field=models.PositiveSmallIntegerField(blank=True, choices=[(0, 'Horizontal'), (1, 'Vertical')], default=None, null=True, verbose_name='image orientation'), + ), + migrations.AddField( + model_name='image', + name='parent', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='parent_image', to='gallery.Image', verbose_name='parent image'), + ), + migrations.AddField( + model_name='image', + name='source', + field=models.PositiveSmallIntegerField(choices=[(0, 'Mobile'), (1, 'Web'), (2, 'All')], default=0, verbose_name='Source'), + ), + migrations.AddField( + model_name='image', + name='title', + field=models.CharField(default='', max_length=255, verbose_name='title'), + ), + migrations.AlterField( + model_name='image', + name='image', + field=easy_thumbnails.fields.ThumbnailerImageField(upload_to=utils.methods.image_path, verbose_name='image file'), + ), + ] diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index f78e4b63..c428b8f2 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -9,6 +9,8 @@ class ImageSerializer(serializers.ModelSerializer): file = serializers.ImageField(source='image', write_only=True) title = serializers.CharField() + orientation = serializers.ChoiceField(choices=models.Image.ORIENTATIONS, + write_only=True) # RESPONSE url = serializers.ImageField(source='image', diff --git a/apps/gallery/urls.py b/apps/gallery/urls.py index 53d1c097..2faa95e4 100644 --- a/apps/gallery/urls.py +++ b/apps/gallery/urls.py @@ -6,5 +6,5 @@ from . import views app_name = 'gallery' urlpatterns = [ - path('upload/', views.ImageUploadView.as_view(), name='upload-image') + path('upload/', views.ImageBaseView.as_view(), name='upload-image'), ] diff --git a/apps/gallery/views.py b/apps/gallery/views.py index 8a9195c3..b35df131 100644 --- a/apps/gallery/views.py +++ b/apps/gallery/views.py @@ -3,8 +3,17 @@ from rest_framework import generics from . import models, serializers -class ImageUploadView(generics.CreateAPIView): - """Upload image to gallery""" +class ImageBaseView(generics.CreateAPIView): + """Upload image to gallery.""" model = models.Image queryset = models.Image.objects.all() serializer_class = serializers.ImageSerializer + + +class NewsImageListView(ImageBaseView, generics.ListAPIView): + """Return list of uploaded images for news object.""" + + def get_queryset(self): + """Override get_queryset method.""" + qs = super(NewsImageListView, self).get_queryset() + return qs.filter(news_gallery__news=self.kwargs.get('news_id')) diff --git a/apps/news/admin.py b/apps/news/admin.py index 7cbfb049..0bb4c8cc 100644 --- a/apps/news/admin.py +++ b/apps/news/admin.py @@ -1,4 +1,5 @@ from django.contrib import admin + from news import models @@ -12,3 +13,8 @@ class NewsTypeAdmin(admin.ModelAdmin): @admin.register(models.News) class NewsAdmin(admin.ModelAdmin): """News admin.""" + + +@admin.register(models.NewsGallery) +class NewsGalleryAdmin(admin.ModelAdmin): + """News gallery admin.""" diff --git a/apps/news/migrations/0014_auto_20190926_1156.py b/apps/news/migrations/0014_auto_20190926_1156.py new file mode 100644 index 00000000..d16f0ad3 --- /dev/null +++ b/apps/news/migrations/0014_auto_20190926_1156.py @@ -0,0 +1,21 @@ +# Generated by Django 2.2.4 on 2019-09-26 11:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('news', '0013_auto_20190924_0806'), + ] + + operations = [ + migrations.RemoveField( + model_name='news', + name='image_url', + ), + migrations.RemoveField( + model_name='news', + name='preview_image_url', + ), + ] diff --git a/apps/news/migrations/0015_newsgallery.py b/apps/news/migrations/0015_newsgallery.py new file mode 100644 index 00000000..a8422dab --- /dev/null +++ b/apps/news/migrations/0015_newsgallery.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.4 on 2019-09-30 08:57 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('gallery', '0002_auto_20190930_0714'), + ('news', '0014_auto_20190926_1156'), + ] + + operations = [ + migrations.CreateModel( + name='NewsGallery', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='news_gallery', to='gallery.Image', verbose_name='gallery')), + ('news', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='news_gallery', to='news.News', verbose_name='news')), + ], + options={ + 'verbose_name': 'news gallery', + 'verbose_name_plural': 'news galleries', + }, + ), + ] diff --git a/apps/news/migrations/0016_news_gallery.py b/apps/news/migrations/0016_news_gallery.py new file mode 100644 index 00000000..7917cf26 --- /dev/null +++ b/apps/news/migrations/0016_news_gallery.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.4 on 2019-09-30 12:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gallery', '0002_auto_20190930_0714'), + ('news', '0015_newsgallery'), + ] + + operations = [ + migrations.AddField( + model_name='news', + name='gallery', + field=models.ManyToManyField(through='news.NewsGallery', to='gallery.Image'), + ), + ] diff --git a/apps/news/models.py b/apps/news/models.py index a442b621..4b9176cd 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -83,6 +83,8 @@ 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: @@ -103,7 +105,7 @@ class NewsGalleryQuerySet(models.QuerySet): """QuerySet for model News""" def originals(self): - """Return QuerySet with originals images.""" + """Return QuerySet with original images.""" return self.filter(gallery__parent__isnull=True) def crops(self): @@ -117,10 +119,10 @@ class NewsGallery(models.Model): related_name='news_gallery', on_delete=models.SET_NULL, verbose_name=_('news')) - gallery = models.ForeignKey('gallery.Image', null=True, - related_name='news_gallery', - on_delete=models.SET_NULL, - verbose_name=_('gallery')) + image = models.ForeignKey('gallery.Image', null=True, + related_name='news_gallery', + on_delete=models.SET_NULL, + verbose_name=_('gallery')) objects = NewsGalleryQuerySet.as_manager() diff --git a/apps/news/serializers.py b/apps/news/serializers.py index b7b11a06..8901b834 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -1,6 +1,9 @@ """News app common serializers.""" +from django.utils.translation import gettext_lazy as _ from rest_framework import serializers +from gallery.models import Image +from gallery.serializers import ImageSerializer from location import models as location_models from location.serializers import CountrySimpleSerializer from main.serializers import MetaDataContentSerializer @@ -28,6 +31,7 @@ class NewsBaseSerializer(serializers.ModelSerializer): # related fields news_type = NewsTypeSerializer(read_only=True) 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) @@ -43,6 +47,7 @@ class NewsBaseSerializer(serializers.ModelSerializer): 'news_type', 'tags', 'slug', + 'gallery', ) @@ -100,3 +105,43 @@ class NewsBackOfficeDetailSerializer(NewsBackOfficeBaseSerializer, 'country_id', ) + +class NewsBackOfficeGallerySerializer(serializers.ModelSerializer): + """Serializer class for model NewsGallery.""" + image = ImageSerializer(read_only=True) + + class Meta: + """Meta class""" + model = models.NewsGallery + fields = [ + 'id', + 'image', + ] + + 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/urls/back.py b/apps/news/urls/back.py index 8522592e..4ab11727 100644 --- a/apps/news/urls/back.py +++ b/apps/news/urls/back.py @@ -1,5 +1,6 @@ """News app urlpatterns for backoffice""" from django.urls import path + from news import views app_name = 'news' @@ -7,5 +8,9 @@ app_name = 'news' urlpatterns = [ path('', views.NewsBackOfficeLCView.as_view(), name='list-create'), path('/', views.NewsBackOfficeRUDView.as_view(), - name='retrieve-update-destroy'), + name='gallery-retrieve-update-destroy'), + path('/gallery/', views.NewsBackOfficeGalleryListView.as_view(), + name='gallery-list'), + path('/gallery//', views.NewsBackOfficeGalleryCreateDestroyView.as_view(), + name='gallery-create-destroy'), ] \ No newline at end of file diff --git a/apps/news/views.py b/apps/news/views.py index 74abe33f..6231fd3b 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -1,5 +1,8 @@ """News app views.""" +from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions + +from gallery.serializers import ImageSerializer from news import filters, models, serializers @@ -59,6 +62,46 @@ class NewsBackOfficeLCView(NewsBackOfficeMixinView, return super().get_serializer_class() +class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, + generics.CreateAPIView, + generics.DestroyAPIView): + """Resource for a create gallery for news for back-office users.""" + serializer_class = serializers.NewsBackOfficeGallerySerializer + + def get_object(self): + """ + Returns the object the view is displaying. + """ + news_qs = self.filter_queryset(self.get_queryset()) + + news = get_object_or_404(news_qs, pk=self.kwargs['pk']) + gallery = get_object_or_404(news.news_gallery, image_id=self.kwargs['image_id']) + + # May raise a permission denied + self.check_object_permissions(self.request, gallery) + + return gallery + + +class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIView): + """Resource for returning gallery for news for back-office users.""" + serializer_class = ImageSerializer + + def get_object(self): + """Override get_object method.""" + qs = super(NewsBackOfficeGalleryListView, self).get_queryset() + news = get_object_or_404(qs, pk=self.kwargs['pk']) + + # May raise a permission denied + self.check_object_permissions(self.request, news) + + return news + + def get_queryset(self): + """Override get_queryset method.""" + return self.get_object().gallery.all() + + class NewsBackOfficeRUDView(NewsBackOfficeMixinView, generics.RetrieveUpdateDestroyAPIView): """Resource for detailed information about news for back-office users."""