refactored galleries, added transfer script

This commit is contained in:
Anatoly 2019-11-17 14:22:30 +03:00
parent 6c82af1940
commit a8b3c9aa63
24 changed files with 454 additions and 254 deletions

View File

@ -0,0 +1,17 @@
# Generated by Django 2.2.7 on 2019-11-17 11:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('advertisement', '0007_auto_20191115_0750'),
]
operations = [
migrations.AlterModelOptions(
name='advertisement',
options={'verbose_name': 'Advertisement', 'verbose_name_plural': 'Advertisements'},
),
]

View File

@ -32,6 +32,12 @@ class ContactPhoneInline(admin.TabularInline):
extra = 0
class GalleryImageInline(admin.TabularInline):
"""Gallery image inline admin."""
model = models.EstablishmentGallery
extra = 0
class ContactEmailInline(admin.TabularInline):
"""Contact email inline admin."""
model = models.ContactEmail
@ -59,6 +65,7 @@ class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin):
list_display = ['id', '__str__', 'image_tag', ]
search_fields = ['id', 'name', 'index_name', 'slug']
list_filter = ['public_mark', 'toque_number']
inlines = [GalleryImageInline, ]
# inlines = [
# AwardInline, ContactPhoneInline, ContactEmailInline,

View File

@ -0,0 +1,41 @@
from django.core.management.base import BaseCommand
from establishment.models import Establishment, EstablishmentGallery
from gallery.models import Image
import requests
from rest_framework import status
class Command(BaseCommand):
help = 'Fill establishment gallery from existing images'
def handle(self, *args, **kwargs):
count = 0
not_valid_link_counter = 0
not_valid_urls = []
cdn_prefix = 'https://1dc3f33f6d-3.optimicdn.com/gaultmillau.com/'
establishments = Establishment.objects.exclude(image_url__isnull=True) \
.exclude(preview_image_url__isnull=True)
for establishment in establishments:
image_url = establishment.image_url.rstrip()
relative_image_path = image_url[len(cdn_prefix):]
response = requests.head(image_url, allow_redirects=True)
if response.status_code != status.HTTP_200_OK:
not_valid_link_counter += 1
not_valid_urls.append(image_url)
image, image_created = Image.objects.get_or_create(
orientation=Image.HORIZONTAL,
title=f'{establishment.name} - {relative_image_path}',
image=relative_image_path)
gallery, _ = EstablishmentGallery.objects.get_or_create(establishment=establishment,
image=image,
is_main=True)
if image_created:
count += 1
self.stdout.write(self.style.WARNING(f'Created/updated {count} objects.\n'
f'Not valid link counter: {not_valid_link_counter}\n'
f'List of non valid image url: {not_valid_urls}'))

View File

@ -0,0 +1,38 @@
# Generated by Django 2.2.7 on 2019-11-17 11:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('gallery', '0006_merge_20191027_1758'),
('establishment', '0061_auto_20191114_0550'),
]
operations = [
migrations.AlterModelOptions(
name='establishmentnote',
options={'verbose_name': 'establishment note', 'verbose_name_plural': 'establishment notes'},
),
migrations.CreateModel(
name='EstablishmentGallery',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('is_main', models.BooleanField(default=False, verbose_name='Is the main image')),
('establishment', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='establishment_gallery', to='establishment.Establishment', verbose_name='establishment')),
('image', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='establishment_gallery', to='gallery.Image', verbose_name='image')),
],
options={
'verbose_name': 'establishment gallery',
'verbose_name_plural': 'establishment galleries',
'unique_together': {('establishment', 'is_main'), ('establishment', 'image')},
},
),
migrations.AddField(
model_name='establishment',
name='gallery',
field=models.ManyToManyField(through='establishment.EstablishmentGallery', to='gallery.Image'),
),
]

View File

