Merge branch 'develop' into feature/navigation-bar

# Conflicts:
#	apps/main/serializers.py
This commit is contained in:
Anatoly 2020-01-14 19:06:59 +03:00
commit 6541070d1c
17 changed files with 249 additions and 64 deletions

View File

@ -0,0 +1,29 @@
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db import transaction
from sorl.thumbnail import get_thumbnail
from collection.models import Collection
from utils.methods import image_url_valid, get_image_meta_by_url
class Command(BaseCommand):
SORL_THUMBNAIL_ALIAS = 'collection_image'
def handle(self, *args, **options):
with transaction.atomic():
for collection in Collection.objects.all():
if not image_url_valid(collection.image_url):
continue
_, width, height = get_image_meta_by_url(collection.image_url)
sorl_settings = settings.SORL_THUMBNAIL_ALIASES[self.SORL_THUMBNAIL_ALIAS]
sorl_width_height = sorl_settings['geometry_string'].split('x')
if int(sorl_width_height[0]) > width or int(sorl_width_height[1]) > height:
collection.image_url = get_thumbnail(
file_=collection.image_url,
**settings.SORL_THUMBNAIL_ALIASES[self.SORL_THUMBNAIL_ALIAS]
).url
collection.save()

View File

@ -0,0 +1,28 @@
from django.conf import settings
from django.core.management.base import BaseCommand
from django.db import transaction
from sorl.thumbnail import get_thumbnail
from establishment.models import Establishment
from utils.methods import image_url_valid, get_image_meta_by_url
class Command(BaseCommand):
SORL_THUMBNAIL_ALIAS = 'establishment_collection_image'
def handle(self, *args, **options):
with transaction.atomic():
for establishment in Establishment.objects.all():
if not image_url_valid(establishment.preview_image_url):
continue
_, width, height = get_image_meta_by_url(establishment.preview_image_url)
sorl_settings = settings.SORL_THUMBNAIL_ALIASES[self.SORL_THUMBNAIL_ALIAS]
sorl_width_height = sorl_settings['geometry_string'].split('x')
if int(sorl_width_height[0]) > width or int(sorl_width_height[1]) > height:
establishment.preview_image_url = get_thumbnail(
file_=establishment.preview_image_url,
**sorl_settings
)
establishment.save()

View File

@ -58,8 +58,6 @@ class EstablishmentType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBas
blank=True, null=True, default=None,
verbose_name='default image')
chosen_tags = generic.GenericRelation(to='tag.ChosenTag')
class Meta:
"""Meta class."""

View File

