From fc2297b17cc48cf08bc4d2a6c5ac3d45be6ce9c6 Mon Sep 17 00:00:00 2001 From: phzhik Date: Fri, 7 Jul 2023 08:22:15 +0400 Subject: [PATCH] + array of payment_proof + Checklist.images is m2m now + Image.type & dynamic upload path --- ...age_checklist_checklist_images_and_more.py | 31 +++++++++++ ...ve_image_is_preview_image_type_and_more.py | 32 ++++++++++++ store/migrations/0037_alter_image_image.py | 19 +++++++ store/models.py | 51 ++++++++++++++----- store/serializers.py | 51 ++++++++++++++----- 5 files changed, 158 insertions(+), 26 deletions(-) create mode 100644 store/migrations/0035_remove_image_checklist_checklist_images_and_more.py create mode 100644 store/migrations/0036_remove_image_is_preview_image_type_and_more.py create mode 100644 store/migrations/0037_alter_image_image.py diff --git a/store/migrations/0035_remove_image_checklist_checklist_images_and_more.py b/store/migrations/0035_remove_image_checklist_checklist_images_and_more.py new file mode 100644 index 0000000..1c87fb0 --- /dev/null +++ b/store/migrations/0035_remove_image_checklist_checklist_images_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 4.2.2 on 2023-07-07 03:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('store', '0034_alter_promocode_discount'), + ] + + operations = [ + migrations.RemoveField( + model_name='image', + name='checklist', + ), + migrations.AddField( + model_name='checklist', + name='images', + field=models.ManyToManyField(related_name='+', to='store.image', verbose_name='Изображения'), + ), + migrations.RemoveField( + model_name='checklist', + name='payment_proof', + ), + migrations.AddField( + model_name='checklist', + name='payment_proof', + field=models.ManyToManyField(related_name='+', to='store.image', verbose_name='Подтверждение оплаты'), + ), + ] diff --git a/store/migrations/0036_remove_image_is_preview_image_type_and_more.py b/store/migrations/0036_remove_image_is_preview_image_type_and_more.py new file mode 100644 index 0000000..40df54a --- /dev/null +++ b/store/migrations/0036_remove_image_is_preview_image_type_and_more.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.2 on 2023-07-07 03:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('store', '0035_remove_image_checklist_checklist_images_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='image', + name='is_preview', + ), + migrations.AddField( + model_name='image', + name='type', + field=models.PositiveSmallIntegerField(choices=[(0, 'Изображение'), (1, 'Превью')], default=0, verbose_name='Тип'), + ), + migrations.AlterField( + model_name='checklist', + name='receipt', + field=models.ImageField(blank=True, null=True, upload_to='', verbose_name='Фото чека'), + ), + migrations.AlterField( + model_name='image', + name='image', + field=models.ImageField(upload_to='', verbose_name='Файл изображения'), + ), + ] diff --git a/store/migrations/0037_alter_image_image.py b/store/migrations/0037_alter_image_image.py new file mode 100644 index 0000000..d068f01 --- /dev/null +++ b/store/migrations/0037_alter_image_image.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.2 on 2023-07-07 03:23 + +from django.db import migrations, models +import store.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('store', '0036_remove_image_is_preview_image_type_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='image', + name='image', + field=models.ImageField(upload_to=store.models.image_upload_path, verbose_name='Файл изображения'), + ), + ] diff --git a/store/models.py b/store/models.py index 55991cf..d245b20 100644 --- a/store/models.py +++ b/store/models.py @@ -1,3 +1,4 @@ +import posixpath from decimal import Decimal import random import string @@ -166,18 +167,37 @@ class PaymentMethod(models.Model): return self.name +def image_upload_path(instance, filename): + dirname = Image.TYPE_TO_UPLOAD_PATH[instance.type] + return posixpath.join(dirname, filename) + + @cleanup.select class Image(models.Model): - image = models.ImageField(upload_to='checklist_images') - is_preview = models.BooleanField(default=False) - checklist = models.ForeignKey('Checklist', on_delete=models.CASCADE, related_name='images') + DEFAULT = 0 + PREVIEW = 1 + DOC = 2 + + TYPE_CHOICES = ( + (DEFAULT, 'Изображение'), + (PREVIEW, 'Превью'), + ) + + TYPE_TO_UPLOAD_PATH = { + DEFAULT: 'checklist_images/', + PREVIEW: 'checklist_images/', + DOC: 'docs/', + } + + image = models.ImageField('Файл изображения', upload_to=image_upload_path) + type = models.PositiveSmallIntegerField('Тип', choices=TYPE_CHOICES, default=DEFAULT) class Meta: verbose_name = 'Изображение' verbose_name_plural = 'Изображения' def __str__(self): - return getattr(self.image, 'name') + return f"{self.get_type_display()}: {getattr(self.image, 'name', '')}" def generate_checklist_id(): @@ -195,6 +215,7 @@ def generate_checklist_id(): class ChecklistQuerySet(models.QuerySet): def with_base_related(self): return self.select_related('manager', 'category', 'payment_method', 'promocode')\ + .prefetch_related('payment_proof') \ .prefetch_related(Prefetch('images', to_attr='_images')) def default_ordering(self): @@ -299,8 +320,10 @@ class Checklist(models.Model): payment_method = models.ForeignKey('PaymentMethod', verbose_name='Метод оплаты', null=True, blank=True, on_delete=models.SET_NULL) - payment_proof = models.ImageField('Подтверждение оплаты', upload_to='docs', null=True, blank=True) # paymentproovement - receipt = models.ImageField('Фото чека', upload_to='docs', null=True, blank=True) # checkphoto + + images = models.ManyToManyField('Image', verbose_name='Изображения', related_name='+') + payment_proof = models.ManyToManyField('Image', verbose_name='Подтверждение оплаты', related_name='+') # paymentproovement + receipt = models.ImageField('Фото чека', upload_to=Image.TYPE_TO_UPLOAD_PATH[Image.DOC], null=True, blank=True) # checkphoto delivery = models.CharField('Тип доставки', max_length=10, choices=DeliveryType.CHOICES, null=True, blank=True) # trackid @@ -330,6 +353,8 @@ class Checklist(models.Model): no_comission = False if self.promocode_id: + # We assume that working promocode was bound correctly through serializer + # and intentionally don't check if promocode is active here promocode = self.promocode price -= promocode.discount * self.price_rub @@ -362,10 +387,10 @@ class Checklist(models.Model): def preview_image(self): # Prefer annotated field if hasattr(self, '_images'): - # Get first is_preview image from all images - return next(filter(lambda x: x.is_preview, self._images), None) + # Get first preview image from all images + return next(filter(lambda x: x.type == Image.PREVIEW, self._images), None) - return self.images.filter(is_preview=True).first() + return self.images.filter(type=Image.PREVIEW).first() @property def preview_image_url(self): @@ -375,9 +400,9 @@ class Checklist(models.Model): def main_images(self) -> list: # Prefer prefetched field if hasattr(self, '_images'): - return [img for img in self._images if not img.is_preview] + return [img for img in self._images if img.type == Image.DEFAULT] - return self.images.filter(is_preview=False) + return self.images.filter(type=Image.DEFAULT) @property def title(self): @@ -399,11 +424,13 @@ class Checklist(models.Model): preview.save(image_io, format='JPEG') # Create Image model and save it - image_obj = Image(is_preview=True, checklist_id=self.id) + image_obj = Image(type=Image.PREVIEW) image_obj.image.save(name=f'{self.id}_preview.jpg', content=ContentFile(image_io.getvalue()), save=True) + self.images.add(image_obj) + def save(self, *args, **kwargs): if self.id: old_obj = Checklist.objects.filter(id=self.id).first() diff --git a/store/serializers.py b/store/serializers.py index 205e9fc..92a42b4 100644 --- a/store/serializers.py +++ b/store/serializers.py @@ -58,7 +58,8 @@ class ImageListSerializer(serializers.ListSerializer): child = ImageSerializer() def to_internal_value(self, data): - data = [{"image": x} for x in data] + if isinstance(data, list): + data = [{"image": x} for x in data] return super().to_internal_value(data) def to_representation(self, data): @@ -72,7 +73,7 @@ class ChecklistSerializer(serializers.ModelSerializer): link = serializers.URLField(source='product_link', required=False) category = serializers.SlugRelatedField(slug_field='slug', queryset=Category.objects.all()) - image = ImageListSerializer(source='main_images') + image = ImageListSerializer(source='main_images', required=False) previewimage = serializers.ImageField(source='preview_image_url', read_only=True) promo = serializers.SlugRelatedField(source='promocode', slug_field='name', @@ -100,7 +101,7 @@ class ChecklistSerializer(serializers.ModelSerializer): paymenttype = serializers.SlugRelatedField(source='payment_method', slug_field='slug', queryset=PaymentMethod.objects.all(), required=False, allow_null=True) - paymentproovement = Base64ImageField(source='payment_proof', required=False, allow_null=True) + paymentproovement = ImageListSerializer(source='payment_proof', required=False, allow_null=True) checkphoto = Base64ImageField(source='receipt', required=False, allow_null=True) trackid = serializers.CharField(source='poizon_tracking', required=False, allow_null=True) cdek_tracking = serializers.CharField(required=False, allow_null=True) @@ -109,27 +110,49 @@ class ChecklistSerializer(serializers.ModelSerializer): startDate = serializers.DateTimeField(source='created_at', read_only=True) currentDate = serializers.DateTimeField(source='status_updated_at', read_only=True) - def _create_images(self, instance, images): - img_objs = [Image(image=img['image'], checklist_id=instance.id) for img in images] - img_objs = Image.objects.bulk_create(img_objs) + def _collect_images_by_fields(self, validated_data): + images = {} + for k in ('main_images', 'payment_proof'): + if k in validated_data: + images[k] = validated_data.pop(k) + return images + + def _create_main_images(self, instance, images): + if images is None: + return + + img_objs = [Image(image=img['image'], type=Image.DEFAULT) for img in images] + img_objs = Image.objects.bulk_create(img_objs) + instance.images.set(img_objs) instance.generate_preview(next(iter(img_objs), None)) + def _create_payment_proofs(self, instance, images): + if images is None: + return + + img_objs = [Image(image=img['image'], type=Image.DOC) for img in images] + img_objs = Image.objects.bulk_create(img_objs) + instance.payment_proof.set(img_objs) + def create(self, validated_data): - images = validated_data.pop('main_images', []) + images = self._collect_images_by_fields(validated_data) + instance = super().create(validated_data) - self._create_images(instance, images) + self._create_main_images(instance, images.get('main_images')) + self._create_payment_proofs(instance, images.get('payment_proof')) return instance def update(self, instance, validated_data): - images = validated_data.pop('main_images', []) + images = self._collect_images_by_fields(validated_data) + + self._create_main_images(instance, images.get('main_images')) + self._create_payment_proofs(instance, images.get('payment_proof')) instance: Checklist = super().update(instance, validated_data) - # Replace images basically - if images: - instance.images.all().delete() - self._create_images(instance, images) - instance.refresh_from_db(fields=('images',)) + # Invalidate prefetched images + delattr(instance, '_images') + return instance @staticmethod