@ -22,7 +22,8 @@ from location.models import Address
from main.models import Award, Currency
from review.models import Review
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
TranslatedFieldsMixin, BaseAttributes)
TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
IntermediateGalleryModelMixin)
# todo: establishment type&subtypes check
@ -316,9 +317,11 @@ class EstablishmentQuerySet(models.QuerySet):
return self.exclude(address__city__country__in=countries)
class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
"""Establishment model."""
# todo: delete image URL fields after moving on gallery
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
name = models.CharField(_('name'), max_length=255, default='')
transliterated_name = models.CharField(default='', max_length=255,
@ -376,6 +379,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
related_name='establishments',
blank=True, default=None,
verbose_name=_('Collections'))
gallery = models.ManyToManyField('gallery.Image', through='EstablishmentGallery')
preview_image_url = models.URLField(verbose_name=_('Preview image URL path'), max_length=255,
blank=True, null=True, default=None)
slug = models.SlugField(unique=True, max_length=255, null=True,
@ -544,6 +548,12 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
"""Return list products with type wine"""
return self.products.wines()
@property
def main_image(self):
qs = self.establishment_gallery.main_image()
if qs.exists():
return qs.first().image
class EstablishmentNoteQuerySet(models.QuerySet):
"""QuerySet for model EstablishmentNote."""
@ -565,8 +575,26 @@ class EstablishmentNote(ProjectBaseMixin):
class Meta:
"""Meta class."""
verbose_name_plural = _('product note')
verbose_name = _('product notes')
verbose_name_plural = _('establishment notes')
verbose_name = _('establishment note')
class EstablishmentGallery(IntermediateGalleryModelMixin):
establishment = models.ForeignKey(Establishment, null=True,
related_name='establishment_gallery',
on_delete=models.CASCADE,
verbose_name=_('establishment'))
image = models.ForeignKey('gallery.Image', null=True,
related_name='establishment_gallery',
on_delete=models.CASCADE,
verbose_name=_('image'))
class Meta:
"""Meta class."""
verbose_name = _('establishment gallery')
verbose_name_plural = _('establishment galleries')
unique_together = (('establishment', 'is_main'), ('establishment', 'image'))
class Position(BaseAttributes, TranslatedFieldsMixin):

View File

@ -9,6 +9,8 @@ from location.serializers import AddressDetailSerializer
from main.models import Currency
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 _
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
@ -160,3 +162,45 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
'user',
'name'
]
class EstablishmentBackOfficeGallerySerializer(serializers.ModelSerializer):
"""Serializer class for model EstablishmentGallery."""
class Meta:
"""Meta class"""
model = models.EstablishmentGallery
fields = [
'id',
'is_main',
]
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."""
establishment_pk = self.get_request_kwargs().get('pk')
image_id = self.get_request_kwargs().get('image_id')
establishment_qs = models.Establishment.objects.filter(pk=establishment_pk)
image_qs = Image.objects.filter(id=image_id)
if not establishment_qs.exists():
raise serializers.ValidationError({'detail': _('Establishment not found')})
if not image_qs.exists():
raise serializers.ValidationError({'detail': _('Image not found')})
establishment = establishment_qs.first()
image = image_qs.first()
if image in establishment.gallery.all():
raise serializers.ValidationError({'detail': _('Image is already added.')})
attrs['establishment'] = establishment
attrs['image'] = image
return attrs

View File

@ -13,6 +13,7 @@ from utils import exceptions as utils_exceptions
from utils.serializers import (ProjectModelSerializer, TranslatedField,
FavoritesCreateSerializer)
from review.serializers import ReviewShortSerializer
from utils.serializers import ImageBaseSerializer
class ContactPhonesSerializer(serializers.ModelSerializer):
@ -206,13 +207,18 @@ class EstablishmentProductSerializer(EstablishmentShortSerializer):
class EstablishmentBaseSerializer(ProjectModelSerializer):
"""Base serializer for Establishment model."""
preview_image = serializers.URLField(source='preview_image_url')
address = AddressBaseSerializer()
in_favorites = serializers.BooleanField(allow_null=True)
tags = TagBaseSerializer(read_only=True, many=True)
currency = CurrencySerializer()
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes')
image = serializers.URLField(source='image_url', read_only=True)
preview_image = serializers.URLField(source='preview_image_url',
allow_null=True,
read_only=True)
new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True)
class Meta:
"""Meta class."""
@ -227,13 +233,15 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
'toque_number',
'public_mark',
'slug',
'preview_image',
'in_favorites',
'address',
'tags',
'currency',
'type',
'subtypes',
'image',
'preview_image',
'new_image',
]
@ -272,7 +280,6 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
"""Serializer for Establishment model."""
description_translated = TranslatedField()
image = serializers.URLField(source='image_url')
awards = AwardSerializer(many=True)
schedule = ScheduleRUDSerializer(many=True, allow_null=True)
phones = ContactPhonesSerializer(read_only=True, many=True)
@ -288,6 +295,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
range_price_menu = RangePriceSerializer(read_only=True)
range_price_carte = RangePriceSerializer(read_only=True)
vintage_year = serializers.ReadOnlyField()
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
class Meta(EstablishmentBaseSerializer.Meta):
"""Meta class."""
@ -313,6 +321,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
'range_price_carte',
'transportation',
'vintage_year',
'gallery',
]
@ -379,4 +388,3 @@ class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
'content_object': validated_data.pop('establishment')
})
return super().create(validated_data)

View File

@ -31,4 +31,11 @@ urlpatterns = [
path('types/<int:pk>/', views.EstablishmentTypeRUDView.as_view(), name='type-rud'),
path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'),
path('subtypes/<int:pk>/', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'),
# gallery
path('<int:pk>/gallery/', views.EstablishmentBackOfficeGalleryListView.as_view(),
name='gallery-list'),
path('<int:pk>/gallery/<int:image_id>/',
views.EstablishmentBackOfficeGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'),
]

View File

@ -2,9 +2,10 @@
from django.shortcuts import get_object_or_404
from rest_framework import generics, permissions
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
from establishment import filters, models, serializers
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
from utils.views import CreateDestroyGalleryViewMixin
class EstablishmentMixinViews:
@ -184,3 +185,43 @@ class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView):
"""Establishment subtype retrieve/update/destroy view."""
serializer_class = serializers.EstablishmentSubTypeBaseSerializer
queryset = models.EstablishmentSubType.objects.all()
class EstablishmentBackOfficeGalleryCreateDestroyView(EstablishmentMixinViews,
CreateDestroyGalleryViewMixin):
"""Resource for a create|destroy gallery for product for back-office users."""
serializer_class = serializers.EstablishmentBackOfficeGallerySerializer
def get_object(self):
"""
Returns the object the view is displaying.
"""
establishment_qs = self.filter_queryset(self.get_queryset())
establishment = get_object_or_404(establishment_qs, pk=self.kwargs['pk'])
gallery = get_object_or_404(establishment.establishment_gallery, image_id=self.kwargs['image_id'])
# May raise a permission denied
self.check_object_permissions(self.request, gallery)
return gallery
class EstablishmentBackOfficeGalleryListView(EstablishmentMixinViews,
generics.ListAPIView):
"""Resource for returning gallery for establishment for back-office users."""
serializer_class = serializers.ImageBaseSerializer
def get_object(self):
"""Override get_object method."""
qs = super(EstablishmentBackOfficeGalleryListView, self).get_queryset()
establishment = get_object_or_404(qs, pk=self.kwargs['pk'])
# May raise a permission denied
self.check_object_permissions(self.request, establishment)
return establishment
def get_queryset(self):
"""Override get_queryset method."""
return self.get_object().crop_gallery

View File

@ -24,12 +24,19 @@ def send_email_action(modeladmin, request, queryset):
send_email_action.short_description = "Send the selected news by email"
class NewsGalleryInline(admin.TabularInline):
"""News gallery inline."""
model = models.NewsGallery
extra = 0
@admin.register(models.News)
class NewsAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""News admin."""
raw_id_fields = ('address',)
actions = [send_email_action]
raw_id_fields = ('news_type', 'address', 'country')
inlines = [NewsGalleryInline, ]
@admin.register(models.NewsGallery)

