prepared some basic model modification for configurator implementation

feature/product_models_refactor
mtyton 2023-07-02 15:51:28 +02:00
rodzic e99a07f45d
commit 51c4ef9623
15 zmienionych plików z 455 dodań i 41 usunięć

Wyświetl plik

@ -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",
),
]

Wyświetl plik

@ -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,
},
),
]

Wyświetl plik

@ -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,
},
),
]

Wyświetl plik

@ -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),
]

Wyświetl plik

@ -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"
),
),
]

Wyświetl plik

@ -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()

Wyświetl plik

@ -0,0 +1,51 @@
{% extends 'base.html' %}
{% block content %}
<div class="card">
<div class="card-header text-center">
<h2>{{template.title}}</h2>
</div>
<div class="card-body">
<div class="container">
<div class="row">
<div class="col-6">
<img src="{{template.main_image.image.url}}" class="img-fluid img-thumbnail" alt="Responsive image">
</div>
<div class="col-6">
<p>{{template.description}}</p>
</div>
</div>
</div>
<h4 class="mt-3">Dostępne konfiguracje:</h4>
<div class="container mt-3">
<div class="row">
{% for product in template.products.all %}
<div class="col-4">
<div class="card">
<div class="card-header text-center">
{{product.name}}
</div>
<div class="card-body">
<img src="{{product.main_image.image.url}}" class="img-fluid img-thumbnail" alt="Responsive image">
</div>
<div class="card-footer text-center">
<input type="button" class="btn btn-primary btn-block" value="Wybierz">
</div>
</div>
</div>
{% if forloop.counter|divisibleby:3 %}</div><div class="row">{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endblock %}

Wyświetl plik

@ -3,21 +3,12 @@
<div class="card h-100" >
<div class="card-header text-truncate">{{item.title}}</div>
<div class="card-body p-0">
<img src="{{item.main_image.url}}" class="img-fluid rounded mx-auto d-block" style="width: 10rem; height: 15rem;" alt="{{item.title}}">
<img src="{{item.main_image.image.url}}"
class="img-fluid img-thumbnail rounded mx-auto d-block mt-2 mb-2" style="width: 13rem; height: 15rem;" alt="{{item.title}}">
<div class="card-footer row d-flex mt-3 m-0">
<div class="col">
<input type="number" id="quantity{{item.id}}" name="quantity" min="1" value="1" class="form-control form-control-sm">
</div>
<div class="col text-end">
<button class="btn btn-outline-success add-to-cart-button"
data-product-id="{{item.id}}"
data-csrf-token="{{csrf_token}}"
data-add-to-cart-url={% url "cart-action-add-product" %}
data-bs-toggle="modal" data-bs-target="#addToCartModal"
>
<img src="{% static 'images/icons/cart.svg' %}" style="width: 1rem; height: 1rem;" alt="Koszyk"/>
</button>
<div class="card-footer row d-flex m-0">
<div class="col text-center">
<a href="{% url 'product-configure' item.id %}" class="btn btn-primary">Konfiguruj</a>
</div>
</div>
</div>

Wyświetl plik

@ -40,6 +40,14 @@ class ProductCategoryParamFactory(DjangoModelFactory):
param_type = 'str'
class ProductCategoryParamValueFactory(DjangoModelFactory):
class Meta:
model = 'store.ProductCategoryParamValue'
param = SubFactory(ProductCategoryParamFactory)
value = Faker('name')
class ProductTemplateFactory(DjangoModelFactory):
class Meta:
model = 'store.ProductTemplate'
@ -56,12 +64,19 @@ class ProductFactory(DjangoModelFactory):
model = 'store.Product'
name = Faker('name')
info = Faker('text')
price = Faker('pydecimal', left_digits=5, right_digits=2, positive=True)
available = Faker('boolean')
template = SubFactory(ProductTemplateFactory)
class ProductParamFactory(DjangoModelFactory):
class Meta:
model = 'store.ProductParam'
product = SubFactory(ProductFactory)
param = SubFactory(ProductCategoryParamFactory)
class PaymentMethodFactory(DjangoModelFactory):
class Meta:
model = 'store.PaymentMethod'

Wyświetl plik

