gm-148: refactored

This commit is contained in:
Anatoly 2019-10-04 11:50:29 +03:00
parent 4796fce784
commit 4890e00b95
14 changed files with 146 additions and 113 deletions

View File

@ -18,7 +18,7 @@ def send_reset_password_email(user_id, country_code):
user.send_email(subject=_('Password resetting'), user.send_email(subject=_('Password resetting'),
message=user.reset_password_template(country_code)) message=user.reset_password_template(country_code))
except: 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'DETAIL: Exception occurred for reset password: '
f'{user_id}') f'{user_id}')
@ -31,7 +31,7 @@ def confirm_new_email_address(user_id, country_code):
user.send_email(subject=_('Validate new email address'), user.send_email(subject=_('Validate new email address'),
message=user.confirm_email_template(country_code)) message=user.confirm_email_template(country_code))
except: 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}') 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'), user.send_email(subject=_('Validate new email address'),
message=user.change_email_template(country_code)) message=user.change_email_template(country_code))
except: 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}') f'DETAIL: Exception occurred for user: {user_id}')

View File

@ -1,7 +1,8 @@
"""Authorization app celery tasks.""" """Authorization app celery tasks."""
import logging import logging
from django.utils.translation import gettext_lazy as _
from celery import shared_task from celery import shared_task
from django.utils.translation import gettext_lazy as _
from account import models as account_models 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'), obj.send_email(subject=_('Email confirmation'),
message=obj.confirm_email_template(country_code)) message=obj.confirm_email_template(country_code))
except: 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}') f'DETAIL: Exception occurred for user: {user_id}')

View File

@ -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 from django.db import migrations
import django.db.models.deletion
import sorl.thumbnail.fields import sorl.thumbnail.fields
import utils.methods import utils.methods
@ -13,14 +12,13 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.RemoveField(
model_name='image',
name='parent',
),
migrations.AlterField( migrations.AlterField(
model_name='image', model_name='image',
name='image', name='image',
field=sorl.thumbnail.fields.ImageField(upload_to=utils.methods.image_path, verbose_name='image file'), 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'),
),
] ]

View File

@ -1,20 +1,15 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from sorl.thumbnail import delete
from sorl.thumbnail.fields import ImageField as SORLImageField from sorl.thumbnail.fields import ImageField as SORLImageField
from utils.methods import image_path from utils.methods import image_path
from django.conf import settings
from . import tasks
from utils.models import ProjectBaseMixin, SORLImageMixin, PlatformMixin from utils.models import ProjectBaseMixin, SORLImageMixin, PlatformMixin
class ImageQuerySet(models.QuerySet): class ImageQuerySet(models.QuerySet):
"""QuerySet for model Image.""" """QuerySet for model Image."""
def original_images(self):
"""Return QuerySet with original images."""
return self.filter(parent__isnull=True)
class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin): class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin):
"""Image model.""" """Image model."""
@ -28,11 +23,6 @@ class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin):
image = SORLImageField(upload_to=image_path, image = SORLImageField(upload_to=image_path,
verbose_name=_('image file')) 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, orientation = models.PositiveSmallIntegerField(choices=ORIENTATIONS,
blank=True, null=True, default=None, blank=True, null=True, default=None,
verbose_name=_('image orientation')) verbose_name=_('image orientation'))
@ -49,9 +39,20 @@ class Image(ProjectBaseMixin, SORLImageMixin, PlatformMixin):
"""String representation""" """String representation"""
return str(self.id) return str(self.id)
def delete_from_remote_storage(self, delete_original: bool = True): def delete_image(self, completely: bool = True):
"""Delete from remote storage""" """
if settings.USE_CELERY: Deletes an instance and crops of instance from media storage.
tasks.delete_image_from_remote_storage.delay(self.id, delete_original) :param completely: if set to False then removed only crop neither original image.
else: """
tasks.delete_image_from_remote_storage(self.id, delete_original) 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()

View File

@ -22,7 +22,6 @@ class ImageSerializer(serializers.ModelSerializer):
'id', 'id',
'file', 'file',
'url', 'url',
'parent',
'orientation', 'orientation',
'orientation_display', 'orientation_display',
'title', 'title',

View File