@ -1,16 +1,27 @@
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from account.serializers.common import UserShortSerializer
from establishment import models
from establishment import serializers as model_serializers
from establishment.models import ContactPhone
from gallery.models import Image
from location.models import Address
from location.serializers import AddressDetailSerializer, TranslatedField
from main.models import Currency
from location.models import Address
from main.serializers import AwardSerializer
from utils.decorators import with_base_attributes
from utils.serializers import TimeZoneChoiceField
from gallery.models import Image
from django.utils.translation import gettext_lazy as _
from account.serializers.common import UserShortSerializer
def phones_handler(phones_list, establishment):
"""
create or update phones for establishment 35016 string
"""
ContactPhone.objects.filter(establishment=establishment).delete()
for new_phone in phones_list:
ContactPhone.objects.create(establishment=establishment, phone=new_phone)
class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSerializer):
@ -37,6 +48,11 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
address_id = serializers.PrimaryKeyRelatedField(write_only=True, source='address',
queryset=Address.objects.all())
tz = TimeZoneChoiceField()
phones_list = serializers.ListField(
child=serializers.CharField(max_length=20),
allow_empty=True,
write_only=True,
)
class Meta:
model = models.Establishment
@ -62,8 +78,15 @@ class EstablishmentListCreateSerializer(model_serializers.EstablishmentBaseSeria
'tags',
'tz',
'address_id',
'phones_list',
]
def create(self, validated_data):
phones_list = validated_data.pop('phones_list')
instance = super().create(validated_data)
phones_handler(phones_list, instance)
return instance
class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
"""Establishment create serializer"""
@ -80,6 +103,11 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
socials = model_serializers.SocialNetworkRelatedSerializers(read_only=False,
many=True, )
type = model_serializers.EstablishmentTypeBaseSerializer(source='establishment_type')
phones_list = serializers.ListField(
child=serializers.CharField(max_length=20),
allow_empty=True,
write_only=True,
)
class Meta:
model = models.Establishment
@ -99,8 +127,15 @@ class EstablishmentRUDSerializer(model_serializers.EstablishmentBaseSerializer):
'is_publish',
'address',
'tags',
'phones_list',
]
def update(self, instance, validated_data):
phones_list = validated_data.pop('phones_list')
instance = super().update(instance, validated_data)
phones_handler(phones_list, instance)
return instance
class SocialChoiceSerializers(serializers.ModelSerializer):
"""SocialChoice serializers."""
@ -166,7 +201,6 @@ class ContactEmailBackSerializers(model_serializers.PlateSerializer):
]
class PositionBackSerializer(serializers.ModelSerializer):
"""Position Back serializer."""
@ -181,6 +215,7 @@ class PositionBackSerializer(serializers.ModelSerializer):
'index_name',
]
# TODO: test decorator
@with_base_attributes
class EmployeeBackSerializers(serializers.ModelSerializer):
@ -190,24 +225,22 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
establishment = serializers.SerializerMethodField()
awards = AwardSerializer(many=True, read_only=True)
def get_public_mark(self, obj):
"""Get last list actual public_mark"""
qs = obj.establishmentemployee_set.actual().order_by('-from_date')\
qs = obj.establishmentemployee_set.actual().order_by('-from_date') \
.values('establishment__public_mark').first()
return qs['establishment__public_mark'] if qs else None
def get_positions(self, obj):
"""Get last list actual positions"""
est_id = obj.establishmentemployee_set.actual().\
est_id = obj.establishmentemployee_set.actual(). \
order_by('-from_date').first()
if not est_id:
return None
qs = obj.establishmentemployee_set.actual()\
.filter(establishment_id=est_id.establishment_id)\
qs = obj.establishmentemployee_set.actual() \
.filter(establishment_id=est_id.establishment_id) \
.prefetch_related('position').values('position')
positions = models.Position.objects.filter(id__in=[q['position'] for q in qs])
@ -216,7 +249,7 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
def get_establishment(self, obj):
"""Get last actual establishment"""
est = obj.establishmentemployee_set.actual().order_by('-from_date')\
est = obj.establishmentemployee_set.actual().order_by('-from_date') \
.first()
if not est:
@ -380,6 +413,7 @@ class EstablishmentNoteListCreateSerializer(EstablishmentNoteBaseSerializer):
class EstablishmentAdminListSerializer(UserShortSerializer):
"""Establishment admin serializer."""
class Meta:
model = UserShortSerializer.Meta.model
fields = [

View File

@ -8,6 +8,8 @@ from comment.serializers import common as comment_serializers
from establishment import models
from location.serializers import AddressBaseSerializer, CitySerializer, AddressDetailSerializer, \
CityShortSerializer
from location.serializers import EstablishmentWineRegionBaseSerializer, \
EstablishmentWineOriginBaseSerializer
from main.serializers import AwardSerializer, CurrencySerializer
from review.serializers import ReviewShortSerializer
from tag.serializers import TagBaseSerializer
@ -16,8 +18,6 @@ from utils import exceptions as utils_exceptions
from utils.serializers import ImageBaseSerializer, CarouselCreateSerializer
from utils.serializers import (ProjectModelSerializer, TranslatedField,
FavoritesCreateSerializer)
from location.serializers import EstablishmentWineRegionBaseSerializer, \
EstablishmentWineOriginBaseSerializer
class ContactPhonesSerializer(serializers.ModelSerializer):

View File

@ -17,6 +17,7 @@ from configuration.models import TranslationSettings
from location.models import Country
from main import methods
from review.models import Review
from tag.models import Tag
from utils.exceptions import UnprocessableEntityError
from utils.methods import dictfetchall
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
@ -118,6 +119,8 @@ class Feature(ProjectBaseMixin, PlatformMixin):
site_settings = models.ManyToManyField(SiteSettings, through='SiteFeature')
old_id = models.IntegerField(null=True, blank=True)
chosen_tags = generic.GenericRelation(to='tag.ChosenTag')
class Meta:
"""Meta class."""
verbose_name = _('Feature')
@ -126,6 +129,10 @@ class Feature(ProjectBaseMixin, PlatformMixin):
def __str__(self):
return f'{self.slug}'
@property
def get_chosen_tags(self):
return Tag.objects.filter(chosen_tags__in=self.chosen_tags.all()).distinct()
class SiteFeatureQuerySet(models.QuerySet):
"""Extended queryset for SiteFeature model."""

View File

@ -6,6 +6,7 @@ from account.models import User
from account.serializers.back import BackUserSerializer
from location.serializers import CountrySerializer
from main import models
from tag.serializers import TagBackOfficeSerializer
from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer
@ -90,6 +91,8 @@ class SiteFeatureSerializer(serializers.ModelSerializer):
route = serializers.CharField(source='feature.route.name', allow_null=True)
source = serializers.IntegerField(source='feature.source', allow_null=True)
nested = RecursiveFieldSerializer(many=True, read_only=True, allow_null=True)
chosen_tags = TagBackOfficeSerializer(
source='feature.get_chosen_tags', many=True, read_only=True)
class Meta:
"""Meta class."""
@ -102,6 +105,7 @@ class SiteFeatureSerializer(serializers.ModelSerializer):
'route',
'source',
'nested',
'chosen_tags',
)

