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:
@@ -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):
|
||||
"""Проверить должна ли форма быть удалена"""
|
||||
|
||||
Reference in New Issue
Block a user