added gallery to model product

This commit is contained in:
Anatoly 2019-11-10 16:08:27 +03:00
parent efb468f00d
commit 8a49a5ee72
14 changed files with 343 additions and 15 deletions

View File

@ -17,7 +17,6 @@ def transfer_establishment():
old_establishments = Establishments.objects.exclude( old_establishments = Establishments.objects.exclude(
id__in=list(Establishment.objects.all().values_list('old_id', flat=True)) id__in=list(Establishment.objects.all().values_list('old_id', flat=True))
).exclude( ).exclude(
Q(type='Wineyard') |
Q(location__timezone__isnull=True), Q(location__timezone__isnull=True),
).prefetch_related( ).prefetch_related(
'establishmentinfos_set', 'establishmentinfos_set',

View File

@ -194,6 +194,7 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
null=True, blank=True, default=None, null=True, blank=True, default=None,
validators=[MinValueValidator(EARLIEST_VINTAGE_YEAR), validators=[MinValueValidator(EARLIEST_VINTAGE_YEAR),
MaxValueValidator(LATEST_VINTAGE_YEAR)]) MaxValueValidator(LATEST_VINTAGE_YEAR)])
gallery = models.ManyToManyField('gallery.Image', through='ProductGallery')
objects = ProductManager.from_queryset(ProductQuerySet)() objects = ProductManager.from_queryset(ProductQuerySet)()
@ -213,9 +214,9 @@ class Product(TranslatedFieldsMixin, BaseAttributes):
raise ValidationError(_('wine_region field must be specified.')) raise ValidationError(_('wine_region field must be specified.'))
if not self.product_type.index_name == ProductType.WINE and self.wine_region: if not self.product_type.index_name == ProductType.WINE and self.wine_region:
raise ValidationError(_('wine_region field must not be specified.')) raise ValidationError(_('wine_region field must not be specified.'))
if (self.wine_region and self.wine_appellation) and \ # if (self.wine_region and self.wine_appellation) and \
self.wine_appellation not in self.wine_region.appellations.all(): # self.wine_appellation not in self.wine_region.appellations.all():
raise ValidationError(_('Wine appellation not exists in wine region.')) # raise ValidationError(_('Wine appellation not exists in wine region.'))
class OnlineProductManager(ProductManager): class OnlineProductManager(ProductManager):
@ -288,6 +289,35 @@ class ProductStandard(models.Model):
verbose_name = _('wine standard') verbose_name = _('wine standard')
class ProductGalleryQuerySet(models.QuerySet):
"""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,
related_name='product_gallery',
on_delete=models.CASCADE,
verbose_name=_('product'))
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()
class Meta:
"""ProductGallery meta class."""
verbose_name = _('product gallery')
verbose_name_plural = _('product galleries')
unique_together = (('product', 'is_main'), ('product', 'image'))
class ProductClassificationType(models.Model): class ProductClassificationType(models.Model):
"""Product classification type.""" """Product classification type."""

View File

@ -1,3 +1,4 @@
from .common import * from .common import *
from .web import * from .web import *
from .mobile import * from .mobile import *
from .back import *

View File

@ -0,0 +1,44 @@
"""Product app back-office serializers."""
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from product import models
from gallery.models import Image
class ProductBackOfficeGallerySerializer(serializers.ModelSerializer):
"""Serializer class for model ProductGallery."""
class Meta:
"""Meta class"""
model = models.ProductGallery
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."""
product_pk = self.get_request_kwargs().get('pk')
image_id = self.get_request_kwargs().get('image_id')
product_qs = models.Product.objects.filter(pk=product_pk)
image_qs = Image.objects.filter(id=image_id)
if not product_qs.exists():
raise serializers.ValidationError({'detail': _('Product not found')})
if not image_qs.exists():
raise serializers.ValidationError({'detail': _('Image not found')})
product = product_qs.first()
image = image_qs.first()
attrs['product'] = product
attrs['image'] = image
return attrs

View File

@ -5,6 +5,7 @@ from product.models import Product, ProductSubType, ProductType
from utils import exceptions as utils_exceptions from utils import exceptions as utils_exceptions
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from location.serializers import WineRegionBaseSerializer, CountrySimpleSerializer from location.serializers import WineRegionBaseSerializer, CountrySimpleSerializer
from gallery.models import Image
class ProductSubTypeBaseSerializer(serializers.ModelSerializer): class ProductSubTypeBaseSerializer(serializers.ModelSerializer):
@ -59,7 +60,7 @@ class ProductBaseSerializer(serializers.ModelSerializer):
'subtypes', 'subtypes',
'public_mark', 'public_mark',
'wine_region', 'wine_region',
'wine_appellation', 'standards',
'available_countries', 'available_countries',
] ]
@ -92,3 +93,75 @@ class ProductFavoritesCreateSerializer(FavoritesCreateSerializer):
'content_object': validated_data.pop('product') 'content_object': validated_data.pop('product')
}) })
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}
}

