Add ProductKit binding to ConfigurableKitProductAttribute values
Implementation of kit binding feature for ConfigurableKitProduct variants: - Added ForeignKey field `kit` to ConfigurableKitProductAttribute * References ProductKit with CASCADE delete * Optional field (blank=True, null=True) * Indexed for efficient queries - Created migration 0007_add_kit_to_attribute * Handles existing data (NULL values for all current records) * Properly indexed for performance - Updated template configurablekit_form.html * Injected available ProductKits into JavaScript * Added kit selector dropdown in card interface * Each value now has associated kit selection * JavaScript validates kit selection alongside values - Updated JavaScript in card interface * serializeAttributeValues() now collects kit IDs * Creates parallel JSON arrays: values and kits * Stores in hidden fields: attributes-X-values and attributes-X-kits - Updated views _save_attributes_from_cards() in both Create and Update * Reads kit IDs from POST JSON * Looks up ProductKit objects * Creates ConfigurableKitProductAttribute with FK populated * Gracefully handles missing kits - Fixed _should_delete_form() method * More robust handling of formset deletion_field * Works with all formset types - Updated __str__() method * Handles NULL kit case Example workflow: Dlina: 50 -> Kit A, 60 -> Kit B, 70 -> Kit C Upakovka: BEZ -> Kit A, V_UPAKOVKE -> (no kit) Tested with test_kit_binding.py - all tests passing - Kit creation and retrieval - Attribute creation with kit FK - Mixed kit-bound and unbound attributes - Querying attributes by kit - Reverse queries (get kit for attribute value) Added documentation: KIT_BINDING_IMPLEMENTATION.md 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -136,7 +136,13 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, CreateView):
|
||||
context['attribute_formset'] = ConfigurableKitProductAttributeFormSetCreate(
|
||||
prefix='attributes'
|
||||
)
|
||||
|
||||
|
||||
# Доступные комплекты для JavaScript (для выбора при добавлении значений атрибутов)
|
||||
context['available_kits'] = ProductKit.objects.filter(
|
||||
status='active',
|
||||
is_temporary=False
|
||||
).order_by('name')
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
@@ -215,10 +221,12 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, CreateView):
|
||||
- attributes-X-position: позиция
|
||||
- attributes-X-visible: видимость
|
||||
- attributes-X-DELETE: помечен ли для удаления
|
||||
|
||||
Значения приходят как инлайн input'ы внутри параметра:
|
||||
- Читаем из POST все 'parameter-value-input' инпуты
|
||||
- attributes-X-values: JSON массив значений параметра
|
||||
- attributes-X-kits: JSON массив ID комплектов для каждого значения
|
||||
"""
|
||||
import json
|
||||
from products.models.kits import ProductKit
|
||||
|
||||
# Сначала удаляем все старые атрибуты
|
||||
ConfigurableKitProductAttribute.objects.filter(parent=self.object).delete()
|
||||
|
||||
@@ -249,39 +257,56 @@ class ConfigurableKitProductCreateView(LoginRequiredMixin, CreateView):
|
||||
|
||||
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
|
||||
kits_json = self.request.POST.get(f'attributes-{idx}-kits', '[]')
|
||||
|
||||
try:
|
||||
values = json.loads(values_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
values = []
|
||||
|
||||
try:
|
||||
kit_ids = json.loads(kits_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
kit_ids = []
|
||||
|
||||
# Создаём 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
|
||||
)
|
||||
# Получаем соответствующий ID комплекта
|
||||
kit_id = kit_ids[value_idx] if value_idx < len(kit_ids) else None
|
||||
|
||||
# Приготавливаем параметры создания
|
||||
create_kwargs = {
|
||||
'parent': self.object,
|
||||
'name': name,
|
||||
'option': value.strip(),
|
||||
'position': position,
|
||||
'visible': visible
|
||||
}
|
||||
|
||||
# Добавляем комплект если указан
|
||||
if kit_id:
|
||||
try:
|
||||
kit = ProductKit.objects.get(id=kit_id)
|
||||
create_kwargs['kit'] = kit
|
||||
except ProductKit.DoesNotExist:
|
||||
# Комплект не найден - создаём без привязки
|
||||
pass
|
||||
|
||||
ConfigurableKitProductAttribute.objects.create(**create_kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _should_delete_form(form, formset):
|
||||
"""Проверить должна ли форма быть удалена"""
|
||||
return formset.can_delete and form.cleaned_data.get(formset.deletion_field.name, False)
|
||||
if not formset.can_delete:
|
||||
return False
|
||||
# Проверяем поле DELETE (стандартное имя для formset deletion field)
|
||||
deletion_field_name = 'DELETE'
|
||||
if hasattr(formset, 'deletion_field') and hasattr(formset.deletion_field, 'name'):
|
||||
deletion_field_name = formset.deletion_field.name
|
||||
return form.cleaned_data.get(deletion_field_name, False)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('products:configurablekit-detail', kwargs={'pk': self.object.pk})
|
||||
@@ -324,7 +349,13 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, UpdateView):
|
||||
instance=self.object,
|
||||
prefix='attributes'
|
||||
)
|
||||
|
||||
|
||||
# Доступные комплекты для JavaScript (для выбора при добавлении значений атрибутов)
|
||||
context['available_kits'] = ProductKit.objects.filter(
|
||||
status='active',
|
||||
is_temporary=False
|
||||
).order_by('name')
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
@@ -398,8 +429,18 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, UpdateView):
|
||||
def _save_attributes_from_cards(self):
|
||||
"""
|
||||
Сохранить атрибуты из карточного интерфейса.
|
||||
См. копию этого метода в ConfigurableKitProductCreateView для подробностей.
|
||||
|
||||
Каждая карточка содержит:
|
||||
- attributes-X-name: название параметра
|
||||
- attributes-X-position: позиция
|
||||
- attributes-X-visible: видимость
|
||||
- attributes-X-DELETE: помечен ли для удаления
|
||||
- attributes-X-values: JSON массив значений параметра
|
||||
- attributes-X-kits: JSON массив ID комплектов для каждого значения
|
||||
"""
|
||||
import json
|
||||
from products.models.kits import ProductKit
|
||||
|
||||
# Сначала удаляем все старые атрибуты
|
||||
ConfigurableKitProductAttribute.objects.filter(parent=self.object).delete()
|
||||
|
||||
@@ -430,29 +471,56 @@ class ConfigurableKitProductUpdateView(LoginRequiredMixin, UpdateView):
|
||||
|
||||
visible = self.request.POST.get(f'attributes-{idx}-visible') == 'on'
|
||||
|
||||
# Получаем все значения параметра из POST
|
||||
# Получаем значения и их привязанные комплекты
|
||||
values_json = self.request.POST.get(f'attributes-{idx}-values', '[]')
|
||||
import json
|
||||
kits_json = self.request.POST.get(f'attributes-{idx}-kits', '[]')
|
||||
|
||||
try:
|
||||
values = json.loads(values_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
values = []
|
||||
|
||||
try:
|
||||
kit_ids = json.loads(kits_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
kit_ids = []
|
||||
|
||||
# Создаём 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
|
||||
)
|
||||
# Получаем соответствующий ID комплекта
|
||||
kit_id = kit_ids[value_idx] if value_idx < len(kit_ids) else None
|
||||
|
||||
# Приготавливаем параметры создания
|
||||
create_kwargs = {
|
||||
'parent': self.object,
|
||||
'name': name,
|
||||
'option': value.strip(),
|
||||
'position': position,
|
||||
'visible': visible
|
||||
}
|
||||
|
||||
# Добавляем комплект если указан
|
||||
if kit_id:
|
||||
try:
|
||||
kit = ProductKit.objects.get(id=kit_id)
|
||||
create_kwargs['kit'] = kit
|
||||
except ProductKit.DoesNotExist:
|
||||
# Комплект не найден - создаём без привязки
|
||||
pass
|
||||
|
||||
ConfigurableKitProductAttribute.objects.create(**create_kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _should_delete_form(form, formset):
|
||||
"""Проверить должна ли форма быть удалена"""
|
||||
return formset.can_delete and form.cleaned_data.get(formset.deletion_field.name, False)
|
||||
if not formset.can_delete:
|
||||
return False
|
||||
# Проверяем поле DELETE (стандартное имя для formset deletion field)
|
||||
deletion_field_name = 'DELETE'
|
||||
if hasattr(formset, 'deletion_field') and hasattr(formset.deletion_field, 'name'):
|
||||
deletion_field_name = formset.deletion_field.name
|
||||
return form.cleaned_data.get(deletion_field_name, False)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('products:configurablekit-detail', kwargs={'pk': self.object.pk})
|
||||
|
||||
Reference in New Issue
Block a user