From 51c4ef96235d1a9787f77c4946c986565d567ade Mon Sep 17 00:00:00 2001 From: mtyton Date: Sun, 2 Jul 2023 15:51:28 +0200 Subject: [PATCH 01/15] prepared some basic model modification for configurator implementation --- ...ename_productimage_producttemplateimage.py | 16 +++ ...o_producttemplateimage_is_main_and_more.py | 47 ++++++++ ...ryparam_order_productcategoryparamvalue.py | 32 ++++++ .../migrations/0010_auto_20230630_1611.py | 29 +++++ ...aram_delete_templateparamvalue_and_more.py | 42 +++++++ artel/store/models.py | 104 ++++++++++++++---- artel/store/static/js/product_configurator.js | 0 .../templates/store/configure_product.html | 51 +++++++++ .../store/partials/product_card.html | 19 +--- artel/store/tests/factories.py | 17 ++- artel/store/tests/test_models.py | 58 +++++++++- artel/store/tests/test_validators.py | 42 +++++++ artel/store/urls.py | 1 + artel/store/validators.py | 20 ++++ artel/store/views.py | 18 ++- 15 files changed, 455 insertions(+), 41 deletions(-) create mode 100644 artel/store/migrations/0007_rename_productimage_producttemplateimage.py create mode 100644 artel/store/migrations/0008_remove_product_info_producttemplateimage_is_main_and_more.py create mode 100644 artel/store/migrations/0009_productcategoryparam_order_productcategoryparamvalue.py create mode 100644 artel/store/migrations/0010_auto_20230630_1611.py create mode 100644 artel/store/migrations/0011_productparam_delete_templateparamvalue_and_more.py create mode 100644 artel/store/static/js/product_configurator.js create mode 100644 artel/store/templates/store/configure_product.html create mode 100644 artel/store/tests/test_validators.py create mode 100644 artel/store/validators.py diff --git a/artel/store/migrations/0007_rename_productimage_producttemplateimage.py b/artel/store/migrations/0007_rename_productimage_producttemplateimage.py new file mode 100644 index 0000000..e6bc809 --- /dev/null +++ b/artel/store/migrations/0007_rename_productimage_producttemplateimage.py @@ -0,0 +1,16 @@ +# Generated by Django 4.1.9 on 2023-06-25 08:05 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("store", "0006_remove_orderdocument_sent"), + ] + + operations = [ + migrations.RenameModel( + old_name="ProductImage", + new_name="ProductTemplateImage", + ), + ] diff --git a/artel/store/migrations/0008_remove_product_info_producttemplateimage_is_main_and_more.py b/artel/store/migrations/0008_remove_product_info_producttemplateimage_is_main_and_more.py new file mode 100644 index 0000000..fdc2854 --- /dev/null +++ b/artel/store/migrations/0008_remove_product_info_producttemplateimage_is_main_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 4.1.9 on 2023-06-25 19:24 + +from django.db import migrations, models +import django.db.models.deletion +import modelcluster.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("store", "0007_rename_productimage_producttemplateimage"), + ] + + operations = [ + migrations.RemoveField( + model_name="product", + name="info", + ), + migrations.AddField( + model_name="producttemplateimage", + name="is_main", + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name="producttemplateimage", + name="template", + field=modelcluster.fields.ParentalKey( + on_delete=django.db.models.deletion.CASCADE, related_name="template_images", to="store.producttemplate" + ), + ), + migrations.CreateModel( + name="ProductImage", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("image", models.ImageField(upload_to="")), + ("is_main", models.BooleanField(default=False)), + ( + "product", + modelcluster.fields.ParentalKey( + on_delete=django.db.models.deletion.CASCADE, related_name="product_images", to="store.product" + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/artel/store/migrations/0009_productcategoryparam_order_productcategoryparamvalue.py b/artel/store/migrations/0009_productcategoryparam_order_productcategoryparamvalue.py new file mode 100644 index 0000000..eb86508 --- /dev/null +++ b/artel/store/migrations/0009_productcategoryparam_order_productcategoryparamvalue.py @@ -0,0 +1,32 @@ +# Generated by Django 4.1.9 on 2023-06-30 16:11 + +from django.db import migrations, models +import django.db.models.deletion +import modelcluster.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("store", "0008_remove_product_info_producttemplateimage_is_main_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="ProductCategoryParamValue", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("value", models.CharField(max_length=255)), + ( + "param", + modelcluster.fields.ParentalKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="param_values", + to="store.productcategoryparam", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/artel/store/migrations/0010_auto_20230630_1611.py b/artel/store/migrations/0010_auto_20230630_1611.py new file mode 100644 index 0000000..c4938f9 --- /dev/null +++ b/artel/store/migrations/0010_auto_20230630_1611.py @@ -0,0 +1,29 @@ +# Generated by Django 4.1.9 on 2023-06-30 16:11 + +from django.db import migrations + + +def copy_old_data(apps, schema_editor): + TemplateParamValue = apps.get_model("store", "TemplateParamValue") + ProductCategoryParamValue = apps.get_model("store", "ProductCategoryParamValue") + + for param_value in TemplateParamValue.objects.all(): + ProductCategoryParamValue.objects.create( + param=param_value.param, + value=param_value.value + ) + + +def remove_new_data(apps, schema_editor): + ProductCategoryParamValue = apps.get_model("store", "ProductCategoryParamValue") + ProductCategoryParamValue.objects.all().delete() + + +class Migration(migrations.Migration): + dependencies = [ + ("store", "0009_productcategoryparam_order_productcategoryparamvalue"), + ] + + operations = [ + migrations.RunPython(copy_old_data, remove_new_data), + ] diff --git a/artel/store/migrations/0011_productparam_delete_templateparamvalue_and_more.py b/artel/store/migrations/0011_productparam_delete_templateparamvalue_and_more.py new file mode 100644 index 0000000..1bceea9 --- /dev/null +++ b/artel/store/migrations/0011_productparam_delete_templateparamvalue_and_more.py @@ -0,0 +1,42 @@ +# Generated by Django 4.1.9 on 2023-07-02 09:34 + +from django.db import migrations, models +import django.db.models.deletion +import modelcluster.fields + + +class Migration(migrations.Migration): + dependencies = [ + ("store", "0010_auto_20230630_1611"), + ] + + operations = [ + migrations.CreateModel( + name="ProductParam", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ( + "param_value", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="store.productcategoryparamvalue" + ), + ), + ( + "product", + modelcluster.fields.ParentalKey( + on_delete=django.db.models.deletion.CASCADE, related_name="product_params", to="store.product" + ), + ), + ], + ), + migrations.DeleteModel( + name="TemplateParamValue", + ), + migrations.AddField( + model_name="product", + name="params", + field=models.ManyToManyField( + blank=True, through="store.ProductParam", to="store.productcategoryparamvalue" + ), + ), + ] diff --git a/artel/store/models.py b/artel/store/models.py index 1f29204..948ca51 100644 --- a/artel/store/models.py +++ b/artel/store/models.py @@ -1,5 +1,6 @@ import pdfkit import datetime +import builtins from decimal import Decimal from typing import Any @@ -14,6 +15,7 @@ from django.template import ( Template, Context ) +from django.core.exceptions import ValidationError from modelcluster.models import ClusterableModel from modelcluster.fields import ParentalKey @@ -27,14 +29,19 @@ from taggit.managers import TaggableManager from phonenumber_field.modelfields import PhoneNumberField from num2words import num2words -from store.utils import ( - notify_user_about_order, - notify_manufacturer_about_order -) from mailings.models import ( OutgoingEmail, Attachment ) +from store.validators import ProductParamDuplicateValidator + + +class BaseImageModel(models.Model): + image = models.ImageField() + is_main = models.BooleanField(default=False) + + class Meta: + abstract = True class PersonalData(models.Model): @@ -92,6 +99,28 @@ class ProductCategoryParam(ClusterableModel): def __str__(self): return self.key + + panels = [ + FieldPanel("category"), + FieldPanel("key"), + FieldPanel("param_type"), + InlinePanel("param_values") + ] + + +class ProductCategoryParamValue(ClusterableModel): + param = ParentalKey(ProductCategoryParam, on_delete=models.CASCADE, related_name="param_values") + value = models.CharField(max_length=255) + + def get_value(self): + try: + func = getattr(builtins, self.param.param_type) + return func(self.value) + except ValueError: + return + + def __str__(self): + return f"{self.param.key}: {self.value}" class ProductTemplate(ClusterableModel): @@ -106,45 +135,58 @@ class ProductTemplate(ClusterableModel): def __str__(self): return self.title + @property + def main_image(self): + try: + return self.template_images.get(is_main=True) + except ProductImage.DoesNotExist: + return self.template_images.first() + panels = [ FieldPanel("category"), FieldPanel("author"), FieldPanel('title'), FieldPanel('code'), FieldPanel('description'), - InlinePanel("images"), + InlinePanel("template_images", label="Template Images"), FieldPanel("tags"), ] -class ProductImage(models.Model): +class ProductTemplateImage(BaseImageModel): template = ParentalKey( - ProductTemplate, on_delete=models.CASCADE, related_name="images" + ProductTemplate, on_delete=models.CASCADE, related_name="template_images" ) image = models.ImageField() + is_main = models.BooleanField(default=False) class Product(ClusterableModel): name = models.CharField(max_length=255, blank=True) - info = models.TextField(blank=True) template = models.ForeignKey(ProductTemplate, on_delete=models.CASCADE, related_name="products") + params = models.ManyToManyField( + ProductCategoryParamValue, blank=True, through="ProductParam", + validators=(ProductParamDuplicateValidator(),) + ) price = models.FloatField() available = models.BooleanField(default=True) - + panels = [ FieldPanel("template"), FieldPanel("price"), - InlinePanel("param_values"), + FieldPanel("params"), FieldPanel("available"), FieldPanel("name"), - FieldPanel("info") + InlinePanel("product_images", label="Variant Images"), ] @property def main_image(self): - images = self.template.images.all() - if images: - return images.first().image + try: + return self.product_images.get(is_main=True) + except ProductImage.DoesNotExist: + return self.product_images.first() + @property def tags(self): @@ -163,10 +205,30 @@ class Product(ClusterableModel): return self.name or self.template.title -class TemplateParamValue(models.Model): - param = models.ForeignKey(ProductCategoryParam, on_delete=models.CASCADE) - product = ParentalKey(Product, on_delete=models.CASCADE, related_name="param_values") - value = models.CharField(max_length=255) +class ProductImage(BaseImageModel): + product = ParentalKey( + "Product", on_delete=models.CASCADE, related_name="product_images" + ) + + +class ProductParam(models.Model): + product = ParentalKey(Product, on_delete=models.CASCADE, related_name="product_params") + param_value = models.ForeignKey(ProductCategoryParamValue, on_delete=models.CASCADE) + + def save(self, *args, **kwargs): + self.full_clean() + return super().save(*args, **kwargs) + + def clean(self) -> None: + print("SDADASDASDASD") + # get all of this product params with same category + product_params_count = self.product.product_params.filter( + param_value__param=self.param_value.param + ).count() + if product_params_count > 1: + raise ValidationError("Product can't have two values for one param") + + return super().clean() class ProductListPage(Page): @@ -177,9 +239,9 @@ class ProductListPage(Page): def _get_items(self): if self.tags.all(): - return Product.objects.filter(available=True, template__tags__in=self.tags.all()) - return Product.objects.filter(available=True) - + return ProductTemplate.objects.filter(tags__in=self.tags.all()) + return ProductTemplate.objects.all() + def get_context(self, request): context = super().get_context(request) items = self._get_items() diff --git a/artel/store/static/js/product_configurator.js b/artel/store/static/js/product_configurator.js new file mode 100644 index 0000000..e69de29 diff --git a/artel/store/templates/store/configure_product.html b/artel/store/templates/store/configure_product.html new file mode 100644 index 0000000..b46e34d --- /dev/null +++ b/artel/store/templates/store/configure_product.html @@ -0,0 +1,51 @@ +{% extends 'base.html' %} + +{% block content %} + +
+ +
+

{{template.title}}

+
+ + + + +
+
+
+
+ Responsive image +
+
+

{{template.description}}

+
+
+ +
+ +

Dostępne konfiguracje:

+ +
+
+ {% for product in template.products.all %} +
+
+
+ {{product.name}} +
+
+ Responsive image +
+ +
+
+ {% if forloop.counter|divisibleby:3 %}
{% endif %} + {% endfor %} +
+
+
+ +{% endblock %} diff --git a/artel/store/templates/store/partials/product_card.html b/artel/store/templates/store/partials/product_card.html index a67c7e9..16623ab 100644 --- a/artel/store/templates/store/partials/product_card.html +++ b/artel/store/templates/store/partials/product_card.html @@ -3,21 +3,12 @@
{{item.title}}
- {{item.title}} + {{item.title}} -