@ -2,7 +2,6 @@
import logging import logging
from celery import shared_task from celery import shared_task
from sorl.thumbnail import delete
from . import models from . import models
@ -11,17 +10,14 @@ logger = logging.getLogger(__name__)
@shared_task @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.""" """Delete an image from remote storage."""
image_qs = models.Image.objects.filter(id=image_id) image_qs = models.Image.objects.filter(id=image_id)
if image_qs.exists(): if image_qs.exists():
try: try:
image = image_qs.first() image = image_qs.first()
# Delete from remote storage image.delete_image(completely=completely)
delete(file_=image.image.file, delete_file=delete_original)
# Delete an instance of image
image.delete()
except: 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'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}')

View File

@ -6,5 +6,6 @@ from . import views
app_name = 'gallery' app_name = 'gallery'
urlpatterns = [ urlpatterns = [
path('upload/', views.ImageBaseView.as_view(), name='upload-image'), path('', views.ImageListCreateView.as_view(), name='list-create-image'),
path('<int:pk>/', views.ImageRetrieveDestroyView.as_view(), name='retrieve-destroy-image'),
] ]

View File

@ -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): class ImageBaseView(generics.GenericAPIView):
"""Upload image to gallery.""" """Base Image view."""
model = models.Image model = models.Image
queryset = models.Image.objects.all() queryset = models.Image.objects.all()
serializer_class = serializers.ImageSerializer serializer_class = serializers.ImageSerializer
class NewsImageListView(ImageBaseView, generics.ListAPIView): class ImageListCreateView(ImageBaseView, generics.ListCreateAPIView):
"""Return list of uploaded images for news object.""" """List/Create Image view."""
def get_queryset(self):
"""Override get_queryset method.""" class ImageRetrieveDestroyView(ImageBaseView, generics.RetrieveDestroyAPIView):
qs = super(NewsImageListView, self).get_queryset() """Destroy view for model Image"""
return qs.filter(news_gallery__news=self.kwargs.get('news_id'))
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)

View File

@ -1,11 +1,11 @@
"""News app models.""" """News app models."""
from django.db import models
from django.contrib.contenttypes import fields as generic from django.contrib.contenttypes import fields as generic
from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin
from random import sample as random_sample
class NewsType(models.Model): class NewsType(models.Model):
@ -149,10 +149,6 @@ class News(BaseAttributes, TranslatedFieldsMixin):
def web_url(self): def web_url(self):
return reverse('web:news:rud', kwargs={'slug': self.slug}) return reverse('web:news:rud', kwargs={'slug': self.slug})
@property
def original_images(self):
return self.gallery.original_images()
class NewsGalleryQuerySet(models.QuerySet): class NewsGalleryQuerySet(models.QuerySet):
"""QuerySet for model News""" """QuerySet for model News"""
@ -169,8 +165,6 @@ class NewsGallery(models.Model):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
verbose_name=_('gallery')) verbose_name=_('gallery'))
objects = NewsGalleryQuerySet.as_manager()
class Meta: class Meta:
"""NewsGallery meta class.""" """NewsGallery meta class."""
verbose_name = _('news gallery') verbose_name = _('news gallery')

View File

@ -4,7 +4,6 @@ from rest_framework import serializers
from account.serializers.common import UserBaseSerializer from account.serializers.common import UserBaseSerializer
from gallery.models import Image from gallery.models import Image
from gallery.serializers import ImageSerializer
from location import models as location_models from location import models as location_models
from location.serializers import CountrySimpleSerializer from location.serializers import CountrySimpleSerializer
from main.serializers import MetaDataContentSerializer from main.serializers import MetaDataContentSerializer
@ -12,12 +11,61 @@ from news import models
from utils.serializers import TranslatedField, ProjectModelSerializer 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.""" """Serializer for returning crop images of news image."""
orientation_display = serializers.CharField(source='get_orientation_display', orientation_display = serializers.CharField(source='get_orientation_display',
read_only=True) read_only=True)
web_url = serializers.SerializerMethodField() original_url = serializers.URLField(source='image.url')
mobile_url = serializers.SerializerMethodField() auto_crop_images = CropImageSerializer(source='image', allow_null=True)
class Meta: class Meta:
model = Image model = Image
@ -25,34 +73,8 @@ class NewsCropImageSerializer(ImageSerializer):
'id', 'id',
'title', 'title',
'orientation_display', 'orientation_display',
'web_url', 'original_url',
'mobile_url', 'auto_crop_images',
]
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',
] ]
extra_kwargs = { extra_kwargs = {
'orientation': {'write_only': True} 'orientation': {'write_only': True}
@ -79,7 +101,7 @@ class NewsBaseSerializer(ProjectModelSerializer):
# related fields # related fields
news_type = NewsTypeSerializer(read_only=True) news_type = NewsTypeSerializer(read_only=True)
tags = MetaDataContentSerializer(read_only=True, many=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: class Meta:
"""Meta class.""" """Meta class."""

View File

@ -1,10 +1,12 @@
"""News app views.""" """News app views."""
from django.conf import settings
from django.db.transaction import on_commit
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions, status from rest_framework import generics, permissions, status
from rest_framework.response import Response from rest_framework.response import Response
from gallery.tasks import delete_image
from news import filters, models, serializers from news import filters, models, serializers
from gallery.serializers import ImageSerializer
class NewsMixinView: class NewsMixinView:
@ -102,13 +104,14 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
"""Override destroy method.""" """Override destroy method."""
gallery_obj = self.get_object() 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 # Delete an instances of NewsGallery model
gallery_obj.delete() gallery_obj.delete()
# Delete an instance of Image model
image_obj.delete_from_remote_storage()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
@ -128,7 +131,7 @@ class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIVie
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method.""" """Override get_queryset method."""
return self.get_object().gallery.original_images() return self.get_object().gallery.all()
class NewsBackOfficeRUDView(NewsBackOfficeMixinView, class NewsBackOfficeRUDView(NewsBackOfficeMixinView,

View File

@ -1,6 +1,8 @@
"""Utils app models.""" """Utils app models."""
import logging import logging
from os.path import exists from os.path import exists
from django.conf import settings
from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.contrib.gis.db import models from django.contrib.gis.db import models
from django.contrib.postgres.fields import JSONField 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.html import mark_safe
from django.utils.translation import ugettext_lazy as _, get_language from django.utils.translation import ugettext_lazy as _, get_language
from easy_thumbnails.fields import ThumbnailerImageField 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.methods import image_path, svg_image_path
from utils.validators import svg_image_validator 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__) logger = logging.getLogger(__name__)
@ -123,7 +124,7 @@ class OAuthProjectMixin:
def get_source(self): def get_source(self):
"""Method to get of platform""" """Method to get of platform"""
return NotImplemented return NotImplementedError
class BaseAttributes(ProjectBaseMixin): class BaseAttributes(ProjectBaseMixin):
@ -193,15 +194,18 @@ class SORLImageMixin(models.Model):
"""Meta class.""" """Meta class."""
abstract = True abstract = True
def get_image(self, thumbnail_key=None): def get_image(self, thumbnail_key: str):
"""Get thumbnail image file.""" """Get thumbnail image file."""
if thumbnail_key in settings.SORL_THUMBNAIL_ALIASES: if thumbnail_key in settings.SORL_THUMBNAIL_ALIASES:
return get_thumbnail(file_=self.image, return get_thumbnail(
**settings.SORL_THUMBNAIL_ALIASES[thumbnail_key]) 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.""" """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): def image_tag(self):
"""Admin preview tag.""" """Admin preview tag."""

View File

@ -1,8 +1,10 @@
"""Utils QuerySet Mixins""" """Utils QuerySet Mixins"""
from django.db import models
from django.db.models import Q, Sum, F
from functools import reduce from functools import reduce
from operator import add from operator import add
from django.db import models
from django.db.models import Q, F
from utils.methods import get_contenttype from utils.methods import get_contenttype
@ -50,7 +52,7 @@ class RelatedObjectsCountMixin(models.QuerySet):
def filter_all_related_gt(self, count): def filter_all_related_gt(self, count):
"""Queryset filter by all related objects 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()\ return self._annotate_related_objects_count()\
.annotate(all_related_count=exp)\ .annotate(all_related_count=exp)\
.filter(all_related_count__gt=count) .filter(all_related_count__gt=count)

View File

@ -1,6 +1,7 @@
"""Local settings.""" """Local settings."""
from .base import * from .base import *
import sys import sys
from .amazon_s3 import *
ALLOWED_HOSTS = ['*', ] ALLOWED_HOSTS = ['*', ]
@ -23,8 +24,8 @@ CELERY_BROKER_URL = BROKER_URL
# MEDIA # MEDIA
MEDIA_URL = f'{SCHEMA_URI}://{DOMAIN_URI}/{MEDIA_LOCATION}/' # MEDIA_URL = f'{SCHEMA_URI}://{DOMAIN_URI}/{MEDIA_LOCATION}/'
MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION) # MEDIA_ROOT = os.path.join(PUBLIC_ROOT, MEDIA_LOCATION)
# LOGGING # LOGGING