View File

@ -7,7 +7,8 @@ from django.utils.translation import gettext_lazy as _
from rest_framework.reverse import reverse
from rating.models import Rating, ViewCount
from utils.models import BaseAttributes, TJSONField, TranslatedFieldsMixin, ProjectBaseMixin
from utils.models import (BaseAttributes, TJSONField, TranslatedFieldsMixin,
ProjectBaseMixin, GalleryModelMixin, IntermediateGalleryModelMixin)
from utils.querysets import TranslationQuerysetMixin
@ -124,7 +125,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
)
class News(BaseAttributes, TranslatedFieldsMixin):
class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin):
"""News model."""
STR_FIELD_NAME = 'title'
@ -248,15 +249,8 @@ class News(BaseAttributes, TranslatedFieldsMixin):
return count_value
class NewsGalleryQuerySet(models.QuerySet):
"""QuerySet for model News"""
class NewsGallery(IntermediateGalleryModelMixin):
def main_image(self):
"""Return objects with flag is_main is True"""
return self.filter(is_main=True)
class NewsGallery(models.Model):
news = models.ForeignKey(News, null=True,
related_name='news_gallery',
on_delete=models.CASCADE,
@ -265,10 +259,6 @@ class NewsGallery(models.Model):
related_name='news_gallery',
on_delete=models.CASCADE,
verbose_name=_('gallery'))
is_main = models.BooleanField(default=False,
verbose_name=_('Is the main image'))
objects = NewsGalleryQuerySet.as_manager()
class Meta:
"""NewsGallery meta class."""

