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

@@ -193,9 +193,9 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, CreateView):
if option_form.instance.pk:
option_form.instance.delete()
# Сохраняем атрибуты родителя
attribute_formset.instance = self.object
attribute_formset.save()
# Сохраняем атрибуты родителя - новый интерфейс
# Карточный интерфейс: значения приходят как инлайн input'ы
self._save_attributes_from_cards()
messages.success(self.request, f'Вариативный товар "{self.object.name}" успешно создан!')
return super().form_valid(form)
@@ -206,6 +206,78 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, CreateView):
traceback.print_exc()
return self.form_invalid(form)
def _save_attributes_from_cards(self):
"""
Сохранить атрибуты из карточного интерфейса.
Каждая карточка содержит:
- attributes-X-name: название параметра
- attributes-X-position: позиция
- attributes-X-visible: видимость
- attributes-X-DELETE: помечен ли для удаления
Значения приходят как инлайн input'ы внутри параметра:
- Читаем из POST все 'parameter-value-input' инпуты
"""
# Сначала удаляем все старые атрибуты
ConfigurableKitProductAttribute.objects.filter(parent=self.object).delete()
# Получаем количество карточек параметров
total_forms_str = self.request.POST.get('attributes-TOTAL_FORMS', '0')
try:
total_forms = int(total_forms_str)
except (ValueError, TypeError):
total_forms = 0
# Обрабатываем каждую карточку параметра
for idx in range(total_forms):
# Пропускаем если карточка помечена для удаления
delete_key = f'attributes-{idx}-DELETE'
if delete_key in self.request.POST and self.request.POST.get(delete_key):
continue
# Получаем название параметра
name = self.request.POST.get(f'attributes-{idx}-name', '').strip()
if not name:
continue
position = self.request.POST.get(f'attributes-{idx}-position', idx)
try:
position = int(position)
except (ValueError, TypeError):
position = idx
visible = self.request.POST.get(f'attributes-{idx}-visible') == 'on'
# Получаем все значения параметра из POST
# Они приходят как data в JSON при отправке формы
# Нужно их извлечь из скрытых input'ов или динамически созданных
# Способ 1: Получаем все значения из POST которые относятся к этому параметру
# Шаблон: 'attr_{idx}_value_{value_idx}' или просто читаем из скрытого JSON поля
# Пока используем упрощённый подход:
# JavaScript должен будет отправить значения в скрытом поле JSON
# Формат: attributes-X-values = ["value1", "value2", "value3"]
values_json = self.request.POST.get(f'attributes-{idx}-values', '[]')
import json
try:
values = json.loads(values_json)
except (json.JSONDecodeError, TypeError):
values = []
# Создаём ConfigurableKitProductAttribute для каждого значения
for value_idx, value in enumerate(values):
if value and value.strip():
ConfigurableKitProductAttribute.objects.create(
parent=self.object,
name=name,
option=value.strip(),
position=position,
visible=visible
)
@staticmethod
def _should_delete_form(form, formset):
"""Проверить должна ли форма быть удалена"""
@@ -310,8 +382,9 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, UpdateView):
if option_form.instance.pk:
option_form.instance.delete()
# Сохраняем атрибуты родителя
attribute_formset.save()
# Сохраняем атрибуты родителя - новый интерфейс
# Карточный интерфейс: значения приходят как инлайн input'ы
self._save_attributes_from_cards()
messages.success(self.request, f'Вариативный товар "{self.object.name}" успешно обновлён!')
return super().form_valid(form)
@@ -322,6 +395,60 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, UpdateView):
traceback.print_exc()
return self.form_invalid(form)
def _save_attributes_from_cards(self):
"""
Сохранить атрибуты из карточного интерфейса.
См. копию этого метода в ConfigurableKitProductCreateView для подробностей.
"""
# Сначала удаляем все старые атрибуты
ConfigurableKitProductAttribute.objects.filter(parent=self.object).delete()
# Получаем количество карточек параметров
total_forms_str = self.request.POST.get('attributes-TOTAL_FORMS', '0')
try:
total_forms = int(total_forms_str)
except (ValueError, TypeError):
total_forms = 0
# Обрабатываем каждую карточку параметра
for idx in range(total_forms):
# Пропускаем если карточка помечена для удаления
delete_key = f'attributes-{idx}-DELETE'
if delete_key in self.request.POST and self.request.POST.get(delete_key):
continue
# Получаем название параметра
name = self.request.POST.get(f'attributes-{idx}-name', '').strip()
if not name:
continue
position = self.request.POST.get(f'attributes-{idx}-position', idx)
try:
position = int(position)
except (ValueError, TypeError):
position = idx
visible = self.request.POST.get(f'attributes-{idx}-visible') == 'on'
# Получаем все значения параметра из POST
values_json = self.request.POST.get(f'attributes-{idx}-values', '[]')
import json
try:
values = json.loads(values_json)
except (json.JSONDecodeError, TypeError):
values = []
# Создаём ConfigurableKitProductAttribute для каждого значения
for value_idx, value in enumerate(values):
if value and value.strip():
ConfigurableKitProductAttribute.objects.create(
parent=self.object,
name=name,
option=value.strip(),
position=position,
visible=visible
)
@staticmethod
def _should_delete_form(form, formset):
"""Проверить должна ли форма быть удалена"""