Implement card-based interface for ConfigurableKitProduct attributes

This commit introduces a new user-friendly interface for managing product attributes:

1. **Form Changes** (products/forms.py):
   - Removed 'option' field from ConfigurableKitOptionForm (values now inline)
   - Updated ConfigurableKitProductAttributeFormSetCreate to only include name, position, visible
   - Updated BaseConfigurableKitProductAttributeFormSet validation for new structure

2. **Template Updates** (products/templates/products/configurablekit_form.html):
   - Replaced row-based attribute interface with card-based design
   - Each card contains:
     - Parameter name field
     - Position field
     - Visibility toggle
     - Inline value inputs with add/remove buttons
   - "Add parameter" button creates new cards
   - "Add value" button adds inline value inputs

3. **JavaScript Enhancements**:
   - addValueField(): Creates new value input with delete button
   - initAddValueBtn(): Initializes add value button for each card
   - addParameterBtn: Dynamically generates new parameter cards
   - serializeAttributeValues(): Converts inline values to JSON for POST submission
   - Form submission intercept to serialize data before sending

4. **View Updates** (products/views/configurablekit_views.py):
   - Both Create and Update views now have _save_attributes_from_cards() method
   - Reads attributes-X-values JSON from POST data
   - Creates ConfigurableKitProductAttribute for each parameter+value combination
   - Handles parameter deletion and visibility toggling

