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, blank=True, null=True, default=None,
verbose_name='default image') verbose_name='default image')
chosen_tags = generic.GenericRelation(to='tag.ChosenTag')
class Meta: class Meta:
"""Meta class.""" """Meta class."""

View File

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

View File

@ -8,6 +8,8 @@ from comment.serializers import common as comment_serializers
from establishment import models from establishment import models
from location.serializers import AddressBaseSerializer, CitySerializer, AddressDetailSerializer, \ from location.serializers import AddressBaseSerializer, CitySerializer, AddressDetailSerializer, \
CityShortSerializer CityShortSerializer
from location.serializers import EstablishmentWineRegionBaseSerializer, \
EstablishmentWineOriginBaseSerializer
from main.serializers import AwardSerializer, CurrencySerializer from main.serializers import AwardSerializer, CurrencySerializer
from review.serializers import ReviewShortSerializer from review.serializers import ReviewShortSerializer
from tag.serializers import TagBaseSerializer 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 ImageBaseSerializer, CarouselCreateSerializer
from utils.serializers import (ProjectModelSerializer, TranslatedField, from utils.serializers import (ProjectModelSerializer, TranslatedField,
FavoritesCreateSerializer) FavoritesCreateSerializer)
from location.serializers import EstablishmentWineRegionBaseSerializer, \
EstablishmentWineOriginBaseSerializer
class ContactPhonesSerializer(serializers.ModelSerializer): class ContactPhonesSerializer(serializers.ModelSerializer):

View File

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

View File

@ -6,6 +6,7 @@ from account.models import User
from account.serializers.back import BackUserSerializer from account.serializers.back import BackUserSerializer
from location.serializers import CountrySerializer from location.serializers import CountrySerializer
from main import models from main import models
from tag.serializers import TagBackOfficeSerializer
from utils.serializers import ProjectModelSerializer, TranslatedField, RecursiveFieldSerializer 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) route = serializers.CharField(source='feature.route.name', allow_null=True)
source = serializers.IntegerField(source='feature.source', allow_null=True) source = serializers.IntegerField(source='feature.source', allow_null=True)
nested = RecursiveFieldSerializer(many=True, read_only=True, 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: class Meta:
"""Meta class.""" """Meta class."""
@ -102,6 +105,7 @@ class SiteFeatureSerializer(serializers.ModelSerializer):
'route', 'route',
'source', 'source',
'nested', '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) name = models.CharField(_('name'), max_length=250)
tag_categories = models.ManyToManyField('tag.TagCategory', tag_categories = models.ManyToManyField('tag.TagCategory',
related_name='news_types') related_name='news_types')
chosen_tags = generic.GenericRelation(to='tag.ChosenTag')
class Meta: class Meta:
"""Meta class.""" """Meta class."""

View File

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

View File

@ -19,10 +19,13 @@ urlpatterns = [
# similar products by type/subtype # similar products by type/subtype
# temporary uses single mechanism, bec. description in process # temporary uses single mechanism, bec. description in process
path('slug/<slug:slug>/similar/wines/', views.SimilarListView.as_view(), # path('slug/<slug:slug>/similar/wines/', views.SimilarListView.as_view(),
name='similar-wine'), # name='similar-wine'),
path('slug/<slug:slug>/similar/liquors/', views.SimilarListView.as_view(), # path('slug/<slug:slug>/similar/liquors/', views.SimilarListView.as_view(),
name='similar-liquor'), # name='similar-liquor'),
path('slug/<slug:slug>/similar/food/', views.SimilarListView.as_view(), # path('slug/<slug:slug>/similar/food/', views.SimilarListView.as_view(),
name='similar-food'), # 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.models import Comment
from comment.serializers import CommentRUDSerializer from comment.serializers import CommentRUDSerializer
from product import filters, serializers from product import filters, serializers
from product.models import Product from product.models import Product, ProductType
from utils.views import FavoritesCreateDestroyMixinView from utils.views import FavoritesCreateDestroyMixinView
from utils.pagination import PortionPagination from utils.pagination import PortionPagination
from django.conf import settings from django.conf import settings
@ -44,8 +44,16 @@ class ProductSimilarView(ProductListView):
""" """
Return base product instance for a getting list of similar products. Return base product instance for a getting list of similar products.
""" """
product = get_object_or_404(Product.objects.all(), find_by = {
slug=self.kwargs.get('slug')) '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 return product

View File

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

View File

@ -4,6 +4,9 @@ import random
import re import re
import string import string
from collections import namedtuple from collections import namedtuple
from io import BytesIO
from PIL import Image
import requests import requests
from django.conf import settings from django.conf import settings
@ -119,6 +122,7 @@ def absolute_url_decorator(func):
return f'{settings.MEDIA_URL}{url_path}/' return f'{settings.MEDIA_URL}{url_path}/'
else: else:
return url_path return url_path
return get_absolute_image_url 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) result = re.findall(re_exp, section_name)
if result: if result:
return f"{' '.join([word.capitalize() if i == 0 else word for i, word in enumerate(result[:-2])])}" 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): class GMEngine(PILEngine):
def create(self, image, geometry, options): def create(self, image, geometry, options):
"""
Processing conductor, returns the thumbnail as an image engine instance
"""
image = self.cropbox(image, geometry, options) image = self.cropbox(image, geometry, options)
image = self.orientation(image, geometry, options) image = self.orientation(image, geometry, options)
image = self.colorspace(image, geometry, options) image = self.colorspace(image, geometry, options)
image = self.remove_border(image, options) image = self.remove_border(image, options)
image = self.scale(image, geometry, options)
image = self.crop(image, geometry, options) image = self.crop(image, geometry, options)
image = self.scale(image, geometry, options)
image = self.rounded(image, geometry, options) image = self.rounded(image, geometry, options)
image = self.blur(image, geometry, options) image = self.blur(image, geometry, options)
image = self.padding(image, geometry, options) image = self.padding(image, geometry, options)

View File

@ -27,4 +27,7 @@
./manage.py transfer --overlook ./manage.py transfer --overlook
./manage.py transfer --inquiries ./manage.py transfer --inquiries
./manage.py transfer --product_review ./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 THUMBNAIL_DEBUG = False
SORL_THUMBNAIL_ALIASES = { SORL_THUMBNAIL_ALIASES = {
'news_preview': {'geometry_string': '300x260', 'crop': 'center'}, 'news_preview': {'geometry_string': '300x260', 'crop': 'center'},
'news_description': {'geometry_string': '100x100'},
'news_promo_horizontal_web': {'geometry_string': '1900x600', 'crop': 'center'}, 'news_promo_horizontal_web': {'geometry_string': '1900x600', 'crop': 'center'},
'news_promo_horizontal_mobile': {'geometry_string': '375x260', 'crop': 'center'}, 'news_promo_horizontal_mobile': {'geometry_string': '375x260', 'crop': 'center'},
'news_tile_horizontal_web': {'geometry_string': '300x275', '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_detail': {'geometry_string': '1120x1120', 'crop': 'center'},
'city_original': {'geometry_string': '2048x1536', 'crop': 'center'}, 'city_original': {'geometry_string': '2048x1536', 'crop': 'center'},
'type_preview': {'geometry_string': '300x260', '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}
} }