View File

@ -10,7 +10,8 @@ from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
from news import models
from tag.serializers import TagBaseSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import TranslatedField, ProjectModelSerializer, FavoritesCreateSerializer
from utils.serializers import (TranslatedField, ProjectModelSerializer,
FavoritesCreateSerializer, ImageBaseSerializer)
class AgendaSerializer(ProjectModelSerializer):
@ -47,78 +48,6 @@ class NewsBannerSerializer(ProjectModelSerializer):
)
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)
original_url = serializers.URLField(source='image.url')
auto_crop_images = CropImageSerializer(source='image', allow_null=True)
class Meta:
model = Image
fields = [
'id',
'title',
'orientation_display',
'original_url',
'auto_crop_images',
]
extra_kwargs = {
'orientation': {'write_only': True}
}
class NewsTypeSerializer(serializers.ModelSerializer):
"""News type serializer."""
@ -170,7 +99,7 @@ class NewsSimilarListSerializer(NewsBaseSerializer):
class NewsListSerializer(NewsBaseSerializer):
"""List serializer for News model."""
image = NewsImageSerializer(source='main_image', allow_null=True)
image = ImageBaseSerializer(source='crop_main_image', allow_null=True)
class Meta(NewsBaseSerializer.Meta):
"""Meta class."""
@ -188,7 +117,7 @@ class NewsDetailSerializer(NewsBaseSerializer):
author = UserBaseSerializer(source='created_by', read_only=True)
state_display = serializers.CharField(source='get_state_display',
read_only=True)
gallery = NewsImageSerializer(read_only=True, many=True)
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
class Meta(NewsBaseSerializer.Meta):
"""Meta class."""

View File