View File

@ -0,0 +1,68 @@
# coding=utf-8
from django.core.management.base import BaseCommand
from utils.methods import get_url_images_in_text, get_image_meta_by_url
from news.models import News
from sorl.thumbnail import get_thumbnail
class Command(BaseCommand):
IMAGE_MAX_SIZE_IN_BYTES = 1048576 # ~ 1mb
IMAGE_QUALITY_PERCENTS = 50
def add_arguments(self, parser):
parser.add_argument(
'-s',
'--size',
default=self.IMAGE_MAX_SIZE_IN_BYTES,
help='Максимальный размер файла в байтах',
type=int
)
parser.add_argument(
'-q',
'--quality',
default=self.IMAGE_QUALITY_PERCENTS,
help='Качество изображения',
type=int
)
def optimize(self, text, max_size, max_quality):
"""optimize news images"""
for image in get_url_images_in_text(text):
try:
size, width, height = get_image_meta_by_url(image)
except IOError as ie:
self.stdout.write(self.style.NOTICE(f'{ie}\n'))
continue
if size < max_size:
self.stdout.write(self.style.SUCCESS(f'No need to compress images size is {size / (2**20)}Mb\n'))
continue
percents = round(max_size / (size * 0.01))
width = round(width * percents / 100)
height = round(height * percents / 100)
optimized_image = get_thumbnail(
file_=image,
geometry_string=f'{width}x{height}',
upscale=False,
quality=max_quality
).url
text = text.replace(image, optimized_image)
self.stdout.write(self.style.SUCCESS(f'Optimized {image} -> {optimized_image}\n'
f'Quality [{percents}%]\n'))
return text
def handle(self, *args, **options):
size = options['size']
quality = options['quality']
for news in News.objects.all():
if not isinstance(news.description, dict):
continue
news.description = {
locale: self.optimize(text, size, quality)
for locale, text in news.description.items()
}
news.save()

View File

@ -54,7 +54,6 @@ class NewsType(models.Model):
name = models.CharField(_('name'), max_length=250)
tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='news_types')
chosen_tags = generic.GenericRelation(to='tag.ChosenTag')
class Meta:
"""Meta class."""

View File