@ -3,15 +3,67 @@ from unittest.mock import patch
from django.test import TestCase
from django.urls import reverse
from django.core import mail
from django.core.exceptions import ValidationError
from store.tests import factories
from store import models as store_models
from mailings.tests.factories import MailTemplateFactory
# TODO - this is fine for now, but we'll want to use factoryboy for this:
# https://factoryboy.readthedocs.io/en/stable/
# TODO - test have to rewritten - I'll do it tommorow
class ProductCategoryParamValueTestCase(TestCase):
def setUp(self):
super().setUp()
self.category = factories.ProductCategoryFactory()
def test_get_value_success(self):
param = factories.ProductCategoryParamFactory(
category=self.category,
param_type="int",
key="test_param"
)
param_value = factories.ProductCategoryParamValueFactory(param=param, value="23")
proper_value = param_value.get_value()
self.assertEqual(proper_value, 23)
def test_get_value_failure_wrong_value(self):
param = factories.ProductCategoryParamFactory(
category=self.category,
param_type="int",
key="test_param"
)
param_value = factories.ProductCategoryParamValueFactory(param=param, value="wrong_value")
proper_value = param_value.get_value()
self.assertEqual(proper_value, None)
class ProductTestCase(TestCase):
def test_category_params_one_value_success(self):
product = factories.ProductFactory()
param = factories.ProductCategoryParamFactory(
category=product.template.category,
param_type="int",
key="test_param"
)
param_value = factories.ProductCategoryParamValueFactory(param=param, value="23")
product.params.add(param_value)
product.save()
self.assertEqual(product.params.count(), 1)
self.assertEqual(product.params.first().get_value(), 23)
def test_category_params_multiple_values_failure(self):
product = factories.ProductFactory()
param = factories.ProductCategoryParamFactory(
category=product.template.category,
param_type="int",
key="test_param"
)
param_value = factories.ProductCategoryParamValueFactory(param=param, value="23")
sec_param_value = factories.ProductCategoryParamValueFactory(param=param, value="24")
with self.assertRaises(ValidationError):
product.params.add(param_value)
product.params.add(sec_param_value)
class OrderProductTestCase(TestCase):

Wyświetl plik

@ -0,0 +1,42 @@
from django.test import TestCase
from django.core.exceptions import ValidationError
from store.tests import factories
from store.validators import ProductParamDuplicateValidator
from store.models import (
ProductParam,
Product
)
class CategoryParamValidationTestCase(TestCase):
def setUp(self):
super().setUp()
self.product = factories.ProductFactory()
def test_set_single_param_success(self):
param = factories.ProductCategoryParamFactory(
category=self.product.template.category,
param_type="int",
key="test_param"
)
param_value = factories.ProductCategoryParamValueFactory(param=param, value="23")
param = ProductParam(param_value=param_value)
self.product.params.add(param_value)
self.assertEqual(self.product.params.count(), 1)
self.assertEqual(self.product.params.first().get_value(), 23)
def test_set_multuiple_same_type_param_failure(self):
param = factories.ProductCategoryParamFactory(
category=self.product.template.category,
param_type="int",
key="test_param"
)
param_value = factories.ProductCategoryParamValueFactory(param=param, value="23")
sec_param_value = factories.ProductCategoryParamValueFactory(param=param, value="24")
param = ProductParam(param_value=param_value)
self.product.params.add(param_value)
with self.assertRaises(ValidationError):
self.product.params.add(sec_param_value)

Wyświetl plik

@ -9,6 +9,7 @@ router.register("cart-action", store_views.CartActionView, "cart-action")
urlpatterns = [
path('product-configure/<int:pk>/', store_views.ConfigureProductView.as_view(), name='product-configure'),
path('cart/', store_views.CartView.as_view(), name='cart'),
path("order/", store_views.OrderView.as_view(), name="order"),
path("order/confirm/", store_views.OrderConfirmView.as_view(), name="order-confirm")

Wyświetl plik

@ -0,0 +1,20 @@
from typing import Any
from django.core.exceptions import ValidationError
from django.core.validators import BaseValidator
class ProductParamDuplicateValidator(BaseValidator):
message = "Product param with this key already exists."
code = "duplicate"
def __init__(self, *args, **kwargs):
super().__init__(limit_value=1, *args, **kwargs)
def compare(self, param: Any, limit: Any) -> bool:
raise ValidationError("Not implemented")
print(param, limit)
count = param.product.params.filter(
param__param_value__param__key=param.param_value.param.key
).count()
return count >= limit

Wyświetl plik

@ -20,7 +20,8 @@ from store.serializers import (
from store.forms import CustomerDataForm
from store.models import (
Order,
Product
Product,
ProductTemplate
)
@ -84,7 +85,20 @@ class CartActionView(ViewSet):
items = cart.get_items()
serializer = CartSerializer(instance=items, many=True)
return Response(serializer.data, status=201)
class ConfigureProductView(View):
template_name = "store/configure_product.html"
def get_context_data(self, pk: int, **kwargs: Any) -> Dict[str, Any]:
context = {}
context["template"] = ProductTemplate.objects.get(pk=pk)
return context
def get(self, request, pk: int, *args, **kwargs):
context = self.get_context_data(pk)
return render(request, self.template_name, context)
class OrderView(View):
template_name = "store/order.html"