@ -1,15 +1,13 @@
"""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 rest_framework import generics, permissions
from gallery.tasks import delete_image
from news import filters, models, serializers
from rating.tasks import add_rating
from utils.permissions import IsCountryAdmin, IsContentPageManager
from utils.views import CreateDestroyGalleryViewMixin
from utils.serializers import ImageBaseSerializer
class NewsMixinView:
@ -118,9 +116,10 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
return gallery
class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIView):
class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView,
generics.ListAPIView):
"""Resource for returning gallery for news for back-office users."""
serializer_class = serializers.NewsImageSerializer
serializer_class = ImageBaseSerializer
def get_object(self):
"""Override get_object method."""
@ -134,7 +133,7 @@ class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIVie
def get_queryset(self):
"""Override get_queryset method."""
return self.get_object().gallery.all()
return self.get_object().crop_gallery
class NewsBackOfficeRUDView(NewsBackOfficeMixinView,

View File

@ -4,22 +4,23 @@ from utils.admin import BaseModelAdminMixin
from .models import Product, ProductType, ProductSubType, ProductGallery, Unit
class ProductGalleryInline(admin.TabularInline):
"""Product gallery inline."""
model = ProductGallery
extra = 0
@admin.register(Product)
class ProductAdmin(BaseModelAdminMixin, admin.ModelAdmin):
"""Admin page for model Product."""
search_fields = ('name', )
list_filter = ('available', 'product_type')
list_display = ('id', '__str__', 'get_category_display', 'product_type')
inlines = [ProductGalleryInline, ]
raw_id_fields = ('subtypes', 'classifications', 'standards',
'tags', 'gallery', 'establishment',)
@admin.register(ProductGallery)
class ProductGalleryAdmin(admin.ModelAdmin):
"""Admin page for model ProductGallery."""
raw_id_fields = ('product', 'image', )
@admin.register(ProductType)
class ProductTypeAdmin(admin.ModelAdmin):
"""Admin page for model ProductType."""

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.7 on 2019-11-17 11:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('product', '0013_auto_20191113_1512'),
]
operations = [
migrations.AlterField(
model_name='productgallery',
name='image',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='product_gallery', to='gallery.Image', verbose_name='image'),
),
]

View File

@ -8,7 +8,8 @@ from django.utils.translation import gettext_lazy as _
from django.core.validators import MaxValueValidator, MinValueValidator
from utils.models import (BaseAttributes, ProjectBaseMixin,
TranslatedFieldsMixin, TJSONField)
TranslatedFieldsMixin, TJSONField,
GalleryModelMixin, IntermediateGalleryModelMixin)
class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
@ -125,7 +126,7 @@ class ProductQuerySet(models.QuerySet):
)
class Product(TranslatedFieldsMixin, BaseAttributes):
class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes):
"""Product models."""
EARLIEST_VINTAGE_YEAR = 1700
@ -222,16 +223,6 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
"""Override str dunder method."""
return f'{self.name}'
def clean_fields(self, exclude=None):
super().clean_fields(exclude=exclude)
if self.product_type.index_name == ProductType.WINE and not self.wine_region:
raise ValidationError(_('wine_region field must be specified.'))
if not self.product_type.index_name == ProductType.WINE and self.wine_region:
raise ValidationError(_('wine_region field must not be specified.'))
# if (self.wine_region and self.wine_appellation) and \
# self.wine_appellation not in self.wine_region.appellations.all():
# raise ValidationError(_('Wine appellation not exists in wine region.'))
@property
def product_type_translated_name(self):
"""Get translated name of product type."""
@ -254,20 +245,6 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
def bottles_produced(self):
return self.tags.filter(category__index_name='bottles-produced')
@property
def main_image(self):
qs = ProductGallery.objects.filter(product=self, is_main=True)
if qs.exists():
return qs.first().image
@property
def main_image_url(self):
return self.main_image.image if self.main_image else None
@property
def preview_main_image_url(self):
return self.main_image.get_image_url('product_preview') if self.main_image else None
@property
def related_tags(self):
return self.tags.exclude(
@ -282,6 +259,21 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
name = f'{self.establishment.name} - ' + name
return name
@property
def main_image(self):
qs = self.product_gallery.main_image()
if qs.exists():
return qs.first().image
@property
def image_url(self):
return self.main_image.image.url if self.main_image else None
@property
def preview_image_url(self):
if self.main_image:
return self.main_image.get_image_url(thumbnail_key='product_preview')
class OnlineProductManager(ProductManager):
"""Extended manger for OnlineProduct model."""
@ -353,15 +345,8 @@ class ProductStandard(models.Model):
verbose_name = _('wine standard')
class ProductGalleryQuerySet(models.QuerySet):
"""QuerySet for model Product"""
class ProductGallery(IntermediateGalleryModelMixin):
def main_image(self):
"""Return objects with flag is_main is True"""
return self.filter(is_main=True)
class ProductGallery(models.Model):
product = models.ForeignKey(Product, null=True,
related_name='product_gallery',
on_delete=models.CASCADE,
@ -369,11 +354,7 @@ class ProductGallery(models.Model):
image = models.ForeignKey('gallery.Image', null=True,
related_name='product_gallery',
on_delete=models.CASCADE,
verbose_name=_('gallery'))
is_main = models.BooleanField(default=False,
verbose_name=_('Is the main image'))
objects = ProductGalleryQuerySet.as_manager()
verbose_name=_('image'))
class Meta:
"""ProductGallery meta class."""

View File

@ -9,7 +9,7 @@ from gallery.models import Image
from product import models
from review.serializers import ReviewShortSerializer
from utils import exceptions as utils_exceptions
from utils.serializers import TranslatedField, FavoritesCreateSerializer
from utils.serializers import TranslatedField, FavoritesCreateSerializer, ImageBaseSerializer
from main.serializers import AwardSerializer
from location.serializers import WineRegionBaseSerializer, WineSubRegionBaseSerializer
from tag.serializers import TagBaseSerializer, TagCategoryShortSerializer
@ -83,6 +83,15 @@ class ProductStandardBaseSerializer(serializers.ModelSerializer):
)
class ProductCropImageSerializer(serializers.Serializer):
"""Serializer for product image."""
class ProductImageSerializer(ImageBaseSerializer):
"""Serializer for product image."""
auto_crop_images = ProductCropImageSerializer(allow_null=True)
class ProductBaseSerializer(serializers.ModelSerializer):
"""Product base serializer."""
name = serializers.CharField(source='display_name', read_only=True)
@ -92,8 +101,7 @@ class ProductBaseSerializer(serializers.ModelSerializer):
tags = ProductTagSerializer(source='related_tags', many=True, read_only=True)
wine_region = WineRegionBaseSerializer(read_only=True)
wine_colors = TagBaseSerializer(many=True, read_only=True)
preview_image_url = serializers.URLField(source='preview_main_image_url',
allow_null=True,
preview_image_url = serializers.URLField(allow_null=True,
read_only=True)
in_favorites = serializers.BooleanField(allow_null=True)
@ -127,9 +135,10 @@ class ProductDetailSerializer(ProductBaseSerializer):
wine_sub_region = WineSubRegionBaseSerializer(read_only=True)
bottles_produced = TagBaseSerializer(many=True, read_only=True)
sugar_contents = TagBaseSerializer(many=True, read_only=True)
image_url = serializers.ImageField(source='main_image_url',
allow_null=True,
read_only=True)
image_url = serializers.URLField(allow_null=True,
read_only=True)
new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True)
class Meta(ProductBaseSerializer.Meta):
fields = ProductBaseSerializer.Meta.fields + [
@ -142,6 +151,7 @@ class ProductDetailSerializer(ProductBaseSerializer):
'bottles_produced',
'sugar_contents',
'image_url',
'new_image',
]
@ -175,78 +185,6 @@ class ProductFavoritesCreateSerializer(FavoritesCreateSerializer):
return super().create(validated_data)
# 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 ProductImageSerializer(serializers.ModelSerializer):
"""Serializer for returning crop images of product image."""
orientation_display = serializers.CharField(source='get_orientation_display',
read_only=True)
original_url = serializers.URLField(source='image.url')
# auto_crop_images = CropImageSerializer(source='image', allow_null=True)
class Meta:
model = Image
fields = [
'id',
'title',
'orientation_display',
'original_url',
# 'auto_crop_images',
]
extra_kwargs = {
'orientation': {'write_only': True}
}
class ProductCommentCreateSerializer(CommentSerializer):
"""Create comment serializer"""
mark = serializers.IntegerField()

View File

@ -5,6 +5,7 @@ from rest_framework.response import Response
from product import serializers, models
from product.views import ProductBaseView
from utils.serializers import ImageBaseSerializer
from utils.views import CreateDestroyGalleryViewMixin
@ -65,9 +66,11 @@ class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView,
return gallery
class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.ListAPIView):
class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView,
generics.ListAPIView):
"""Resource for returning gallery for product for back-office users."""
serializer_class = serializers.ProductImageSerializer
serializer_class = ImageBaseSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_object(self):
"""Override get_object method."""
@ -81,10 +84,11 @@ class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.List
def get_queryset(self):
"""Override get_queryset method."""
return self.get_object().gallery.all()
return self.get_object().crop_gallery
class ProductDetailBackOfficeView(ProductBackOfficeMixinView, generics.RetrieveUpdateDestroyAPIView):
class ProductDetailBackOfficeView(ProductBackOfficeMixinView,
generics.RetrieveUpdateDestroyAPIView):
"""Product back-office R/U/D view."""
serializer_class = serializers.ProductBackOfficeDetailSerializer

View File

@ -0,0 +1,19 @@
# Generated by Django 2.2.7 on 2019-11-17 11:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('review', '0017_auto_20191115_0737'),
]
operations = [
migrations.AlterField(
model_name='inquiriesgallery',
name='image',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='inquiries_gallery', to='gallery.Image', verbose_name='image'),
),
]

View File

@ -4,8 +4,9 @@ from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from django.utils.translation import gettext_lazy as _
from utils.models import BaseAttributes, TranslatedFieldsMixin, ProjectBaseMixin
from utils.models import TJSONField
from utils.models import (BaseAttributes, TranslatedFieldsMixin,
ProjectBaseMixin, GalleryModelMixin,
TJSONField, IntermediateGalleryModelMixin)
class ReviewQuerySet(models.QuerySet):
@ -92,7 +93,7 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
verbose_name_plural = _('Reviews')
class Inquiries(ProjectBaseMixin):
class Inquiries(GalleryModelMixin, ProjectBaseMixin):
NONE = 0
DINER = 1
LUNCH = 2
@ -145,15 +146,7 @@ class GridItems(ProjectBaseMixin):
return f'inquiry: {self.inquiry.id}, grid id: {self.id}'
class InquiriesGalleryQuerySet(models.QuerySet):
"""QuerySet for model Inquiries"""
def main_image(self):
"""Return objects with flag is_main is True"""
return self.filter(is_main=True)
class InquiriesGallery(models.Model):
class InquiriesGallery(IntermediateGalleryModelMixin):
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
inquiry = models.ForeignKey(
Inquiries,
@ -167,11 +160,8 @@ class InquiriesGallery(models.Model):
null=True,
related_name='inquiries_gallery',
on_delete=models.CASCADE,
verbose_name=_('gallery'),
verbose_name=_('image'),
)
is_main = models.BooleanField(default=False, verbose_name=_('Is the main image'))
objects = InquiriesGalleryQuerySet.as_manager()
class Meta:
verbose_name = _('inquiry gallery')

View File

@ -30,12 +30,12 @@ class Command(BaseCommand):
'update_country_flag',
'comment',
'inquiries', # №6 - перенос запросов оценок
'wine_characteristics',
'product',
'product_note',
'souvenir',
'establishment_note',
'assemblage',
'wine_characteristics', # №5 - перенос характиристик вин
'product', # №5 - перенос продуктов
'product_note', # №6 - перенос заметок продуктов
'souvenir', # №5 - перенос продуктов типа - сувениры
'establishment_note', # №5 - перенос заметок заведений
'assemblage', # №6 - перенос тегов для типа продуктов - вино
'rating_count',
'product_review',
'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1

View File

@ -345,4 +345,77 @@ class GMTokenGenerator(PasswordResetTokenGenerator):
return self.get_fields(user, timestamp)
class GalleryModelMixin(models.Model):
"""Mixin for models that has gallery."""
@property
def crop_gallery(self):
if hasattr(self, 'gallery'):
gallery = []
images = self.gallery.all()
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
if p.startswith(self._meta.model_name.lower())]
for image in images:
d = {
'id': image.id,
'title': image.title,
'original_url': image.image.url,
'orientation_display': image.get_orientation_display(),
'auto_crop_images': {},
}
for crop in crop_parameters:
d['auto_crop_images'].update({crop: image.get_image_url(crop)})
gallery.append(d)
return gallery
@property
def crop_main_image(self):
if hasattr(self, 'main_image') and self.main_image:
image = self.main_image
image_property = {
'id': image.id,
'title': image.title,
'original_url': image.image.url,
'orientation_display': image.get_orientation_display(),
'auto_crop_images': {},
}
crop_parameters = [p for p in settings.SORL_THUMBNAIL_ALIASES
if p.startswith(self._meta.model_name.lower())]
for crop in crop_parameters:
image_property['auto_crop_images'].update(
{crop: image.get_image_url(crop)}
)
return image_property
class Meta:
"""Meta class."""
abstract = True
class IntermediateGalleryModelQuerySet(models.QuerySet):
"""Extended QuerySet."""
def main_image(self):
"""Return objects with flag is_main is True"""
return self.filter(is_main=True)
class IntermediateGalleryModelMixin(models.Model):
"""Mixin for intermediate gallery model."""
is_main = models.BooleanField(default=False,
verbose_name=_('Is the main image'))
objects = IntermediateGalleryModelQuerySet.as_manager()
class Meta:
"""Meta class."""
abstract = True
def __str__(self):
"""Overridden str method."""
if hasattr(self, 'image'):
return self.image.title if self.image.title else self.id
timezone.datetime.now().date().isoformat()

View File

@ -5,6 +5,7 @@ from rest_framework import serializers
from utils import models
from translation.models import Language
from favorites.models import Favorites
from gallery.models import Image
class EmptySerializer(serializers.Serializer):
@ -104,3 +105,13 @@ class RecursiveFieldSerializer(serializers.Serializer):
def to_representation(self, value):
serializer = self.parent.parent.__class__(value, context=self.context)
return serializer.data
class ImageBaseSerializer(serializers.Serializer):
"""Serializer for returning crop images of model image."""
id = serializers.IntegerField()
title = serializers.CharField()
original_url = serializers.URLField()
orientation_display = serializers.CharField()
auto_crop_images = serializers.DictField(allow_null=True)

View File

@ -380,6 +380,14 @@ SORL_THUMBNAIL_ALIASES = {
'news_editor_mobile': {'geometry_string': '343x260', 'crop': 'center'}, # при загрузке через контент эдитор
'avatar_comments_web': {'geometry_string': '116x116', 'crop': 'center'}, # через контент эдитор в мобильном браузерe
'product_preview': {'geometry_string': '300x260', 'crop': 'center'},
'establishment_preview': {'geometry_string': '300x260', 'crop': 'center'},
'establishment_xsmall': {'geometry_string': '60x34', 'crop': 'center'},
'establishment_small': {'geometry_string': '80x45', 'crop': 'center'},
'establishment_medium': {'geometry_string': '280x158', 'crop': 'center'},
'establishment_large': {'geometry_string': '440x248', 'crop': 'center'},
'establishment_xlarge': {'geometry_string': '640x360', 'crop': 'center'},
'establishment_detail': {'geometry_string': '2048x1152', 'crop': 'center'},
'establishment_original': {'geometry_string': '1920x1080', 'crop': 'center'},
}