@ -38,6 +38,13 @@ class ProductType(TypeDefaultImageMixin, TranslatedFieldsMixin, ProjectBaseMixin
(SOUVENIR, 'souvenir'),
(BOOK, 'book')
)
INDEX_PLURAL_ONE = {
'food': 'food',
'wines': 'wine',
'liquors': 'liquor',
}
name = TJSONField(blank=True, null=True, default=None,
verbose_name=_('Name'), help_text='{"en-GB":"some text"}')
index_name = models.CharField(max_length=50, unique=True, db_index=True,

View File

@ -19,10 +19,13 @@ urlpatterns = [
# similar products by type/subtype
# temporary uses single mechanism, bec. description in process
path('slug/<slug:slug>/similar/wines/', views.SimilarListView.as_view(),
name='similar-wine'),
path('slug/<slug:slug>/similar/liquors/', views.SimilarListView.as_view(),
name='similar-liquor'),
path('slug/<slug:slug>/similar/food/', views.SimilarListView.as_view(),
name='similar-food'),
# path('slug/<slug:slug>/similar/wines/', views.SimilarListView.as_view(),
# name='similar-wine'),
# path('slug/<slug:slug>/similar/liquors/', views.SimilarListView.as_view(),
# name='similar-liquor'),
# path('slug/<slug:slug>/similar/food/', views.SimilarListView.as_view(),
# name='similar-food'),
path('slug/<slug:slug>/similar/<str:type>/', views.SimilarListView.as_view(),
name='similar-products')
]

View File

@ -6,7 +6,7 @@ from rest_framework import generics, permissions
from comment.models import Comment
from comment.serializers import CommentRUDSerializer
from product import filters, serializers
from product.models import Product
from product.models import Product, ProductType
from utils.views import FavoritesCreateDestroyMixinView
from utils.pagination import PortionPagination
from django.conf import settings
@ -44,8 +44,16 @@ class ProductSimilarView(ProductListView):
"""
Return base product instance for a getting list of similar products.
"""
product = get_object_or_404(Product.objects.all(),
slug=self.kwargs.get('slug'))
find_by = {
'slug': self.kwargs.get('slug'),
}
if isinstance(self.kwargs.get('type'), str):
if not self.kwargs.get('type') in ProductType.INDEX_PLURAL_ONE:
return None
find_by['product_type'] = get_object_or_404(ProductType.objects.all(), index_name=ProductType.INDEX_PLURAL_ONE[self.kwargs.get('type')])
product = get_object_or_404(Product.objects.all(), **find_by)
return product

View File

@ -9,6 +9,7 @@ from tag import models
from utils.exceptions import BindingObjectNotFound, ObjectAlreadyAdded, RemovedBindingObjectNotFound
from utils.serializers import TranslatedField
from utils.models import get_default_locale, get_language, to_locale
from main.models import Feature
def translate_obj(obj):
@ -309,48 +310,25 @@ class ChosenTagSerializer(serializers.ModelSerializer):
class ChosenTagBindObjectSerializer(serializers.Serializer):
"""Serializer for binding chosen tag and objects"""
ESTABLISHMENT_TYPE = 'establishment_type'
NEWS_TYPE = 'news_type'
TYPE_CHOICES = (
(ESTABLISHMENT_TYPE, 'Establishment type'),
(NEWS_TYPE, 'News type'),
)
type = serializers.ChoiceField(TYPE_CHOICES)
object_id = serializers.IntegerField()
feature_id = serializers.IntegerField()
def validate(self, attrs):
view = self.context.get('view')
request = self.context.get('request')
obj_type = attrs.get('type')
obj_id = attrs.get('object_id')
obj_id = attrs.get('feature_id')
tag = view.get_object()
attrs['tag'] = tag
if obj_type == self.ESTABLISHMENT_TYPE:
establishment_type = EstablishmentType.objects.filter(pk=obj_id). \
first()
if not establishment_type:
raise BindingObjectNotFound()
if request.method == 'DELETE' and not establishment_type. \
chosen_tags.filter(tag=tag). \
exists():
raise RemovedBindingObjectNotFound()
attrs['related_object'] = establishment_type
elif obj_type == self.NEWS_TYPE:
news_type = NewsType.objects.filter(pk=obj_id).first()
if not news_type:
raise BindingObjectNotFound()
if request.method == 'POST' and news_type.chosen_tags. \
filter(tag=tag).exists():
raise ObjectAlreadyAdded()
if request.method == 'DELETE' and not news_type.chosen_tags. \
filter(tag=tag).exists():
raise RemovedBindingObjectNotFound()
attrs['related_object'] = news_type
feature = Feature.objects.filter(pk=obj_id). \
first()
if not feature:
raise BindingObjectNotFound()
if request.method == 'DELETE' and not feature. \
chosen_tags.filter(tag=tag). \
exists():
raise RemovedBindingObjectNotFound()
attrs['related_object'] = feature
return attrs

View File

@ -4,6 +4,9 @@ import random
import re
import string
from collections import namedtuple
from io import BytesIO
from PIL import Image
import requests
from django.conf import settings
@ -119,6 +122,7 @@ def absolute_url_decorator(func):
return f'{settings.MEDIA_URL}{url_path}/'
else:
return url_path
return get_absolute_image_url
@ -169,3 +173,16 @@ def section_name_into_index_name(section_name: str):
result = re.findall(re_exp, section_name)
if result:
return f"{' '.join([word.capitalize() if i == 0 else word for i, word in enumerate(result[:-2])])}"
def get_url_images_in_text(text):
"""Find images urls in text"""
return re.findall(r'(?:http:|https:)?//.*\.(?:png|jpg|svg)', text)
def get_image_meta_by_url(url) -> (int, int, int):
"""Returns image size (bytes, width, height)"""
image_raw = requests.get(url)
image = Image.open(BytesIO(image_raw.content))
width, height = image.size
return int(image_raw.headers.get('content-length')), width, height

View File

@ -5,14 +5,13 @@ from sorl.thumbnail.engines.pil_engine import Engine as PILEngine
class GMEngine(PILEngine):
def create(self, image, geometry, options):
"""
Processing conductor, returns the thumbnail as an image engine instance
"""
image = self.cropbox(image, geometry, options)
image = self.orientation(image, geometry, options)
image = self.colorspace(image, geometry, options)
image = self.remove_border(image, options)
image = self.scale(image, geometry, options)
image = self.crop(image, geometry, options)
image = self.scale(image, geometry, options)
image = self.rounded(image, geometry, options)
image = self.blur(image, geometry, options)
image = self.padding(image, geometry, options)

View File

@ -27,4 +27,7 @@
./manage.py transfer --overlook
./manage.py transfer --inquiries
./manage.py transfer --product_review
./manage.py transfer --transfer_text_review
./manage.py transfer --transfer_text_review
# оптимизация изображений
/manage.py news_optimize_images # сжимает картинки в описаниях новостей

View File

@ -385,6 +385,7 @@ THUMBNAIL_QUALITY = 85
THUMBNAIL_DEBUG = False
SORL_THUMBNAIL_ALIASES = {
'news_preview': {'geometry_string': '300x260', 'crop': 'center'},
'news_description': {'geometry_string': '100x100'},
'news_promo_horizontal_web': {'geometry_string': '1900x600', 'crop': 'center'},
'news_promo_horizontal_mobile': {'geometry_string': '375x260', 'crop': 'center'},
'news_tile_horizontal_web': {'geometry_string': '300x275', 'crop': 'center'},
@ -411,6 +412,8 @@ SORL_THUMBNAIL_ALIASES = {
'city_detail': {'geometry_string': '1120x1120', 'crop': 'center'},
'city_original': {'geometry_string': '2048x1536', 'crop': 'center'},
'type_preview': {'geometry_string': '300x260', 'crop': 'center'},
'collection_image': {'geometry_string': '940x620', 'upscale': False, 'quality': 100},
'establishment_collection_image': {'geometry_string': '940x620', 'upscale': False, 'quality': 100}
}