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:
@@ -106,6 +106,15 @@ input[name*="DELETE"] {
|
||||
|
||||
{{ attribute_formset.management_form }}
|
||||
|
||||
<!-- Список доступных комплектов для JavaScript -->
|
||||
<script>
|
||||
window.AVAILABLE_KITS = [
|
||||
{% for kit in available_kits %}
|
||||
{ id: {{ kit.id }}, name: "{{ kit.name }}" }{% if not forloop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
</script>
|
||||
|
||||
{% if attribute_formset.non_form_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{{ attribute_formset.non_form_errors }}
|
||||
@@ -463,17 +472,28 @@ initDefaultSwitches();
|
||||
|
||||
// === Управление параметрами товара (карточный интерфейс) ===
|
||||
|
||||
// Функция для добавления нового поля значения параметра
|
||||
function addValueField(container, valueText = '') {
|
||||
// Функция для добавления нового поля значения параметра с выбором ProductKit
|
||||
function addValueField(container, valueText = '', kitId = '') {
|
||||
const index = container.querySelectorAll('.value-field-group').length;
|
||||
const fieldId = `value-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// Получаем список доступных комплектов из скрытого элемента
|
||||
const kitOptionsHtml = getKitOptionsHtml(kitId);
|
||||
|
||||
const html = `
|
||||
<div class="value-field-group d-flex gap-2 mb-2">
|
||||
<div class="value-field-group d-flex gap-2 mb-2 align-items-start">
|
||||
<input type="text" class="form-control form-control-sm parameter-value-input"
|
||||
placeholder="Введите значение"
|
||||
value="${valueText}"
|
||||
data-field-id="${fieldId}">
|
||||
data-field-id="${fieldId}"
|
||||
style="min-width: 100px;">
|
||||
<select class="form-select form-select-sm parameter-kit-select"
|
||||
data-field-id="${fieldId}"
|
||||
title="Выберите комплект для этого значения"
|
||||
style="min-width: 150px;">
|
||||
<option value="">-- Выберите комплект --</option>
|
||||
${kitOptionsHtml}
|
||||
</select>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-value-btn" title="Удалить значение">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
@@ -482,6 +502,14 @@ function addValueField(container, valueText = '') {
|
||||
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
|
||||
// Установка выбранного комплекта если был передан
|
||||
if (kitId) {
|
||||
const kitSelect = container.querySelector('.parameter-kit-select:last-child');
|
||||
if (kitSelect) {
|
||||
kitSelect.value = kitId;
|
||||
}
|
||||
}
|
||||
|
||||
// Обработчик удаления значения
|
||||
container.querySelector('.remove-value-btn:last-child').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
@@ -489,6 +517,15 @@ function addValueField(container, valueText = '') {
|
||||
});
|
||||
}
|
||||
|
||||
// Получить HTML с опциями комплектов
|
||||
function getKitOptionsHtml(selectedKitId = '') {
|
||||
const kitsData = window.AVAILABLE_KITS || [];
|
||||
return kitsData.map(kit => {
|
||||
const selected = kit.id == selectedKitId ? 'selected' : '';
|
||||
return `<option value="${kit.id}" ${selected}>${kit.name}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Инициализация существующих параметров с их значениями из БД
|
||||
function initializeParameterCards() {
|
||||
document.querySelectorAll('.attribute-card').forEach(card => {
|
||||
@@ -595,36 +632,55 @@ function initParamDeleteToggle(card) {
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для сериализации значений параметров перед отправкой формы
|
||||
// Функция для сериализации значений параметров и их комплектов перед отправкой формы
|
||||
function serializeAttributeValues() {
|
||||
/**
|
||||
* Перед отправкой формы нужно сериализовать все значения параметров
|
||||
* из инлайн input'ов в скрытые JSON поля для отправки на сервер
|
||||
* и их связанные комплекты из инлайн input'ов в скрытые JSON поля
|
||||
*/
|
||||
document.querySelectorAll('.attribute-card').forEach((card, idx) => {
|
||||
// Получаем все инпуты с значениями внутри этой карточки
|
||||
const valueInputs = card.querySelectorAll('.parameter-value-input');
|
||||
// Получаем все инпуты с значениями и их комплектами внутри этой карточки
|
||||
const valueGroups = card.querySelectorAll('.value-field-group');
|
||||
const values = [];
|
||||
const kits = [];
|
||||
|
||||
valueInputs.forEach(input => {
|
||||
const value = input.value.trim();
|
||||
if (value) {
|
||||
values.push(value);
|
||||
valueGroups.forEach(group => {
|
||||
const valueInput = group.querySelector('.parameter-value-input');
|
||||
const kitSelect = group.querySelector('.parameter-kit-select');
|
||||
|
||||
if (valueInput) {
|
||||
const value = valueInput.value.trim();
|
||||
const kitId = kitSelect ? kitSelect.value : '';
|
||||
|
||||
if (value && kitId) { // Требуем чтобы оба поля были заполнены
|
||||
values.push(value);
|
||||
kits.push(parseInt(kitId));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Создаем или обновляем скрытое поле JSON с названием attributes-{idx}-values
|
||||
const jsonFieldName = `attributes-${idx}-values`;
|
||||
let jsonField = document.querySelector(`input[name="${jsonFieldName}"]`);
|
||||
|
||||
if (!jsonField) {
|
||||
jsonField = document.createElement('input');
|
||||
jsonField.type = 'hidden';
|
||||
jsonField.name = jsonFieldName;
|
||||
card.appendChild(jsonField);
|
||||
// Создаем или обновляем скрытые поля JSON
|
||||
// поле values: ["50", "60", "70"]
|
||||
const valuesFieldName = `attributes-${idx}-values`;
|
||||
let valuesField = document.querySelector(`input[name="${valuesFieldName}"]`);
|
||||
if (!valuesField) {
|
||||
valuesField = document.createElement('input');
|
||||
valuesField.type = 'hidden';
|
||||
valuesField.name = valuesFieldName;
|
||||
card.appendChild(valuesField);
|
||||
}
|
||||
valuesField.value = JSON.stringify(values);
|
||||
|
||||
jsonField.value = JSON.stringify(values);
|
||||
// поле kits: [1, 2, 3] (id ProductKit)
|
||||
const kitsFieldName = `attributes-${idx}-kits`;
|
||||
let kitsField = document.querySelector(`input[name="${kitsFieldName}"]`);
|
||||
if (!kitsField) {
|
||||
kitsField = document.createElement('input');
|
||||
kitsField.type = 'hidden';
|
||||
kitsField.name = kitsFieldName;
|
||||
card.appendChild(kitsField);
|
||||
}
|
||||
kitsField.value = JSON.stringify(kits);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user