refactored galleries, added transfer script
This commit is contained in:
parent
6c82af1940
commit
a8b3c9aa63
17
apps/advertisement/migrations/0008_auto_20191117_1117.py
Normal file
17
apps/advertisement/migrations/0008_auto_20191117_1117.py
Normal 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'},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -32,6 +32,12 @@ class ContactPhoneInline(admin.TabularInline):
|
||||||
extra = 0
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
|
class GalleryImageInline(admin.TabularInline):
|
||||||
|
"""Gallery image inline admin."""
|
||||||
|
model = models.EstablishmentGallery
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
class ContactEmailInline(admin.TabularInline):
|
class ContactEmailInline(admin.TabularInline):
|
||||||
"""Contact email inline admin."""
|
"""Contact email inline admin."""
|
||||||
model = models.ContactEmail
|
model = models.ContactEmail
|
||||||
|
|
@ -59,6 +65,7 @@ class EstablishmentAdmin(BaseModelAdminMixin, admin.ModelAdmin):
|
||||||
list_display = ['id', '__str__', 'image_tag', ]
|
list_display = ['id', '__str__', 'image_tag', ]
|
||||||
search_fields = ['id', 'name', 'index_name', 'slug']
|
search_fields = ['id', 'name', 'index_name', 'slug']
|
||||||
list_filter = ['public_mark', 'toque_number']
|
list_filter = ['public_mark', 'toque_number']
|
||||||
|
inlines = [GalleryImageInline, ]
|
||||||
|
|
||||||
# inlines = [
|
# inlines = [
|
||||||
# AwardInline, ContactPhoneInline, ContactEmailInline,
|
# AwardInline, ContactPhoneInline, ContactEmailInline,
|
||||||
|
|
|
||||||
|
|
@ -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}'))
|
||||||
38
apps/establishment/migrations/0062_auto_20191117_1117.py
Normal file
38
apps/establishment/migrations/0062_auto_20191117_1117.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -22,7 +22,8 @@ from location.models import Address
|
||||||
from main.models import Award, Currency
|
from main.models import Award, Currency
|
||||||
from review.models import Review
|
from review.models import Review
|
||||||
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
from utils.models import (ProjectBaseMixin, TJSONField, URLImageMixin,
|
||||||
TranslatedFieldsMixin, BaseAttributes)
|
TranslatedFieldsMixin, BaseAttributes, GalleryModelMixin,
|
||||||
|
IntermediateGalleryModelMixin)
|
||||||
|
|
||||||
|
|
||||||
# todo: establishment type&subtypes check
|
# todo: establishment type&subtypes check
|
||||||
|
|
@ -316,9 +317,11 @@ class EstablishmentQuerySet(models.QuerySet):
|
||||||
return self.exclude(address__city__country__in=countries)
|
return self.exclude(address__city__country__in=countries)
|
||||||
|
|
||||||
|
|
||||||
class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
|
class Establishment(GalleryModelMixin, ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
|
||||||
"""Establishment model."""
|
"""Establishment model."""
|
||||||
|
|
||||||
|
# todo: delete image URL fields after moving on gallery
|
||||||
|
|
||||||
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
||||||
name = models.CharField(_('name'), max_length=255, default='')
|
name = models.CharField(_('name'), max_length=255, default='')
|
||||||
transliterated_name = models.CharField(default='', max_length=255,
|
transliterated_name = models.CharField(default='', max_length=255,
|
||||||
|
|
@ -376,6 +379,7 @@ class Establishment(ProjectBaseMixin, URLImageMixin, TranslatedFieldsMixin):
|
||||||
related_name='establishments',
|
related_name='establishments',
|
||||||
blank=True, default=None,
|
blank=True, default=None,
|
||||||
verbose_name=_('Collections'))
|
verbose_name=_('Collections'))
|
||||||
|
gallery = models.ManyToManyField('gallery.Image', through='EstablishmentGallery')
|
||||||
preview_image_url = models.URLField(verbose_name=_('Preview image URL path'), max_length=255,
|
preview_image_url = models.URLField(verbose_name=_('Preview image URL path'), max_length=255,
|
||||||
blank=True, null=True, default=None)
|
blank=True, null=True, default=None)
|
||||||
slug = models.SlugField(unique=True, max_length=255, null=True,
|
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 list products with type wine"""
|
||||||
return self.products.wines()
|
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):
|
class EstablishmentNoteQuerySet(models.QuerySet):
|
||||||
"""QuerySet for model EstablishmentNote."""
|
"""QuerySet for model EstablishmentNote."""
|
||||||
|
|
@ -565,8 +575,26 @@ class EstablishmentNote(ProjectBaseMixin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
verbose_name_plural = _('product note')
|
verbose_name_plural = _('establishment notes')
|
||||||
verbose_name = _('product 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):
|
class Position(BaseAttributes, TranslatedFieldsMixin):
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ from location.serializers import AddressDetailSerializer
|
||||||
from main.models import Currency
|
from main.models import Currency
|
||||||
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 _
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
|
class EstablishmentListCreateSerializer(EstablishmentBaseSerializer):
|
||||||
|
|
@ -160,3 +162,45 @@ class EmployeeBackSerializers(serializers.ModelSerializer):
|
||||||
'user',
|
'user',
|
||||||
'name'
|
'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
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ from utils import exceptions as utils_exceptions
|
||||||
from utils.serializers import (ProjectModelSerializer, TranslatedField,
|
from utils.serializers import (ProjectModelSerializer, TranslatedField,
|
||||||
FavoritesCreateSerializer)
|
FavoritesCreateSerializer)
|
||||||
from review.serializers import ReviewShortSerializer
|
from review.serializers import ReviewShortSerializer
|
||||||
|
from utils.serializers import ImageBaseSerializer
|
||||||
|
|
||||||
|
|
||||||
class ContactPhonesSerializer(serializers.ModelSerializer):
|
class ContactPhonesSerializer(serializers.ModelSerializer):
|
||||||
|
|
@ -206,13 +207,18 @@ class EstablishmentProductSerializer(EstablishmentShortSerializer):
|
||||||
class EstablishmentBaseSerializer(ProjectModelSerializer):
|
class EstablishmentBaseSerializer(ProjectModelSerializer):
|
||||||
"""Base serializer for Establishment model."""
|
"""Base serializer for Establishment model."""
|
||||||
|
|
||||||
preview_image = serializers.URLField(source='preview_image_url')
|
|
||||||
address = AddressBaseSerializer()
|
address = AddressBaseSerializer()
|
||||||
in_favorites = serializers.BooleanField(allow_null=True)
|
in_favorites = serializers.BooleanField(allow_null=True)
|
||||||
tags = TagBaseSerializer(read_only=True, many=True)
|
tags = TagBaseSerializer(read_only=True, many=True)
|
||||||
currency = CurrencySerializer()
|
currency = CurrencySerializer()
|
||||||
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
|
type = EstablishmentTypeBaseSerializer(source='establishment_type', read_only=True)
|
||||||
subtypes = EstablishmentSubTypeBaseSerializer(many=True, source='establishment_subtypes')
|
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:
|
class Meta:
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -227,13 +233,15 @@ class EstablishmentBaseSerializer(ProjectModelSerializer):
|
||||||
'toque_number',
|
'toque_number',
|
||||||
'public_mark',
|
'public_mark',
|
||||||
'slug',
|
'slug',
|
||||||
'preview_image',
|
|
||||||
'in_favorites',
|
'in_favorites',
|
||||||
'address',
|
'address',
|
||||||
'tags',
|
'tags',
|
||||||
'currency',
|
'currency',
|
||||||
'type',
|
'type',
|
||||||
'subtypes',
|
'subtypes',
|
||||||
|
'image',
|
||||||
|
'preview_image',
|
||||||
|
'new_image',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -272,7 +280,6 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
|
||||||
"""Serializer for Establishment model."""
|
"""Serializer for Establishment model."""
|
||||||
|
|
||||||
description_translated = TranslatedField()
|
description_translated = TranslatedField()
|
||||||
image = serializers.URLField(source='image_url')
|
|
||||||
awards = AwardSerializer(many=True)
|
awards = AwardSerializer(many=True)
|
||||||
schedule = ScheduleRUDSerializer(many=True, allow_null=True)
|
schedule = ScheduleRUDSerializer(many=True, allow_null=True)
|
||||||
phones = ContactPhonesSerializer(read_only=True, many=True)
|
phones = ContactPhonesSerializer(read_only=True, many=True)
|
||||||
|
|
@ -288,6 +295,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
|
||||||
range_price_menu = RangePriceSerializer(read_only=True)
|
range_price_menu = RangePriceSerializer(read_only=True)
|
||||||
range_price_carte = RangePriceSerializer(read_only=True)
|
range_price_carte = RangePriceSerializer(read_only=True)
|
||||||
vintage_year = serializers.ReadOnlyField()
|
vintage_year = serializers.ReadOnlyField()
|
||||||
|
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
|
||||||
|
|
||||||
class Meta(EstablishmentBaseSerializer.Meta):
|
class Meta(EstablishmentBaseSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -313,6 +321,7 @@ class EstablishmentDetailSerializer(EstablishmentBaseSerializer):
|
||||||
'range_price_carte',
|
'range_price_carte',
|
||||||
'transportation',
|
'transportation',
|
||||||
'vintage_year',
|
'vintage_year',
|
||||||
|
'gallery',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -379,4 +388,3 @@ class EstablishmentFavoritesCreateSerializer(FavoritesCreateSerializer):
|
||||||
'content_object': validated_data.pop('establishment')
|
'content_object': validated_data.pop('establishment')
|
||||||
})
|
})
|
||||||
return super().create(validated_data)
|
return super().create(validated_data)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,4 +31,11 @@ urlpatterns = [
|
||||||
path('types/<int:pk>/', views.EstablishmentTypeRUDView.as_view(), name='type-rud'),
|
path('types/<int:pk>/', views.EstablishmentTypeRUDView.as_view(), name='type-rud'),
|
||||||
path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'),
|
path('subtypes/', views.EstablishmentSubtypeListCreateView.as_view(), name='subtype-list'),
|
||||||
path('subtypes/<int:pk>/', views.EstablishmentSubtypeRUDView.as_view(), name='subtype-rud'),
|
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'),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions
|
||||||
|
|
||||||
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
|
|
||||||
from establishment import filters, models, serializers
|
from establishment import filters, models, serializers
|
||||||
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
|
from timetable.serialziers import ScheduleRUDSerializer, ScheduleCreateSerializer
|
||||||
|
from utils.permissions import IsCountryAdmin, IsEstablishmentManager
|
||||||
|
from utils.views import CreateDestroyGalleryViewMixin
|
||||||
|
|
||||||
|
|
||||||
class EstablishmentMixinViews:
|
class EstablishmentMixinViews:
|
||||||
|
|
@ -184,3 +185,43 @@ class EstablishmentSubtypeRUDView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
"""Establishment subtype retrieve/update/destroy view."""
|
"""Establishment subtype retrieve/update/destroy view."""
|
||||||
serializer_class = serializers.EstablishmentSubTypeBaseSerializer
|
serializer_class = serializers.EstablishmentSubTypeBaseSerializer
|
||||||
queryset = models.EstablishmentSubType.objects.all()
|
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
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,19 @@ def send_email_action(modeladmin, request, queryset):
|
||||||
send_email_action.short_description = "Send the selected news by email"
|
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)
|
@admin.register(models.News)
|
||||||
class NewsAdmin(BaseModelAdminMixin, admin.ModelAdmin):
|
class NewsAdmin(BaseModelAdminMixin, admin.ModelAdmin):
|
||||||
"""News admin."""
|
"""News admin."""
|
||||||
raw_id_fields = ('address',)
|
raw_id_fields = ('address',)
|
||||||
actions = [send_email_action]
|
actions = [send_email_action]
|
||||||
raw_id_fields = ('news_type', 'address', 'country')
|
raw_id_fields = ('news_type', 'address', 'country')
|
||||||
|
inlines = [NewsGalleryInline, ]
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.NewsGallery)
|
@admin.register(models.NewsGallery)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from rating.models import Rating, ViewCount
|
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
|
from utils.querysets import TranslationQuerysetMixin
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -124,7 +125,7 @@ class NewsQuerySet(TranslationQuerysetMixin):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class News(BaseAttributes, TranslatedFieldsMixin):
|
class News(GalleryModelMixin, BaseAttributes, TranslatedFieldsMixin):
|
||||||
"""News model."""
|
"""News model."""
|
||||||
|
|
||||||
STR_FIELD_NAME = 'title'
|
STR_FIELD_NAME = 'title'
|
||||||
|
|
@ -248,15 +249,8 @@ class News(BaseAttributes, TranslatedFieldsMixin):
|
||||||
return count_value
|
return count_value
|
||||||
|
|
||||||
|
|
||||||
class NewsGalleryQuerySet(models.QuerySet):
|
class NewsGallery(IntermediateGalleryModelMixin):
|
||||||
"""QuerySet for model News"""
|
|
||||||
|
|
||||||
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,
|
news = models.ForeignKey(News, null=True,
|
||||||
related_name='news_gallery',
|
related_name='news_gallery',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
|
@ -265,10 +259,6 @@ class NewsGallery(models.Model):
|
||||||
related_name='news_gallery',
|
related_name='news_gallery',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
verbose_name=_('gallery'))
|
verbose_name=_('gallery'))
|
||||||
is_main = models.BooleanField(default=False,
|
|
||||||
verbose_name=_('Is the main image'))
|
|
||||||
|
|
||||||
objects = NewsGalleryQuerySet.as_manager()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""NewsGallery meta class."""
|
"""NewsGallery meta class."""
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,8 @@ from location.serializers import CountrySimpleSerializer, AddressBaseSerializer
|
||||||
from news import models
|
from news import models
|
||||||
from tag.serializers import TagBaseSerializer
|
from tag.serializers import TagBaseSerializer
|
||||||
from utils import exceptions as utils_exceptions
|
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):
|
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):
|
class NewsTypeSerializer(serializers.ModelSerializer):
|
||||||
"""News type serializer."""
|
"""News type serializer."""
|
||||||
|
|
||||||
|
|
@ -170,7 +99,7 @@ class NewsSimilarListSerializer(NewsBaseSerializer):
|
||||||
class NewsListSerializer(NewsBaseSerializer):
|
class NewsListSerializer(NewsBaseSerializer):
|
||||||
"""List serializer for News model."""
|
"""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):
|
class Meta(NewsBaseSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
@ -188,7 +117,7 @@ class NewsDetailSerializer(NewsBaseSerializer):
|
||||||
author = UserBaseSerializer(source='created_by', read_only=True)
|
author = UserBaseSerializer(source='created_by', read_only=True)
|
||||||
state_display = serializers.CharField(source='get_state_display',
|
state_display = serializers.CharField(source='get_state_display',
|
||||||
read_only=True)
|
read_only=True)
|
||||||
gallery = NewsImageSerializer(read_only=True, many=True)
|
gallery = ImageBaseSerializer(read_only=True, source='crop_gallery', many=True)
|
||||||
|
|
||||||
class Meta(NewsBaseSerializer.Meta):
|
class Meta(NewsBaseSerializer.Meta):
|
||||||
"""Meta class."""
|
"""Meta class."""
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
"""News app views."""
|
"""News app views."""
|
||||||
from django.conf import settings
|
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
|
||||||
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 rating.tasks import add_rating
|
from rating.tasks import add_rating
|
||||||
from utils.permissions import IsCountryAdmin, IsContentPageManager
|
from utils.permissions import IsCountryAdmin, IsContentPageManager
|
||||||
from utils.views import CreateDestroyGalleryViewMixin
|
from utils.views import CreateDestroyGalleryViewMixin
|
||||||
|
from utils.serializers import ImageBaseSerializer
|
||||||
|
|
||||||
|
|
||||||
class NewsMixinView:
|
class NewsMixinView:
|
||||||
|
|
@ -118,9 +116,10 @@ class NewsBackOfficeGalleryCreateDestroyView(NewsBackOfficeMixinView,
|
||||||
return gallery
|
return gallery
|
||||||
|
|
||||||
|
|
||||||
class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView, generics.ListAPIView):
|
class NewsBackOfficeGalleryListView(NewsBackOfficeMixinView,
|
||||||
|
generics.ListAPIView):
|
||||||
"""Resource for returning gallery for news for back-office users."""
|
"""Resource for returning gallery for news for back-office users."""
|
||||||
serializer_class = serializers.NewsImageSerializer
|
serializer_class = ImageBaseSerializer
|
||||||
|
|
||||||
def get_object(self):
|
def get_object(self):
|
||||||
"""Override get_object method."""
|
"""Override get_object method."""
|
||||||
|
|
@ -134,7 +133,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.all()
|
return self.get_object().crop_gallery
|
||||||
|
|
||||||
|
|
||||||
class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
|
class NewsBackOfficeRUDView(NewsBackOfficeMixinView,
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,23 @@ from utils.admin import BaseModelAdminMixin
|
||||||
from .models import Product, ProductType, ProductSubType, ProductGallery, Unit
|
from .models import Product, ProductType, ProductSubType, ProductGallery, Unit
|
||||||
|
|
||||||
|
|
||||||
|
class ProductGalleryInline(admin.TabularInline):
|
||||||
|
"""Product gallery inline."""
|
||||||
|
model = ProductGallery
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Product)
|
@admin.register(Product)
|
||||||
class ProductAdmin(BaseModelAdminMixin, admin.ModelAdmin):
|
class ProductAdmin(BaseModelAdminMixin, admin.ModelAdmin):
|
||||||
"""Admin page for model Product."""
|
"""Admin page for model Product."""
|
||||||
search_fields = ('name', )
|
search_fields = ('name', )
|
||||||
list_filter = ('available', 'product_type')
|
list_filter = ('available', 'product_type')
|
||||||
list_display = ('id', '__str__', 'get_category_display', 'product_type')
|
list_display = ('id', '__str__', 'get_category_display', 'product_type')
|
||||||
|
inlines = [ProductGalleryInline, ]
|
||||||
raw_id_fields = ('subtypes', 'classifications', 'standards',
|
raw_id_fields = ('subtypes', 'classifications', 'standards',
|
||||||
'tags', 'gallery', 'establishment',)
|
'tags', 'gallery', 'establishment',)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ProductGallery)
|
|
||||||
class ProductGalleryAdmin(admin.ModelAdmin):
|
|
||||||
"""Admin page for model ProductGallery."""
|
|
||||||
raw_id_fields = ('product', 'image', )
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(ProductType)
|
@admin.register(ProductType)
|
||||||
class ProductTypeAdmin(admin.ModelAdmin):
|
class ProductTypeAdmin(admin.ModelAdmin):
|
||||||
"""Admin page for model ProductType."""
|
"""Admin page for model ProductType."""
|
||||||
|
|
|
||||||
19
apps/product/migrations/0014_auto_20191117_1117.py
Normal file
19
apps/product/migrations/0014_auto_20191117_1117.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -8,7 +8,8 @@ from django.utils.translation import gettext_lazy as _
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
|
|
||||||
from utils.models import (BaseAttributes, ProjectBaseMixin,
|
from utils.models import (BaseAttributes, ProjectBaseMixin,
|
||||||
TranslatedFieldsMixin, TJSONField)
|
TranslatedFieldsMixin, TJSONField,
|
||||||
|
GalleryModelMixin, IntermediateGalleryModelMixin)
|
||||||
|
|
||||||
|
|
||||||
class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
|
class ProductType(TranslatedFieldsMixin, ProjectBaseMixin):
|
||||||
|
|
@ -125,7 +126,7 @@ class ProductQuerySet(models.QuerySet):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Product(TranslatedFieldsMixin, BaseAttributes):
|
class Product(GalleryModelMixin, TranslatedFieldsMixin, BaseAttributes):
|
||||||
"""Product models."""
|
"""Product models."""
|
||||||
|
|
||||||
EARLIEST_VINTAGE_YEAR = 1700
|
EARLIEST_VINTAGE_YEAR = 1700
|
||||||
|
|
@ -222,16 +223,6 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
|
||||||
"""Override str dunder method."""
|
"""Override str dunder method."""
|
||||||
return f'{self.name}'
|
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
|
@property
|
||||||
def product_type_translated_name(self):
|
def product_type_translated_name(self):
|
||||||
"""Get translated name of product type."""
|
"""Get translated name of product type."""
|
||||||
|
|
@ -254,20 +245,6 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
|
||||||
def bottles_produced(self):
|
def bottles_produced(self):
|
||||||
return self.tags.filter(category__index_name='bottles-produced')
|
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
|
@property
|
||||||
def related_tags(self):
|
def related_tags(self):
|
||||||
return self.tags.exclude(
|
return self.tags.exclude(
|
||||||
|
|
@ -282,6 +259,21 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
|
||||||
name = f'{self.establishment.name} - ' + name
|
name = f'{self.establishment.name} - ' + name
|
||||||
return 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):
|
class OnlineProductManager(ProductManager):
|
||||||
"""Extended manger for OnlineProduct model."""
|
"""Extended manger for OnlineProduct model."""
|
||||||
|
|
@ -353,15 +345,8 @@ class ProductStandard(models.Model):
|
||||||
verbose_name = _('wine standard')
|
verbose_name = _('wine standard')
|
||||||
|
|
||||||
|
|
||||||
class ProductGalleryQuerySet(models.QuerySet):
|
class ProductGallery(IntermediateGalleryModelMixin):
|
||||||
"""QuerySet for model Product"""
|
|
||||||
|
|
||||||
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,
|
product = models.ForeignKey(Product, null=True,
|
||||||
related_name='product_gallery',
|
related_name='product_gallery',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
|
@ -369,11 +354,7 @@ class ProductGallery(models.Model):
|
||||||
image = models.ForeignKey('gallery.Image', null=True,
|
image = models.ForeignKey('gallery.Image', null=True,
|
||||||
related_name='product_gallery',
|
related_name='product_gallery',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
verbose_name=_('gallery'))
|
verbose_name=_('image'))
|
||||||
is_main = models.BooleanField(default=False,
|
|
||||||
verbose_name=_('Is the main image'))
|
|
||||||
|
|
||||||
objects = ProductGalleryQuerySet.as_manager()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
"""ProductGallery meta class."""
|
"""ProductGallery meta class."""
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ from gallery.models import Image
|
||||||
from product import models
|
from product import models
|
||||||
from review.serializers import ReviewShortSerializer
|
from review.serializers import ReviewShortSerializer
|
||||||
from utils import exceptions as utils_exceptions
|
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 main.serializers import AwardSerializer
|
||||||
from location.serializers import WineRegionBaseSerializer, WineSubRegionBaseSerializer
|
from location.serializers import WineRegionBaseSerializer, WineSubRegionBaseSerializer
|
||||||
from tag.serializers import TagBaseSerializer, TagCategoryShortSerializer
|
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):
|
class ProductBaseSerializer(serializers.ModelSerializer):
|
||||||
"""Product base serializer."""
|
"""Product base serializer."""
|
||||||
name = serializers.CharField(source='display_name', read_only=True)
|
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)
|
tags = ProductTagSerializer(source='related_tags', many=True, read_only=True)
|
||||||
wine_region = WineRegionBaseSerializer(read_only=True)
|
wine_region = WineRegionBaseSerializer(read_only=True)
|
||||||
wine_colors = TagBaseSerializer(many=True, read_only=True)
|
wine_colors = TagBaseSerializer(many=True, read_only=True)
|
||||||
preview_image_url = serializers.URLField(source='preview_main_image_url',
|
preview_image_url = serializers.URLField(allow_null=True,
|
||||||
allow_null=True,
|
|
||||||
read_only=True)
|
read_only=True)
|
||||||
in_favorites = serializers.BooleanField(allow_null=True)
|
in_favorites = serializers.BooleanField(allow_null=True)
|
||||||
|
|
||||||
|
|
@ -127,9 +135,10 @@ class ProductDetailSerializer(ProductBaseSerializer):
|
||||||
wine_sub_region = WineSubRegionBaseSerializer(read_only=True)
|
wine_sub_region = WineSubRegionBaseSerializer(read_only=True)
|
||||||
bottles_produced = TagBaseSerializer(many=True, read_only=True)
|
bottles_produced = TagBaseSerializer(many=True, read_only=True)
|
||||||
sugar_contents = TagBaseSerializer(many=True, read_only=True)
|
sugar_contents = TagBaseSerializer(many=True, read_only=True)
|
||||||
image_url = serializers.ImageField(source='main_image_url',
|
image_url = serializers.URLField(allow_null=True,
|
||||||
allow_null=True,
|
read_only=True)
|
||||||
read_only=True)
|
|
||||||
|
new_image = ImageBaseSerializer(source='crop_main_image', allow_null=True, read_only=True)
|
||||||
|
|
||||||
class Meta(ProductBaseSerializer.Meta):
|
class Meta(ProductBaseSerializer.Meta):
|
||||||
fields = ProductBaseSerializer.Meta.fields + [
|
fields = ProductBaseSerializer.Meta.fields + [
|
||||||
|
|
@ -142,6 +151,7 @@ class ProductDetailSerializer(ProductBaseSerializer):
|
||||||
'bottles_produced',
|
'bottles_produced',
|
||||||
'sugar_contents',
|
'sugar_contents',
|
||||||
'image_url',
|
'image_url',
|
||||||
|
'new_image',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -175,78 +185,6 @@ class ProductFavoritesCreateSerializer(FavoritesCreateSerializer):
|
||||||
return super().create(validated_data)
|
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):
|
class ProductCommentCreateSerializer(CommentSerializer):
|
||||||
"""Create comment serializer"""
|
"""Create comment serializer"""
|
||||||
mark = serializers.IntegerField()
|
mark = serializers.IntegerField()
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
from product import serializers, models
|
from product import serializers, models
|
||||||
from product.views import ProductBaseView
|
from product.views import ProductBaseView
|
||||||
|
from utils.serializers import ImageBaseSerializer
|
||||||
from utils.views import CreateDestroyGalleryViewMixin
|
from utils.views import CreateDestroyGalleryViewMixin
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -65,9 +66,11 @@ class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView,
|
||||||
return gallery
|
return gallery
|
||||||
|
|
||||||
|
|
||||||
class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.ListAPIView):
|
class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView,
|
||||||
|
generics.ListAPIView):
|
||||||
"""Resource for returning gallery for product for back-office users."""
|
"""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):
|
def get_object(self):
|
||||||
"""Override get_object method."""
|
"""Override get_object method."""
|
||||||
|
|
@ -81,10 +84,11 @@ class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.List
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Override get_queryset method."""
|
"""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."""
|
"""Product back-office R/U/D view."""
|
||||||
serializer_class = serializers.ProductBackOfficeDetailSerializer
|
serializer_class = serializers.ProductBackOfficeDetailSerializer
|
||||||
|
|
||||||
|
|
|
||||||
19
apps/review/migrations/0018_auto_20191117_1117.py
Normal file
19
apps/review/migrations/0018_auto_20191117_1117.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -4,8 +4,9 @@ from django.core.validators import MinValueValidator, MaxValueValidator
|
||||||
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 utils.models import BaseAttributes, TranslatedFieldsMixin, ProjectBaseMixin
|
from utils.models import (BaseAttributes, TranslatedFieldsMixin,
|
||||||
from utils.models import TJSONField
|
ProjectBaseMixin, GalleryModelMixin,
|
||||||
|
TJSONField, IntermediateGalleryModelMixin)
|
||||||
|
|
||||||
|
|
||||||
class ReviewQuerySet(models.QuerySet):
|
class ReviewQuerySet(models.QuerySet):
|
||||||
|
|
@ -92,7 +93,7 @@ class Review(BaseAttributes, TranslatedFieldsMixin):
|
||||||
verbose_name_plural = _('Reviews')
|
verbose_name_plural = _('Reviews')
|
||||||
|
|
||||||
|
|
||||||
class Inquiries(ProjectBaseMixin):
|
class Inquiries(GalleryModelMixin, ProjectBaseMixin):
|
||||||
NONE = 0
|
NONE = 0
|
||||||
DINER = 1
|
DINER = 1
|
||||||
LUNCH = 2
|
LUNCH = 2
|
||||||
|
|
@ -145,15 +146,7 @@ class GridItems(ProjectBaseMixin):
|
||||||
return f'inquiry: {self.inquiry.id}, grid id: {self.id}'
|
return f'inquiry: {self.inquiry.id}, grid id: {self.id}'
|
||||||
|
|
||||||
|
|
||||||
class InquiriesGalleryQuerySet(models.QuerySet):
|
class InquiriesGallery(IntermediateGalleryModelMixin):
|
||||||
"""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):
|
|
||||||
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
old_id = models.PositiveIntegerField(_('old id'), blank=True, null=True, default=None)
|
||||||
inquiry = models.ForeignKey(
|
inquiry = models.ForeignKey(
|
||||||
Inquiries,
|
Inquiries,
|
||||||
|
|
@ -167,11 +160,8 @@ class InquiriesGallery(models.Model):
|
||||||
null=True,
|
null=True,
|
||||||
related_name='inquiries_gallery',
|
related_name='inquiries_gallery',
|
||||||
on_delete=models.CASCADE,
|
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:
|
class Meta:
|
||||||
verbose_name = _('inquiry gallery')
|
verbose_name = _('inquiry gallery')
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,12 @@ class Command(BaseCommand):
|
||||||
'update_country_flag',
|
'update_country_flag',
|
||||||
'comment',
|
'comment',
|
||||||
'inquiries', # №6 - перенос запросов оценок
|
'inquiries', # №6 - перенос запросов оценок
|
||||||
'wine_characteristics',
|
'wine_characteristics', # №5 - перенос характиристик вин
|
||||||
'product',
|
'product', # №5 - перенос продуктов
|
||||||
'product_note',
|
'product_note', # №6 - перенос заметок продуктов
|
||||||
'souvenir',
|
'souvenir', # №5 - перенос продуктов типа - сувениры
|
||||||
'establishment_note',
|
'establishment_note', # №5 - перенос заметок заведений
|
||||||
'assemblage',
|
'assemblage', # №6 - перенос тегов для типа продуктов - вино
|
||||||
'rating_count',
|
'rating_count',
|
||||||
'product_review',
|
'product_review',
|
||||||
'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1
|
'newsletter_subscriber', # подписчики на рассылку - переносить после переноса пользователей №1
|
||||||
|
|
|
||||||
|
|
@ -345,4 +345,77 @@ class GMTokenGenerator(PasswordResetTokenGenerator):
|
||||||
return self.get_fields(user, timestamp)
|
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()
|
timezone.datetime.now().date().isoformat()
|
||||||
|
|
@ -5,6 +5,7 @@ from rest_framework import serializers
|
||||||
from utils import models
|
from utils import models
|
||||||
from translation.models import Language
|
from translation.models import Language
|
||||||
from favorites.models import Favorites
|
from favorites.models import Favorites
|
||||||
|
from gallery.models import Image
|
||||||
|
|
||||||
|
|
||||||
class EmptySerializer(serializers.Serializer):
|
class EmptySerializer(serializers.Serializer):
|
||||||
|
|
@ -104,3 +105,13 @@ class RecursiveFieldSerializer(serializers.Serializer):
|
||||||
def to_representation(self, value):
|
def to_representation(self, value):
|
||||||
serializer = self.parent.parent.__class__(value, context=self.context)
|
serializer = self.parent.parent.__class__(value, context=self.context)
|
||||||
return serializer.data
|
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)
|
||||||
|
|
|
||||||
|
|
@ -380,6 +380,14 @@ SORL_THUMBNAIL_ALIASES = {
|
||||||
'news_editor_mobile': {'geometry_string': '343x260', 'crop': 'center'}, # при загрузке через контент эдитор
|
'news_editor_mobile': {'geometry_string': '343x260', 'crop': 'center'}, # при загрузке через контент эдитор
|
||||||
'avatar_comments_web': {'geometry_string': '116x116', 'crop': 'center'}, # через контент эдитор в мобильном браузерe
|
'avatar_comments_web': {'geometry_string': '116x116', 'crop': 'center'}, # через контент эдитор в мобильном браузерe
|
||||||
'product_preview': {'geometry_string': '300x260', 'crop': 'center'},
|
'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'},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user