From 4890e00b95a460ca958335bfc388b63e98ed41e7 Mon Sep 17 00:00:00 2001 From: Anatoly Date: Fri, 4 Oct 2019 11:50:29 +0300 Subject: [PATCH] gm-148: refactored --- apps/account/tasks.py | 6 +- apps/authorization/tasks.py | 5 +- ...002_1456.py => 0003_auto_20191003_1228.py} | 14 ++- apps/gallery/models.py | 35 ++++---- apps/gallery/serializers.py | 1 - apps/gallery/tasks.py | 12 +-- apps/gallery/urls.py | 3 +- apps/gallery/views.py | 31 ++++--- apps/news/models.py | 10 +-- apps/news/serializers.py | 88 ++++++++++++------- apps/news/views.py | 17 ++-- apps/utils/models.py | 24 ++--- apps/utils/querysets.py | 8 +- project/settings/local.py | 5 +- 14 files changed, 146 insertions(+), 113 deletions(-) rename apps/gallery/migrations/{0003_auto_20191002_1456.py => 0003_auto_20191003_1228.py} (57%) diff --git a/apps/account/tasks.py b/apps/account/tasks.py index 03a231b3..13c6f594 100644 --- a/apps/account/tasks.py +++ b/apps/account/tasks.py @@ -18,7 +18,7 @@ def send_reset_password_email(user_id, country_code): user.send_email(subject=_('Password resetting'), message=user.reset_password_template(country_code)) except: - logger.error(f'METHOD_NAME: {send_reset_password_email.__name__}\n' + logger.error(f'TASK_NAME: {send_reset_password_email.__name__}\n' f'DETAIL: Exception occurred for reset password: ' f'{user_id}') @@ -31,7 +31,7 @@ def confirm_new_email_address(user_id, country_code): user.send_email(subject=_('Validate new email address'), message=user.confirm_email_template(country_code)) except: - logger.error(f'METHOD_NAME: {confirm_new_email_address.__name__}\n' + logger.error(f'TASK_NAME: {confirm_new_email_address.__name__}\n' f'DETAIL: Exception occurred for user: {user_id}') @@ -43,5 +43,5 @@ def change_email_address(user_id, country_code): user.send_email(subject=_('Validate new email address'), message=user.change_email_template(country_code)) except: - logger.error(f'METHOD_NAME: {change_email_address.__name__}\n' + logger.error(f'TASK_NAME: {change_email_address.__name__}\n' f'DETAIL: Exception occurred for user: {user_id}') diff --git a/apps/authorization/tasks.py b/apps/authorization/tasks.py index 9947c2a3..4df94bdc 100644 --- a/apps/authorization/tasks.py +++ b/apps/authorization/tasks.py @@ -1,7 +1,8 @@ """Authorization app celery tasks.""" import logging -from django.utils.translation import gettext_lazy as _ + from celery import shared_task +from django.utils.translation import gettext_lazy as _ from account import models as account_models @@ -17,5 +18,5 @@ def send_confirm_email(user_id, country_code): obj.send_email(subject=_('Email confirmation'), message=obj.confirm_email_template(country_code)) except: - logger.error(f'METHOD_NAME: {send_confirm_email.__name__}\n' + logger.error(f'TASK_NAME: {send_confirm_email.__name__}\n' f'DETAIL: Exception occurred for user: {user_id}') diff --git a/apps/gallery/migrations/0003_auto_20191002_1456.py b/apps/gallery/migrations/0003_auto_20191003_1228.py similarity index 57% rename from apps/gallery/migrations/0003_auto_20191002_1456.py rename to apps/gallery/migrations/0003_auto_20191003_1228.py index 4ef3d1d0..4d054a29 100644 --- a/apps/gallery/migrations/0003_auto_20191002_1456.py +++ b/apps/gallery/migrations/0003_auto_20191003_1228.py @@ -1,7 +1,6 @@ -# Generated by Django 2.2.4 on 2019-10-02 14:56 +# Generated by Django 2.2.4 on 2019-10-03 12:28 -from django.db import migrations, models -import django.db.models.deletion +from django.db import migrations import sorl.thumbnail.fields import utils.methods @@ -13,14 +12,13 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RemoveField( + model_name='image', + name='parent', + ), migrations.AlterField( model_name='image', name='image', field=sorl.thumbnail.fields.ImageField(upload_to=utils.methods.image_path, verbose_name='image file'), ), - migrations.AlterField( - model_name='image', - name='parent', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='children', to='gallery.Image', verbose_name='parent image'), - ), ] diff --git a/apps/gallery/models.py b/apps/gallery/models.py index 3f60e9af..1388f910 100644 --- a/apps/gallery/models.py +++ b/apps/gallery/models.py @@ -1,20 +1,15 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from sorl.thumbnail import delete from sorl.thumbnail.fields import ImageField as SORLImageField from utils.methods import image_path -from django.conf import settings -from . import tasks from utils.models import ProjectBaseMixin, SORLImageMixin, PlatformMixin class ImageQuerySet(models.QuerySet): """QuerySet for model Image.""" - def original_images(self): - """Return QuerySet with original images.""" - return self.filter(parent__isnull=True) - class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin): """Image model.""" @@ -28,11 +23,6 @@ class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin): image = SORLImageField(upload_to=image_path, verbose_name=_('image file')) - parent = models.ForeignKey('self', - blank=True, null=True, default=None, - related_name='children', - on_delete=models.SET_DEFAULT, - verbose_name=_('parent image')) orientation = models.PositiveSmallIntegerField(choices=ORIENTATIONS, blank=True, null=True, default=None, verbose_name=_('image orientation')) @@ -49,9 +39,20 @@ class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin): """String representation""" return str(self.id) - def delete_from_remote_storage(self, delete_original: bool = True): - """Delete from remote storage""" - if settings.USE_CELERY: - tasks.delete_image_from_remote_storage.delay(self.id, delete_original) - else: - tasks.delete_image_from_remote_storage(self.id, delete_original) + def delete_image(self, completely: bool = True): + """ + Deletes an instance and crops of instance from media storage. + :param completely: if set to False then removed only crop neither original image. + """ + try: + # Delete from remote storage + delete(file_=self.image.file, delete_file=completely) + except FileNotFoundError: + pass + finally: + if completely: + # Delete an instance of image + super().delete() + + + diff --git a/apps/gallery/serializers.py b/apps/gallery/serializers.py index 08ca4a0f..e817cbd8 100644 --- a/apps/gallery/serializers.py +++ b/apps/gallery/serializers.py @@ -22,7 +22,6 @@ class ImageSerializer(serializers.ModelSerializer): 'id', 'file', 'url', - 'parent', 'orientation', 'orientation_display', 'title', diff --git a/apps/gallery/tasks.py b/apps/gallery/tasks.py index 8f3f5453..1a64d297 100644 --- a/apps/gallery/tasks.py +++ b/apps/gallery/tasks.py @@ -2,7 +2,6 @@ import logging from celery import shared_task -from sorl.thumbnail import delete from . import models @@ -11,17 +10,14 @@ logger = logging.getLogger(__name__) @shared_task -def delete_image_from_remote_storage(image_id, delete_original=True): +def delete_image(image_id: int, completely: bool = True): """Delete an image from remote storage.""" image_qs = models.Image.objects.filter(id=image_id) if image_qs.exists(): try: image = image_qs.first() - # Delete from remote storage - delete(file_=image.image.file, delete_file=delete_original) - # Delete an instance of image - image.delete() + image.delete_image(completely=completely) except: - logger.error(f'METHOD_NAME: delete_image_from_remote_storage\n' + logger.error(f'TASK_NAME: delete_image\n' f'DETAIL: Exception occurred while deleting an image ' - f'from remote storage: image_id - {image_id}') + f'and related crops from remote storage: image_id - {image_id}') diff --git a/apps/gallery/urls.py b/apps/gallery/urls.py index 2faa95e4..8258092c 100644 --- a/apps/gallery/urls.py +++ b/apps/gallery/urls.py @@ -6,5 +6,6 @@ from . import views app_name = 'gallery' urlpatterns = [ - path('upload/', views.ImageBaseView.as_view(), name='upload-image'), + path('', views.ImageListCreateView.as_view(), name='list-create-image'), + path('/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy-image'), ] diff --git a/apps/gallery/views.py b/apps/gallery/views.py index b35df131..2b155035 100644 --- a/apps/gallery/views.py +++ b/apps/gallery/views.py @@ -1,19 +1,30 @@ -from rest_framework import generics +from django.conf import settings +from django.db.transaction import on_commit +from rest_framework import generics, status +from rest_framework.response import Response -from . import models, serializers +from . import tasks, models, serializers -class ImageBaseView(generics.CreateAPIView): - """Upload image to gallery.""" +class ImageBaseView(generics.GenericAPIView): + """Base Image view.""" 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.""" +class ImageListCreateView(ImageBaseView, generics.ListCreateAPIView): + """List/Create Image view.""" - 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')) + +class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView): + """Destroy view for model Image""" + + def delete(self, request, *args, **kwargs): + """Override destroy view""" + instance = self.get_object() + if settings.USE_CELERY: + on_commit(lambda: tasks.delete_image.delay(image_id=instance.id)) + else: + on_commit(lambda: tasks.delete_image(image_id=instance.id)) + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apps/news/models.py b/apps/news/models.py index 9022a6a2..2e79994a 100644 --- a/apps/news/models.py +++ b/apps/news/models.py @@ -1,11 +1,11 @@ """News app models.""" -from django.db import models from django.contrib.contenttypes import fields as generic +from django.db import models 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 -from random import sample as random_sample class NewsType(models.Model): @@ -149,10 +149,6 @@ class News(BaseAttributes, TranslatedFieldsMixin): def web_url(self): return reverse('web:news:rud', kwargs={'slug': self.slug}) - @property - def original_images(self): - return self.gallery.original_images() - class NewsGalleryQuerySet(models.QuerySet): """QuerySet for model News""" @@ -169,8 +165,6 @@ class NewsGallery(models.Model): on_delete=models.SET_NULL, verbose_name=_('gallery')) - objects = NewsGalleryQuerySet.as_manager() - class Meta: """NewsGallery meta class.""" verbose_name = _('news gallery') diff --git a/apps/news/serializers.py b/apps/news/serializers.py index 08dfce5e..0ea5606f 100644 --- a/apps/news/serializers.py +++ b/apps/news/serializers.py @@ -4,7 +4,6 @@ from rest_framework import serializers from account.serializers.common import UserBaseSerializer 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 @@ -12,12 +11,61 @@ from news import models from utils.serializers import TranslatedField, ProjectModelSerializer -class NewsCropImageSerializer(ImageSerializer): +class CropImageSerializer(serializers.Serializer): + """Serializer for crop images for News object.""" + preview_url = serializers.SerializerMethodField() + promo_horizontal_web_url = serializers.SerializerMethodField() + promo_horizontal_mobile_url = serializers.SerializerMethodField() + tile_horizontal_web_url = serializers.SerializerMethodField() + tile_horizontal_mobile_url = serializers.SerializerMethodField() + tile_vertical_web_url = serializers.SerializerMethodField() + highlight_vertical_web_url = serializers.SerializerMethodField() + editor_web_url = serializers.SerializerMethodField() + editor_mobile_url = serializers.SerializerMethodField() + + def get_preview_url(self, obj): + """Get crop preview.""" + return obj.instance.get_image_url('news_preview') + + def get_promo_horizontal_web_url(self, obj): + """Get crop promo_horizontal_web.""" + return obj.instance.get_image_url('news_promo_horizontal_web') + + def get_promo_horizontal_mobile_url(self, obj): + """Get crop promo_horizontal_mobile.""" + return obj.instance.get_image_url('news_promo_horizontal_mobile') + + def get_tile_horizontal_web_url(self, obj): + """Get crop tile_horizontal_web.""" + return obj.instance.get_image_url('news_tile_horizontal_web') + + def get_tile_horizontal_mobile_url(self, obj): + """Get crop tile_horizontal_mobile.""" + return obj.instance.get_image_url('news_tile_horizontal_mobile') + + def get_tile_vertical_web_url(self, obj): + """Get crop tile_vertical_web.""" + return obj.instance.get_image_url('news_tile_vertical_web') + + def get_highlight_vertical_web_url(self, obj): + """Get crop highlight_vertical_web.""" + return obj.instance.get_image_url('news_highlight_vertical_web') + + def get_editor_web_url(self, obj): + """Get crop editor_web.""" + return obj.instance.get_image_url('news_editor_web') + + def get_editor_mobile_url(self, obj): + """Get crop editor_mobile.""" + return obj.instance.get_image_url('news_editor_mobile') + + +class NewsImageSerializer(serializers.ModelSerializer): """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() + original_url = serializers.URLField(source='image.url') + auto_crop_images = CropImageSerializer(source='image', allow_null=True) class Meta: model = Image @@ -25,34 +73,8 @@ class NewsCropImageSerializer(ImageSerializer): '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='children', allow_null=True, many=True) - - class Meta: - model = Image - fields = [ - 'id', - 'title', - 'url', - 'crops', + 'original_url', + 'auto_crop_images', ] extra_kwargs = { 'orientation': {'write_only': True} @@ -79,7 +101,7 @@ 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) + gallery = NewsImageSerializer(read_only=True, many=True) class Meta: """Meta class.""" diff --git a/apps/news/views.py b/apps/news/views.py index f9d94742..9fee70e5 100644 --- a/apps/news/views.py +++ b/apps/news/views.py @@ -1,10 +1,12 @@ """News app views.""" +from django.conf import settings +from django.db.transaction import on_commit from django.shortcuts import get_object_or_404 from rest_framework import generics, permissions, status from rest_framework.response import Response +from gallery.tasks import delete_image from news import filters, models, serializers -from gallery.serializers import ImageSerializer class NewsMixinView: @@ -102,13 +104,14 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView, def destroy(self, request, *args, **kwargs): """Override destroy method.""" gallery_obj = self.get_object() - image_obj = gallery_obj.image - + if settings.USE_CELERY: + on_commit(lambda: delete_image.delay(image_id=gallery_obj.image.id, + completely=False)) + else: + on_commit(lambda: delete_image(image_id=gallery_obj.image.id, + completely=False)) # Delete an instances of NewsGallery model gallery_obj.delete() - # Delete an instance of Image model - image_obj.delete_from_remote_storage() - return Response(status=status.HTTP_204_NO_CONTENT) @@ -128,7 +131,7 @@ class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIVie def get_queryset(self): """Override get_queryset method.""" - return self.get_object().gallery.original_images() + return self.get_object().gallery.all() class NewsBackOfficeRUDView(NewsBackOfficeMixinView, diff --git a/apps/utils/models.py b/apps/utils/models.py index 299013b7..5ff52e22 100644 --- a/apps/utils/models.py +++ b/apps/utils/models.py @@ -1,6 +1,8 @@ """Utils app models.""" import logging from os.path import exists + +from django.conf import settings from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.gis.db import models from django.contrib.postgres.fields import JSONField @@ -9,12 +11,11 @@ from django.utils import timezone from django.utils.html import mark_safe from django.utils.translation import ugettext_lazy as _, get_language from easy_thumbnails.fields import ThumbnailerImageField +from sorl.thumbnail import get_thumbnail +from sorl.thumbnail.fields import ImageField as SORLImageField + from utils.methods import image_path, svg_image_path from utils.validators import svg_image_validator -from sorl.thumbnail.fields import ImageField as SORLImageField -from sorl.thumbnail import get_thumbnail -from django.conf import settings -from utils.methods import image_url_valid logger = logging.getLogger(__name__) @@ -123,7 +124,7 @@ class OAuthProjectMixin: def get_source(self): """Method to get of platform""" - return NotImplemented + return NotImplementedError class BaseAttributes(ProjectBaseMixin): @@ -193,15 +194,18 @@ class SORLImageMixin(models.Model): """Meta class.""" abstract = True - def get_image(self, thumbnail_key=None): + def get_image(self, thumbnail_key: str): """Get thumbnail image file.""" if thumbnail_key in settings.SORL_THUMBNAIL_ALIASES: - return get_thumbnail(file_=self.image, - **settings.SORL_THUMBNAIL_ALIASES[thumbnail_key]) + return get_thumbnail( + file_=self.image, + **settings.SORL_THUMBNAIL_ALIASES[thumbnail_key]) - def get_image_url(self, thumbnail_key=None): + def get_image_url(self, thumbnail_key: str): """Get image thumbnail url.""" - return self.get_image(thumbnail_key).url + crop_image = self.get_image(thumbnail_key) + if hasattr(crop_image, 'url'): + return self.get_image(thumbnail_key).url def image_tag(self): """Admin preview tag.""" diff --git a/apps/utils/querysets.py b/apps/utils/querysets.py index bf2816f2..45798fbb 100644 --- a/apps/utils/querysets.py +++ b/apps/utils/querysets.py @@ -1,8 +1,10 @@ """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 django.db import models +from django.db.models import Q, F + from utils.methods import get_contenttype @@ -50,7 +52,7 @@ class RelatedObjectsCountMixin(models.QuerySet): 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()]) + 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/project/settings/local.py b/project/settings/local.py index d842894c..15fbbb80 100644 --- a/project/settings/local.py +++ b/project/settings/local.py @@ -1,6 +1,7 @@ """Local settings.""" from .base import * import sys +from .amazon_s3 import * ALLOWED_HOSTS = ['*', ] @@ -23,8 +24,8 @@ CELERY_BROKER_URL = BROKER_URL # MEDIA -MEDIA_URL = f'{SCHEMA_URI}://{DOMAIN_URI}/{MEDIA_LOCATION}/' -MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION) +# MEDIA_URL = f'{SCHEMA_URI}://{DOMAIN_URI}/{MEDIA_LOCATION}/' +# MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION) # LOGGING