View File

@ -152,7 +152,7 @@ def transfer_product():
pprint(f"transfer_product errors: {errors}") pprint(f"transfer_product errors: {errors}")
def transfer_plates(): def transfer_plate():
queryset = transfer_models.Merchandise.objects.all() queryset = transfer_models.Merchandise.objects.all()
serialized_data = product_serializers.PlateSerializer( serialized_data = product_serializers.PlateSerializer(
data=list(queryset.values()), data=list(queryset.values()),
@ -165,6 +165,19 @@ def transfer_plates():
pprint(f"transfer_plates errors: {errors}") pprint(f"transfer_plates errors: {errors}")
def transfer_plate_image():
queryset = transfer_models.Merchandise.objects.all()
serialized_data = product_serializers.PlateImageSerializer(
data=list(queryset.values()),
many=True)
if serialized_data.is_valid():
serialized_data.save()
else:
errors = []
for d in serialized_data.errors: errors.append(d) if d else None
pprint(f"transfer_plates_images errors: {errors}")
data_types = { data_types = {
"partner": [transfer_partner], "partner": [transfer_partner],
"product_type": [ "product_type": [
@ -181,6 +194,7 @@ data_types = {
], ],
"product": [ "product": [
transfer_product, transfer_product,
transfer_plates, transfer_plate,
transfer_plate_image,
], ],
} }

View File

@ -0,0 +1,13 @@
"""Product backoffice url patterns."""
from django.urls import path
from product.urls.common import urlpatterns as common_urlpatterns
from product import views
urlpatterns = [
path('<int:pk>/gallery/', views.ProductBackOfficeGalleryListView.as_view(),
name='gallery-list'),
path('<int:pk>/gallery/<int:image_id>/', views.ProductBackOfficeGalleryCreateDestroyView.as_view(),
name='gallery-create-destroy'),
]
urlpatterns.extend(common_urlpatterns)

View File

@ -7,6 +7,7 @@ app_name = 'product'
urlpatterns = [ urlpatterns = [
path('', views.ProductListView.as_view(), name='list'), path('', views.ProductListView.as_view(), name='list'),
path('slug/<slug:slug>', views.ProductDetailView.as_view(), name='detail'),
path('slug/<slug:slug>/favorites/', views.CreateFavoriteProductView.as_view(), path('slug/<slug:slug>/favorites/', views.CreateFavoriteProductView.as_view(),
name='create-destroy-favorites') name='create-destroy-favorites')
] ]

View File

@ -0,0 +1,75 @@
"""Product app back-office 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, status, permissions
from rest_framework.response import Response
from gallery.tasks import delete_image
from product import serializers, models
class ProductBackOfficeMixinView:
"""Product back-office mixin view."""
permission_classes = (permissions.IsAuthenticated,)
queryset = models.Product.objects.with_base_related() \
.order_by('-created', )
class ProductBackOfficeGalleryCreateDestroyView(ProductBackOfficeMixinView,
generics.CreateAPIView,
generics.DestroyAPIView):
"""Resource for a create gallery for product for back-office users."""
serializer_class = serializers.ProductBackOfficeGallerySerializer
def get_object(self):
"""
Returns the object the view is displaying.
"""
product_qs = self.filter_queryset(self.get_queryset())
product = get_object_or_404(product_qs, pk=self.kwargs['pk'])
gallery = get_object_or_404(product.product_gallery, image_id=self.kwargs['image_id'])
# May raise a permission denied
self.check_object_permissions(self.request, gallery)
return gallery
def create(self, request, *args, **kwargs):
"""Overridden create method"""
super().create(request, *args, **kwargs)
return Response(status=status.HTTP_201_CREATED)
def destroy(self, request, *args, **kwargs):
"""Override destroy method."""
gallery_obj = self.get_object()
if settings.USE_CELERY:
on_commit(lambda: delete_image.delay(image_id=gallery_obj.image.id,
completely=False))
else:
on_commit(lambda: delete_image(image_id=gallery_obj.image.id,
completely=False))
# Delete an instances of ProductGallery model
gallery_obj.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class ProductBackOfficeGalleryListView(ProductBackOfficeMixinView, generics.ListAPIView):
"""Resource for returning gallery for product for back-office users."""
serializer_class = serializers.ProductImageSerializer
def get_object(self):
"""Override get_object method."""
qs = super(ProductBackOfficeGalleryListView, self).get_queryset()
product = get_object_or_404(qs, pk=self.kwargs['pk'])
# May raise a permission denied
self.check_object_permissions(self.request, product)
return product
def get_queryset(self):
"""Override get_queryset method."""
return self.get_object().gallery.all()

View File

@ -8,6 +8,7 @@ from product import filters
class ProductBaseView(generics.GenericAPIView): class ProductBaseView(generics.GenericAPIView):
"""Product base view""" """Product base view"""
permission_classes = (permissions.AllowAny, )
def get_queryset(self): def get_queryset(self):
"""Override get_queryset method.""" """Override get_queryset method."""
@ -16,11 +17,16 @@ class ProductBaseView(generics.GenericAPIView):
class ProductListView(ProductBaseView, generics.ListAPIView): class ProductListView(ProductBaseView, generics.ListAPIView):
"""List view for model Product.""" """List view for model Product."""
permission_classes = (permissions.AllowAny, )
serializer_class = serializers.ProductBaseSerializer serializer_class = serializers.ProductBaseSerializer
filter_class = filters.ProductFilterSet filter_class = filters.ProductFilterSet
class ProductDetailView(ProductBaseView, generics.RetrieveAPIView):
"""Detail view fro model Product."""
lookup_field = 'slug'
serializer_class = serializers.ProductBaseSerializer
class CreateFavoriteProductView(generics.CreateAPIView, class CreateFavoriteProductView(generics.CreateAPIView,
generics.DestroyAPIView): generics.DestroyAPIView):
"""View for create/destroy product in favorites.""" """View for create/destroy product in favorites."""

View File

@ -1041,3 +1041,7 @@ class Merchandise(MigrateMixin):
highlighted = models.CharField(max_length=255) highlighted = models.CharField(max_length=255)
site = models.ForeignKey('Sites', models.DO_NOTHING) site = models.ForeignKey('Sites', models.DO_NOTHING)
attachment_suffix_url = models.CharField(max_length=255) attachment_suffix_url = models.CharField(max_length=255)
class Meta:
managed = False
db_table = 'merchandises'

View File

@ -1,13 +1,16 @@
from django.conf import settings
from django.core.exceptions import MultipleObjectsReturned, ValidationError from django.core.exceptions import MultipleObjectsReturned, ValidationError
from django.db import transaction from django.db import transaction
from rest_framework import serializers from rest_framework import serializers
from establishment.models import Establishment, ContactEmail, ContactPhone, EstablishmentType from establishment.models import Establishment, ContactEmail, ContactPhone, EstablishmentType, \
EstablishmentSubType
from location.models import Address from location.models import Address
from timetable.models import Timetable from timetable.models import Timetable
from utils.legacy_parser import parse_legacy_schedule_content from utils.legacy_parser import parse_legacy_schedule_content
from utils.serializers import TimeZoneChoiceField from utils.serializers import TimeZoneChoiceField
from utils.slug_generator import generate_unique_slug from utils.slug_generator import generate_unique_slug
from django.utils.text import slugify
class EstablishmentSerializer(serializers.ModelSerializer): class EstablishmentSerializer(serializers.ModelSerializer):
@ -53,14 +56,16 @@ class EstablishmentSerializer(serializers.ModelSerializer):
) )
def validate(self, data): def validate(self, data):
old_type = data.pop('type', None)
data.update({ data.update({
'slug': generate_unique_slug(Establishment, data['slug'] if data['slug'] else data['name']), 'slug': generate_unique_slug(Establishment, data['slug'] if data['slug'] else data['name']),
'address_id': self.get_address(data['location']), 'address_id': self.get_address(data['location']),
'establishment_type_id': self.get_type(data), 'establishment_type_id': self.get_type(old_type),
'is_publish': data.get('state') == 'published', 'is_publish': data.get('state') == 'published',
'subtype': self.get_subtype(old_type),
}) })
data.pop('location') data.pop('location')
data.pop('type')
data.pop('state') data.pop('state')
return data return data
@ -69,6 +74,7 @@ class EstablishmentSerializer(serializers.ModelSerializer):
email = validated_data.pop('email') email = validated_data.pop('email')
phone = validated_data.pop('phone') phone = validated_data.pop('phone')
schedules = validated_data.pop('schedules') schedules = validated_data.pop('schedules')
subtypes = [validated_data.pop('subtype', None)]
establishment = Establishment.objects.create(**validated_data) establishment = Establishment.objects.create(**validated_data)
if email: if email:
@ -86,6 +92,8 @@ class EstablishmentSerializer(serializers.ModelSerializer):
for schedule in new_schedules: for schedule in new_schedules:
establishment.schedule.add(schedule) establishment.schedule.add(schedule)
establishment.save() establishment.save()
if subtypes:
establishment.establishment_subtypes.add(*[i for i in subtypes if i])
return establishment return establishment
@ -97,12 +105,20 @@ class EstablishmentSerializer(serializers.ModelSerializer):
return None return None
@staticmethod @staticmethod
def get_type(data): def get_type(old_type):
types = { types = {
'Restaurant': EstablishmentType.RESTAURANT, 'Restaurant': EstablishmentType.RESTAURANT,
'Shop': EstablishmentType.ARTISAN, 'Shop': EstablishmentType.ARTISAN,
'Producer': EstablishmentType.PRODUCER,
} }
obj, _ = EstablishmentType.objects.get_or_create(index_name=types[data['type']]) if old_type == 'Wineyard':
index_name = types['Producer']
elif old_type in types.keys():
index_name = types[old_type]
else:
return None
obj, _ = EstablishmentType.objects.get_or_create(index_name=index_name)
return obj.id return obj.id
@staticmethod @staticmethod
@ -152,3 +168,13 @@ class EstablishmentSerializer(serializers.ModelSerializer):
result.append(obj) result.append(obj)
return result return result
def get_subtype(self, old_type):
if old_type == 'Wineyard':
subtype_name = 'Winery'
establishment_type_id = self.get_type(old_type)
subtype, _ = EstablishmentSubType.objects.get_or_create(
name={settings.FALLBACK_LOCALE: subtype_name},
index_name=slugify(subtype_name),
establishment_type_id=establishment_type_id)
return subtype

View File

@ -10,6 +10,7 @@ from utils.methods import get_point_from_coordinates
from transfer.mixins import TransferSerializerMixin from transfer.mixins import TransferSerializerMixin
from django.conf import settings from django.conf import settings
from functools import reduce from functools import reduce
from gallery.models import Image
class WineColorSerializer(TransferSerializerMixin): class WineColorSerializer(TransferSerializerMixin):
@ -458,6 +459,7 @@ class PlateSerializer(TransferSerializerMixin):
attrs['old_id'] = attrs.pop('id') attrs['old_id'] = attrs.pop('id')
attrs['vintage'] = self.get_vintage_year(attrs.pop('vintage')) attrs['vintage'] = self.get_vintage_year(attrs.pop('vintage'))
attrs['product_type'] = product_type attrs['product_type'] = product_type
attrs['state'] = self.Meta.model.PUBLISHED
attrs['subtype'] = self.get_product_sub_type(product_type, attrs['subtype'] = self.get_product_sub_type(product_type,
self.PRODUCT_SUB_TYPE_INDEX_NAME) self.PRODUCT_SUB_TYPE_INDEX_NAME)
return attrs return attrs
@ -475,3 +477,42 @@ class PlateSerializer(TransferSerializerMixin):
# adding classification # adding classification
obj.subtypes.add(*[i for i in subtypes if i]) obj.subtypes.add(*[i for i in subtypes if i])
return obj return obj
class PlateImageSerializer(serializers.ModelSerializer):
id = serializers.IntegerField()
name = serializers.CharField()
attachment_suffix_url = serializers.CharField()
class Meta:
model = models.ProductGallery
fields = (
'id',
'attachment_suffix_url',
)
def create(self, validated_data):
image = self.get_image(validated_data.pop('attachment_suffix_url', None),
validated_data.pop('name', None))
product = self.get_product(validated_data.pop('id'))
if product and image:
obj, created = models.ProductGallery.objects.get_or_create(
product=product,
image=image,
is_main=True)
return obj
def get_image(self, image_url, name):
if image_url:
obj, created = Image.objects.get_or_create(
title=name,
image=image_url)
return obj if created else None
def get_product(self, product_id):
if product_id:
product_qs = models.Product.objects.filter(old_id=product_id)
if product_qs.exists():
return product_qs.first()

View File

@ -11,5 +11,6 @@ urlpatterns = [
path('news/', include('news.urls.back')), path('news/', include('news.urls.back')),
path('review/', include('review.urls.back')), path('review/', include('review.urls.back')),
path('tags/', include(('tag.urls.back', 'tag'), namespace='tag')), path('tags/', include(('tag.urls.back', 'tag'), namespace='tag')),
path('products/', include(('product.urls.back', 'product'), namespace='product')),
] ]