From 8a49a5ee72ede67333036a756b54966f2f53a14c Mon Sep 17 00:00:00 2001 From: Anatoly Date: Sun, 10 Nov 2019 16:08:27 +0300 Subject: [PATCH] added gallery to model product --- apps/establishment/transfer_data.py | 1 - apps/product/models.py | 36 +++++++++- apps/product/serializers/__init__.py | 1 + apps/product/serializers/back.py | 44 +++++++++++++ apps/product/serializers/common.py | 77 +++++++++++++++++++++- apps/product/transfer_data.py | 18 ++++- apps/product/urls/back.py | 13 ++++ apps/product/urls/common.py | 1 + apps/product/views/back.py | 75 +++++++++++++++++++++ apps/product/views/common.py | 8 ++- apps/transfer/models.py | 4 ++ apps/transfer/serializers/establishment.py | 36 ++++++++-- apps/transfer/serializers/product.py | 43 +++++++++++- project/urls/back.py | 1 + 14 files changed, 343 insertions(+), 15 deletions(-) create mode 100644 apps/product/serializers/back.py diff --git a/apps/establishment/transfer_data.py b/apps/establishment/transfer_data.py index b178b18a..ced64902 100644 --- a/apps/establishment/transfer_data.py +++ b/apps/establishment/transfer_data.py @@ -17,7 +17,6 @@ def transfer_establishment(): old_establishments = Establishments.objects.exclude( id__in=list(Establishment.objects.all().values_list('old_id', flat=True)) ).exclude( - Q(type='Wineyard') | Q(location__timezone__isnull=True), ).prefetch_related( 'establishmentinfos_set', diff --git a/apps/product/models.py b/apps/product/models.py index d4426cc0..00d49eba 100644 --- a/apps/product/models.py +++ b/apps/product/models.py @@ -194,6 +194,7 @@ class Product(TranslatedFieldsMixin, BaseAttributes): null=True, blank=True, default=None, validators=[MinValueValidator(EARLIEST_VINTAGE_YEAR), MaxValueValidator(LATEST_VINTAGE_YEAR)]) + gallery = models.ManyToManyField('gallery.Image', through='ProductGallery') objects = ProductManager.from_queryset(ProductQuerySet)() @@ -213,9 +214,9 @@ class Product(TranslatedFieldsMixin, BaseAttributes): 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.')) + # 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.')) class OnlineProductManager(ProductManager): @@ -288,6 +289,35 @@ class ProductStandard(models.Model): 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): """Product classification type.""" diff --git a/apps/product/serializers/__init__.py b/apps/product/serializers/__init__.py index c564831e..7390ae2b 100644 --- a/apps/product/serializers/__init__.py +++ b/apps/product/serializers/__init__.py @@ -1,3 +1,4 @@ from .common import * from .web import * from .mobile import * +from .back import * diff --git a/apps/product/serializers/back.py b/apps/product/serializers/back.py new file mode 100644 index 00000000..3ffe5579 --- /dev/null +++ b/apps/product/serializers/back.py @@ -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 diff --git a/apps/product/serializers/common.py b/apps/product/serializers/common.py index 66fc132e..d6b22157 100644 --- a/apps/product/serializers/common.py +++ b/apps/product/serializers/common.py @@ -5,6 +5,7 @@ from product.models import Product, ProductSubType, ProductType from utils import exceptions as utils_exceptions from django.utils.translation import gettext_lazy as _ from location.serializers import WineRegionBaseSerializer, CountrySimpleSerializer +from gallery.models import Image class ProductSubTypeBaseSerializer(serializers.ModelSerializer): @@ -59,7 +60,7 @@ class ProductBaseSerializer(serializers.ModelSerializer): 'subtypes', 'public_mark', 'wine_region', - 'wine_appellation', + 'standards', 'available_countries', ] @@ -91,4 +92,76 @@ class ProductFavoritesCreateSerializer(FavoritesCreateSerializer): 'user': self.user, 'content_object': validated_data.pop('product') }) - return super().create(validated_data) \ No newline at end of file + 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} + } diff --git a/apps/product/transfer_data.py b/apps/product/transfer_data.py index c5e88dc0..e092dae8 100644 --- a/apps/product/transfer_data.py +++ b/apps/product/transfer_data.py @@ -152,7 +152,7 @@ def transfer_product(): pprint(f"transfer_product errors: {errors}") -def transfer_plates(): +def transfer_plate(): queryset = transfer_models.Merchandise.objects.all() serialized_data = product_serializers.PlateSerializer( data=list(queryset.values()), @@ -165,6 +165,19 @@ def transfer_plates(): 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 = { "partner": [transfer_partner], "product_type": [ @@ -181,6 +194,7 @@ data_types = { ], "product": [ transfer_product, - transfer_plates, + transfer_plate, + transfer_plate_image, ], } diff --git a/apps/product/urls/back.py b/apps/product/urls/back.py index e69de29b..3330cdd5 100644 --- a/apps/product/urls/back.py +++ b/apps/product/urls/back.py @@ -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('/gallery/', views.ProductBackOfficeGalleryListView.as_view(), + name='gallery-list'), + path('/gallery//', views.ProductBackOfficeGalleryCreateDestroyView.as_view(), + name='gallery-create-destroy'), +] + +urlpatterns.extend(common_urlpatterns) diff --git a/apps/product/urls/common.py b/apps/product/urls/common.py index d0dbb8a9..ea75bf7d 100644 --- a/apps/product/urls/common.py +++ b/apps/product/urls/common.py @@ -7,6 +7,7 @@ app_name = 'product' urlpatterns = [ path('', views.ProductListView.as_view(), name='list'), + path('slug/', views.ProductDetailView.as_view(), name='detail'), path('slug//favorites/', views.CreateFavoriteProductView.as_view(), name='create-destroy-favorites') ] diff --git a/apps/product/views/back.py b/apps/product/views/back.py index e69de29b..42c1ea71 100644 --- a/apps/product/views/back.py +++ b/apps/product/views/back.py @@ -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() diff --git a/apps/product/views/common.py b/apps/product/views/common.py index 63b9677f..c8fdcd2b 100644 --- a/apps/product/views/common.py +++ b/apps/product/views/common.py @@ -8,6 +8,7 @@ from product import filters class ProductBaseView(generics.GenericAPIView): """Product base view""" + permission_classes = (permissions.AllowAny, ) def get_queryset(self): """Override get_queryset method.""" @@ -16,11 +17,16 @@ class ProductBaseView(generics.GenericAPIView): class ProductListView(ProductBaseView, generics.ListAPIView): """List view for model Product.""" - permission_classes = (permissions.AllowAny, ) serializer_class = serializers.ProductBaseSerializer 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, generics.DestroyAPIView): """View for create/destroy product in favorites.""" diff --git a/apps/transfer/models.py b/apps/transfer/models.py index 905b859d..711863a2 100644 --- a/apps/transfer/models.py +++ b/apps/transfer/models.py @@ -1041,3 +1041,7 @@ class Merchandise(MigrateMixin): highlighted = models.CharField(max_length=255) site = models.ForeignKey('Sites', models.DO_NOTHING) attachment_suffix_url = models.CharField(max_length=255) + + class Meta: + managed = False + db_table = 'merchandises' diff --git a/apps/transfer/serializers/establishment.py b/apps/transfer/serializers/establishment.py index 78db0995..a228a577 100644 --- a/apps/transfer/serializers/establishment.py +++ b/apps/transfer/serializers/establishment.py @@ -1,13 +1,16 @@ +from django.conf import settings from django.core.exceptions import MultipleObjectsReturned, ValidationError from django.db import transaction 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 timetable.models import Timetable from utils.legacy_parser import parse_legacy_schedule_content from utils.serializers import TimeZoneChoiceField from utils.slug_generator import generate_unique_slug +from django.utils.text import slugify class EstablishmentSerializer(serializers.ModelSerializer): @@ -53,14 +56,16 @@ class EstablishmentSerializer(serializers.ModelSerializer): ) def validate(self, data): + old_type = data.pop('type', None) + data.update({ 'slug': generate_unique_slug(Establishment, data['slug'] if data['slug'] else data['name']), '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', + 'subtype': self.get_subtype(old_type), }) data.pop('location') - data.pop('type') data.pop('state') return data @@ -69,6 +74,7 @@ class EstablishmentSerializer(serializers.ModelSerializer): email = validated_data.pop('email') phone = validated_data.pop('phone') schedules = validated_data.pop('schedules') + subtypes = [validated_data.pop('subtype', None)] establishment = Establishment.objects.create(**validated_data) if email: @@ -86,6 +92,8 @@ class EstablishmentSerializer(serializers.ModelSerializer): for schedule in new_schedules: establishment.schedule.add(schedule) establishment.save() + if subtypes: + establishment.establishment_subtypes.add(*[i for i in subtypes if i]) return establishment @@ -97,12 +105,20 @@ class EstablishmentSerializer(serializers.ModelSerializer): return None @staticmethod - def get_type(data): + def get_type(old_type): types = { 'Restaurant': EstablishmentType.RESTAURANT, '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 @staticmethod @@ -152,3 +168,13 @@ class EstablishmentSerializer(serializers.ModelSerializer): result.append(obj) 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 diff --git a/apps/transfer/serializers/product.py b/apps/transfer/serializers/product.py index 8ef02ecd..c7c24318 100644 --- a/apps/transfer/serializers/product.py +++ b/apps/transfer/serializers/product.py @@ -10,6 +10,7 @@ from utils.methods import get_point_from_coordinates from transfer.mixins import TransferSerializerMixin from django.conf import settings from functools import reduce +from gallery.models import Image class WineColorSerializer(TransferSerializerMixin): @@ -458,6 +459,7 @@ class PlateSerializer(TransferSerializerMixin): attrs['old_id'] = attrs.pop('id') attrs['vintage'] = self.get_vintage_year(attrs.pop('vintage')) attrs['product_type'] = product_type + attrs['state'] = self.Meta.model.PUBLISHED attrs['subtype'] = self.get_product_sub_type(product_type, self.PRODUCT_SUB_TYPE_INDEX_NAME) return attrs @@ -474,4 +476,43 @@ class PlateSerializer(TransferSerializerMixin): # adding classification obj.subtypes.add(*[i for i in subtypes if i]) - return obj \ No newline at end of file + 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() diff --git a/project/urls/back.py b/project/urls/back.py index 40b3415a..1df437ca 100644 --- a/project/urls/back.py +++ b/project/urls/back.py @@ -11,5 +11,6 @@ urlpatterns = [ path('news/', include('news.urls.back')), path('review/', include('review.urls.back')), path('tags/', include(('tag.urls.back', 'tag'), namespace='tag')), + path('products/', include(('product.urls.back', 'product'), namespace='product')), ]