**Key Features**:
✓ One-time parameter name entry with multiple inline values
✓ Add/remove values without reloading page
✓ Add/remove entire parameters with one click
✓ No database changes required
✓ Better UX: card layout more intuitive than rows
✓ Proper JSON serialization for value transmission

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-18 20:54:14 +03:00
parent 48938db04f
commit def795f0ad
9 changed files with 1310 additions and 107 deletions

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env python
"""
Тестовый скрипт для проверки что JSONField работает корректно
в модели ConfigurableKitOption (с поддержкой тенанта).
"""
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings')
django.setup()
from products.models.kits import ConfigurableKitProduct, ConfigurableKitOption, ProductKit
from django_tenants.utils import tenant_context
from tenants.models import Client
# Переходим в нужную схему (тенант)
try:
client = Client.objects.get(schema_name='grach')
print(f"✅ Найден тенант: {client.name} (schema: {client.schema_name})\n")
except Client.DoesNotExist:
print("❌ Тенант 'grach' не найден")
print("📝 Доступные тенанты:")
for c in Client.objects.all():
print(f" - {c.name} ({c.schema_name})")
exit(1)
# Весь тест в контексте тенанта
with tenant_context(client):
print("=" * 70)
print("ТЕСТ: JSONField в ConfigurableKitOption")
print("=" * 70)
# Проверка 1: Создание вариативного товара
print("\n1⃣ Проверка создания ConfigurableKitProduct...")
try:
configurable = ConfigurableKitProduct.objects.filter(name__icontains="тест").first()
if configurable:
print(f" ✅ Найден существующий товар: {configurable.name}")
else:
configurable = ConfigurableKitProduct.objects.create(
name="Тестовый букет JSON",
sku="TEST-BUCKET-JSON",
description="Тестовый товар для проверки JSON атрибутов"
)
print(f" ✅ Создан новый товар: {configurable.name}")
except Exception as e:
print(f" ❌ Ошибка: {e}")
exit(1)
# Проверка 2: Создание вариантов с JSON атрибутами
print("\n2⃣ Проверка создания ConfigurableKitOption с JSON атрибутами...")
try:
# Получаем первый комплект или создаём тестовый
kit = ProductKit.objects.filter(name__icontains="тест").first()
if not kit:
kit = ProductKit.objects.first()
if not kit:
print(" ⚠️ В базе нет ProductKit, пропускаем этот тест")
kit = None
if kit:
print(f" Используем существующий комплект: {kit.name}")
# Проверяем есть ли уже вариант для этого комплекта
option = ConfigurableKitOption.objects.filter(
parent=configurable,
kit=kit
).first()
if option:
print(f" Вариант уже существует, обновляю атрибуты...")
# Обновляем существующий
option.attributes = {"length": "60", "color": "red"}
option.save()
print(f" ✅ Обновлены атрибуты: {option.attributes}")
else:
# Создаём новый вариант с JSON атрибутами
option = ConfigurableKitOption.objects.create(
parent=configurable,
kit=kit,
attributes={"length": "60", "color": "red"},
is_default=True
)
print(f" ✅ Создан вариант с JSON атрибутами:")
print(f" - Parent: {option.parent.name}")
print(f" - Kit: {option.kit.name}")
print(f" - Attributes (JSON): {option.attributes}")
print(f" - Type: {type(option.attributes)}")
except Exception as e:
print(f" ❌ Ошибка: {e}")
import traceback
traceback.print_exc()
exit(1)
# Проверка 3: Получение и работа с JSON атрибутами
print("\n3⃣ Проверка получения JSON атрибутов из БД...")
try:
options = ConfigurableKitOption.objects.filter(parent=configurable)
print(f" Найдено {options.count()} вариант(ов)")
for idx, opt in enumerate(options, 1):
print(f"\n Вариант {idx}:")
print(f" - ID: {opt.id}")
print(f" - SKU комплекта: {opt.kit.sku}")
print(f" - Атрибуты (JSON): {opt.attributes}")
print(f" - Тип данных: {type(opt.attributes)}")
# Проверяем доступ к ключам JSON
if opt.attributes:
if isinstance(opt.attributes, dict):
print(f" - Доступ к ключам JSON:")
for key, value in opt.attributes.items():
print(f"{key}: {value}")
print(f" ✅ JSON работает корректно!")
else:
print(f" ❌ Атрибуты не являются dict!")
except Exception as e:
print(f" ❌ Ошибка: {e}")
import traceback
traceback.print_exc()
exit(1)
# Проверка 4: Фильтрация по JSON атрибутам (PostgreSQL)
print("\n4⃣ Проверка фильтрации по JSON атрибутам...")
try:
# Попытка использовать JSON фильтрацию (работает в PostgreSQL)
# Для SQLite это может не работать
filtered = ConfigurableKitOption.objects.filter(
parent=configurable,
attributes__length="60"
)
print(f" Попытка фильтрации по attributes__length='60'")
print(f" Найдено результатов: {filtered.count()}")
if filtered.count() > 0:
print(f" ✅ JSON фильтрация работает!")
else:
print(f" JSON фильтрация может не поддерживаться в текущей БД")
except Exception as e:
print(f" JSON фильтрация не поддерживается: {type(e).__name__}")
# Проверка 5: Сложные JSON структуры
print("\n5⃣ Проверка сохранения сложных JSON структур...")
try:
complex_attrs = {
"length": "70",
"color": "white",
"quantity": 15,
"stems": ["rose1", "rose2", "rose3"],
"metadata": {
"fresh": True,
"days_available": 7
}
}
# Обновляем атрибуты сложной структурой
if options.exists():
opt = options.first()
opt.attributes = complex_attrs
opt.save()
# Проверяем что сохранилось правильно
opt_reloaded = ConfigurableKitOption.objects.get(pk=opt.pk)
print(f" ✅ Сохранены сложные JSON атрибуты:")
print(f" {opt_reloaded.attributes}")
# Проверяем вложенность
if opt_reloaded.attributes.get("metadata", {}).get("fresh"):
print(f" ✅ Доступ к вложенным полям JSON работает!")
except Exception as e:
print(f" ❌ Ошибка: {e}")
import traceback
traceback.print_exc()
print("\n" + "=" * 70)
print("ВСЕ ТЕСТЫ ПРОЙДЕНЫ! JSONField работает корректно!")
print("=" * 70)