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:
@@ -783,71 +783,76 @@ ConfigurableKitOptionFormSetUpdate = inlineformset_factory(
|
||||
|
||||
class ConfigurableKitProductAttributeForm(forms.ModelForm):
|
||||
"""
|
||||
Форма для добавления атрибута родительского товара.
|
||||
Пример: name="Цвет", option="Красный"
|
||||
Форма для добавления атрибута родительского товара в карточном интерфейсе.
|
||||
На фронтенде: одна карточка параметра (имя + позиция + видимость)
|
||||
+ множество инлайн значений через JavaScript
|
||||
|
||||
Пример структуры:
|
||||
- name: "Длина"
|
||||
- position: 0
|
||||
- visible: True
|
||||
- values: [50, 60, 70] (будут созданы как отдельные ConfigurableKitProductAttribute)
|
||||
"""
|
||||
class Meta:
|
||||
model = ConfigurableKitProductAttribute
|
||||
fields = ['name', 'option', 'position', 'visible']
|
||||
fields = ['name', 'position', 'visible']
|
||||
labels = {
|
||||
'name': 'Название атрибута',
|
||||
'option': 'Значение опции',
|
||||
'name': 'Название параметра',
|
||||
'position': 'Порядок',
|
||||
'visible': 'Видимый'
|
||||
'visible': 'Видимый на витрине'
|
||||
}
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Например: Цвет, Размер, Длина'
|
||||
}),
|
||||
'option': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Например: Красный, M, 60см'
|
||||
'class': 'form-control param-name-input',
|
||||
'placeholder': 'Например: Длина, Цвет, Размер',
|
||||
'readonly': 'readonly' # Должен быть заполнен через JavaScript
|
||||
}),
|
||||
'position': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'class': 'form-control param-position-input',
|
||||
'min': '0',
|
||||
'value': '0'
|
||||
}),
|
||||
'visible': forms.CheckboxInput(attrs={
|
||||
'class': 'form-check-input'
|
||||
'class': 'form-check-input param-visible-input'
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
class BaseConfigurableKitProductAttributeFormSet(forms.BaseInlineFormSet):
|
||||
def clean(self):
|
||||
"""Проверка на дубликаты атрибутов"""
|
||||
"""Проверка на дубликаты параметров и что у каждого параметра есть значения"""
|
||||
if any(self.errors):
|
||||
return
|
||||
|
||||
attributes = []
|
||||
parameter_names = []
|
||||
|
||||
for form in self.forms:
|
||||
if self.can_delete and self._should_delete_form(form):
|
||||
continue
|
||||
|
||||
name = form.cleaned_data.get('name')
|
||||
option = form.cleaned_data.get('option')
|
||||
# Пропускаем пустые формы
|
||||
if not form.cleaned_data.get('name'):
|
||||
continue
|
||||
|
||||
# Проверка дубликатов
|
||||
if name and option:
|
||||
attr_tuple = (name.strip(), option.strip())
|
||||
if attr_tuple in attributes:
|
||||
raise forms.ValidationError(
|
||||
f'Атрибут "{name}: {option}" добавлен более одного раза. '
|
||||
f'Каждая комбинация атрибут-значение должна быть уникальной.'
|
||||
)
|
||||
attributes.append(attr_tuple)
|
||||
name = form.cleaned_data.get('name').strip()
|
||||
|
||||
# Проверка дубликатов параметров (в карточном интерфейсе каждый параметр должен быть один раз)
|
||||
if name in parameter_names:
|
||||
raise forms.ValidationError(
|
||||
f'Параметр "{name}" добавлен более одного раза. '
|
||||
f'Каждый параметр должен быть добавлен только один раз.'
|
||||
)
|
||||
parameter_names.append(name)
|
||||
|
||||
|
||||
# Формсет для создания атрибутов родительского товара
|
||||
# Формсет для создания атрибутов родительского товара (карточный интерфейс)
|
||||
ConfigurableKitProductAttributeFormSetCreate = inlineformset_factory(
|
||||
ConfigurableKitProduct,
|
||||
ConfigurableKitProductAttribute,
|
||||
form=ConfigurableKitProductAttributeForm,
|
||||
formset=BaseConfigurableKitProductAttributeFormSet,
|
||||
fields=['name', 'option', 'position', 'visible'],
|
||||
# Убрали 'option' - значения будут добавляться через JavaScript в карточку
|
||||
fields=['name', 'position', 'visible'],
|
||||
extra=1,
|
||||
can_delete=True,
|
||||
min_num=0,
|
||||
@@ -861,7 +866,8 @@ ConfigurableKitProductAttributeFormSetUpdate = inlineformset_factory(
|
||||
ConfigurableKitProductAttribute,
|
||||
form=ConfigurableKitProductAttributeForm,
|
||||
formset=BaseConfigurableKitProductAttributeFormSet,
|
||||
fields=['name', 'option', 'position', 'visible'],
|
||||
# Убрали 'option' - значения будут добавляться через JavaScript в карточку
|
||||
fields=['name', 'position', 'visible'],
|
||||
extra=0,
|
||||
can_delete=True,
|
||||
min_num=0,
|
||||
|
||||
@@ -94,14 +94,14 @@ input[name*="DELETE"] {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Атрибуты родительского товара -->
|
||||
<!-- Атрибуты родительского товара - Карточный интерфейс -->
|
||||
<div class="card border-0 shadow-sm mb-3">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="mb-0">Атрибуты товара (для WooCommerce)</h5>
|
||||
<h5 class="mb-0">Параметры товара</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small text-muted mb-3">
|
||||
Определите схему атрибутов для вариативного товара. Например: Цвет=Красный, Размер=M, Длина=60см.
|
||||
Определите параметры вариативного товара и их значения. Например: Длина (50, 60, 70), Упаковка (БЕЗ, В УПАКОВКЕ).
|
||||
</p>
|
||||
|
||||
{{ attribute_formset.management_form }}
|
||||
@@ -114,26 +114,20 @@ input[name*="DELETE"] {
|
||||
|
||||
<div id="attributeFormsetContainer">
|
||||
{% for form in attribute_formset %}
|
||||
<div class="attribute-form border rounded p-3 mb-3" style="background: #f8f9fa;">
|
||||
<div class="attribute-card border rounded p-4 mb-3" style="background: #f8f9fa;" data-formset-index="{{ forloop.counter0 }}">
|
||||
{{ form.id }}
|
||||
{% if form.instance.pk %}
|
||||
<input type="hidden" name="attributes-{{ forloop.counter0 }}-id" value="{{ form.instance.pk }}">
|
||||
{% endif %}
|
||||
<div class="row g-2">
|
||||
|
||||
<div class="row align-items-end g-3 mb-3">
|
||||
<!-- Название параметра -->
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">{{ form.name.label }}</label>
|
||||
<label class="form-label fw-semibold">{{ form.name.label }}</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
<div class="text-danger small">{{ form.name.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">{{ form.option.label }}</label>
|
||||
{{ form.option }}
|
||||
{% if form.option.errors %}
|
||||
<div class="text-danger small">{{ form.option.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Позиция параметра -->
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">{{ form.position.label }}</label>
|
||||
{{ form.position }}
|
||||
@@ -141,34 +135,47 @@ input[name*="DELETE"] {
|
||||
<div class="text-danger small">{{ form.position.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small d-block">{{ form.visible.label }}</label>
|
||||
<div class="form-check">
|
||||
|
||||
<!-- Видимость -->
|
||||
<div class="col-md-3">
|
||||
<div class="form-check form-switch">
|
||||
{{ form.visible }}
|
||||
<label class="form-check-label" for="{{ form.visible.id_for_label }}">
|
||||
Показывать
|
||||
{{ form.visible.label }}
|
||||
</label>
|
||||
</div>
|
||||
{% if form.visible.errors %}
|
||||
<div class="text-danger small">{{ form.visible.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
|
||||
<!-- Удалить параметр -->
|
||||
<div class="col-md-2 text-end">
|
||||
{% if attribute_formset.can_delete %}
|
||||
<label class="form-label small d-block"> </label>
|
||||
{{ form.DELETE }}
|
||||
<label for="{{ form.DELETE.id_for_label }}" class="btn btn-sm btn-outline-danger d-block">
|
||||
<label for="{{ form.DELETE.id_for_label }}" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</label>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Значения параметра (добавляются инлайн через JavaScript) -->
|
||||
<div class="parameter-values-container mt-3 p-3 bg-white rounded border">
|
||||
<label class="form-label small fw-semibold d-block mb-2">Значения параметра:</label>
|
||||
<div class="value-fields-wrapper" data-param-index="{{ forloop.counter0 }}">
|
||||
<!-- Значения будут добавлены через JavaScript -->
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-2 add-value-btn">
|
||||
<i class="bi bi-plus-circle me-1"></i> Добавить значение
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addAttributeBtn">
|
||||
<i class="bi bi-plus-circle me-1"></i>Добавить атрибут
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="addParameterBtn">
|
||||
<i class="bi bi-plus-circle me-1"></i> Добавить параметр
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -454,78 +461,187 @@ function initDefaultSwitches() {
|
||||
// Запускаем инициализацию
|
||||
initDefaultSwitches();
|
||||
|
||||
// === Добавление новых форм атрибутов ===
|
||||
document.getElementById('addAttributeBtn').addEventListener('click', function() {
|
||||
// === Управление параметрами товара (карточный интерфейс) ===
|
||||
|
||||
// Функция для добавления нового поля значения параметра
|
||||
function addValueField(container, valueText = '') {
|
||||
const index = container.querySelectorAll('.value-field-group').length;
|
||||
const fieldId = `value-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const html = `
|
||||
<div class="value-field-group d-flex gap-2 mb-2">
|
||||
<input type="text" class="form-control form-control-sm parameter-value-input"
|
||||
placeholder="Введите значение"
|
||||
value="${valueText}"
|
||||
data-field-id="${fieldId}">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger remove-value-btn" title="Удалить значение">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', html);
|
||||
|
||||
// Обработчик удаления значения
|
||||
container.querySelector('.remove-value-btn:last-child').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
this.closest('.value-field-group').remove();
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация существующих параметров с их значениями из БД
|
||||
function initializeParameterCards() {
|
||||
document.querySelectorAll('.attribute-card').forEach(card => {
|
||||
// Если это существующий параметр с ID, загрузим его значения
|
||||
// Это будет обработано при первой загрузке в view
|
||||
initAddValueBtn(card);
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация кнопки добавления значения для карточки
|
||||
function initAddValueBtn(card) {
|
||||
const addBtn = card.querySelector('.add-value-btn');
|
||||
if (addBtn) {
|
||||
addBtn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const container = this.closest('.parameter-values-container').querySelector('.value-fields-wrapper');
|
||||
addValueField(container);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Добавление нового параметра
|
||||
document.getElementById('addParameterBtn')?.addEventListener('click', function() {
|
||||
const container = document.getElementById('attributeFormsetContainer');
|
||||
const totalForms = document.querySelector('[name="attributes-TOTAL_FORMS"]');
|
||||
const formIdx = parseInt(totalForms.value);
|
||||
|
||||
// Создаём новую форму HTML
|
||||
const newFormHtml = `
|
||||
<div class="attribute-form border rounded p-3 mb-3" style="background: #f8f9fa;">
|
||||
<div class="row g-2">
|
||||
|
||||
const newCardHtml = `
|
||||
<div class="attribute-card border rounded p-4 mb-3" style="background: #f8f9fa;" data-formset-index="${formIdx}">
|
||||
<input type="hidden" name="attributes-${formIdx}-id">
|
||||
|
||||
<div class="row align-items-end g-3 mb-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Название атрибута</label>
|
||||
<input type="text" name="attributes-${formIdx}-name"
|
||||
id="id_attributes-${formIdx}-name"
|
||||
class="form-control"
|
||||
placeholder="Например: Цвет, Размер, Длина">
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small">Значение опции</label>
|
||||
<input type="text" name="attributes-${formIdx}-option"
|
||||
id="id_attributes-${formIdx}-option"
|
||||
class="form-control"
|
||||
placeholder="Например: Красный, M, 60см">
|
||||
<label class="form-label fw-semibold">Название параметра</label>
|
||||
<input type="text" name="attributes-${formIdx}-name"
|
||||
id="id_attributes-${formIdx}-name"
|
||||
class="form-control param-name-input"
|
||||
placeholder="Например: Длина, Цвет, Размер">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small">Порядок</label>
|
||||
<input type="number" name="attributes-${formIdx}-position"
|
||||
id="id_attributes-${formIdx}-position"
|
||||
class="form-control"
|
||||
min="0" value="0">
|
||||
<input type="number" name="attributes-${formIdx}-position"
|
||||
id="id_attributes-${formIdx}-position"
|
||||
class="form-control param-position-input"
|
||||
min="0" value="${formIdx}">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small d-block">Видимый</label>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" name="attributes-${formIdx}-visible"
|
||||
id="id_attributes-${formIdx}-visible"
|
||||
class="form-check-input" checked>
|
||||
<div class="col-md-3">
|
||||
<div class="form-check form-switch">
|
||||
<input type="checkbox" name="attributes-${formIdx}-visible"
|
||||
id="id_attributes-${formIdx}-visible"
|
||||
class="form-check-input param-visible-input"
|
||||
checked>
|
||||
<label class="form-check-label" for="id_attributes-${formIdx}-visible">
|
||||
Показывать
|
||||
Видимый на витрине
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small d-block"> </label>
|
||||
<input type="checkbox" name="attributes-${formIdx}-DELETE"
|
||||
id="id_attributes-${formIdx}-DELETE"
|
||||
<div class="col-md-2 text-end">
|
||||
<input type="checkbox" name="attributes-${formIdx}-DELETE"
|
||||
id="id_attributes-${formIdx}-DELETE"
|
||||
style="display:none;">
|
||||
<label for="id_attributes-${formIdx}-DELETE" class="btn btn-sm btn-outline-danger d-block">
|
||||
<label for="id_attributes-${formIdx}-DELETE" class="btn btn-sm btn-outline-danger">
|
||||
<i class="bi bi-trash"></i> Удалить
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="parameter-values-container mt-3 p-3 bg-white rounded border">
|
||||
<label class="form-label small fw-semibold d-block mb-2">Значения параметра:</label>
|
||||
<div class="value-fields-wrapper" data-param-index="${formIdx}">
|
||||
<!-- Значения добавляются сюда -->
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-2 add-value-btn">
|
||||
<i class="bi bi-plus-circle me-1"></i> Добавить значение
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', newFormHtml);
|
||||
|
||||
container.insertAdjacentHTML('beforeend', newCardHtml);
|
||||
totalForms.value = formIdx + 1;
|
||||
|
||||
// Инициализируем новую карточку
|
||||
const newCard = container.querySelector(`[data-formset-index="${formIdx}"]`);
|
||||
initAddValueBtn(newCard);
|
||||
|
||||
// Инициализируем удаление параметра
|
||||
initParamDeleteToggle(newCard);
|
||||
});
|
||||
|
||||
// Скрытие удаленных атрибутов
|
||||
document.addEventListener('change', function(e) {
|
||||
if (e.target.type === 'checkbox' && e.target.name && e.target.name.includes('attributes') && e.target.name.includes('DELETE')) {
|
||||
const form = e.target.closest('.attribute-form');
|
||||
if (form) {
|
||||
if (e.target.checked) {
|
||||
form.style.opacity = '0.5';
|
||||
form.style.textDecoration = 'line-through';
|
||||
// Функция для скрытия удаленного параметра
|
||||
function initParamDeleteToggle(card) {
|
||||
const deleteCheckbox = card.querySelector('input[type="checkbox"][name$="-DELETE"]');
|
||||
if (deleteCheckbox) {
|
||||
deleteCheckbox.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
card.style.opacity = '0.5';
|
||||
card.style.textDecoration = 'line-through';
|
||||
} else {
|
||||
form.style.opacity = '1';
|
||||
form.style.textDecoration = 'none';
|
||||
card.style.opacity = '1';
|
||||
card.style.textDecoration = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для сериализации значений параметров перед отправкой формы
|
||||
function serializeAttributeValues() {
|
||||
/**
|
||||
* Перед отправкой формы нужно сериализовать все значения параметров
|
||||
* из инлайн input'ов в скрытые JSON поля для отправки на сервер
|
||||
*/
|
||||
document.querySelectorAll('.attribute-card').forEach((card, idx) => {
|
||||
// Получаем все инпуты с значениями внутри этой карточки
|
||||
const valueInputs = card.querySelectorAll('.parameter-value-input');
|
||||
const values = [];
|
||||
|
||||
valueInputs.forEach(input => {
|
||||
const value = input.value.trim();
|
||||
if (value) {
|
||||
values.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
// Создаем или обновляем скрытое поле 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);
|
||||
}
|
||||
|
||||
jsonField.value = JSON.stringify(values);
|
||||
});
|
||||
}
|
||||
|
||||
// Инициализация при загрузке страницы
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeParameterCards();
|
||||
document.querySelectorAll('.attribute-card').forEach(card => {
|
||||
initParamDeleteToggle(card);
|
||||
});
|
||||
|
||||
// Добавляем сериализацию значений перед отправкой формы
|
||||
const form = document.querySelector('form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', function(e) {
|
||||
// Перед отправкой формы сериализуем все значения параметров
|
||||
serializeAttributeValues();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -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