feat: Добавить валидацию для заполнения одного поля корректировки цены
Реализована логика чтобы только одно из четырёх полей корректировки цены можно было заполнить одновременно: JavaScript валидация: - При заполнении одного поля остальные 3 автоматически отключаются - При попытке заполнить два поля одновременно: - Оставляется только первое заполненное - Остальные очищаются и помечаются как ошибка - При очистке всех полей они снова активируются CSS стили: - Disabled поля: серый фон, пониженная прозрачность, запрещённый курсор - Invalid поля: красная граница и shadow (Bootstrap стиль) Валидация работает на обе стороны: - Frontend JavaScript (instant feedback) - Backend Python валидация (безопасность) Файлы: - products/templates/products/productkit_create.html - products/templates/products/productkit_edit.html - products/forms.py (документация)
This commit is contained in:
@@ -139,7 +139,9 @@ class ProductKitForm(forms.ModelForm):
|
||||
def clean(self):
|
||||
"""
|
||||
Валидация формы комплекта.
|
||||
Проверяет что если выбран тип корректировки, указано значение.
|
||||
Проверяет:
|
||||
1. Что если выбран тип корректировки, указано значение
|
||||
2. Что заполнено максимум одно поле корректировки (увеличение или уменьшение)
|
||||
"""
|
||||
cleaned_data = super().clean()
|
||||
|
||||
|
||||
@@ -387,6 +387,25 @@
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
/* Стили для полей корректировки цены */
|
||||
#id_increase_percent:disabled,
|
||||
#id_increase_amount:disabled,
|
||||
#id_decrease_percent:disabled,
|
||||
#id_decrease_amount:disabled {
|
||||
background-color: #e9ecef;
|
||||
color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#id_increase_percent.is-invalid,
|
||||
#id_increase_amount.is-invalid,
|
||||
#id_decrease_percent.is-invalid,
|
||||
#id_decrease_amount.is-invalid {
|
||||
border-color: #dc3545;
|
||||
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 991px) {
|
||||
.col-lg-8, .col-lg-4 {
|
||||
@@ -584,11 +603,85 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
finalPriceDisplay.textContent = finalPrice.toFixed(2) + ' руб.';
|
||||
}
|
||||
|
||||
// Функция для управления состоянием полей (только одно может быть заполнено)
|
||||
function validateSingleAdjustment() {
|
||||
const increasePercent = parseFloat(increasePercentInput.value) || 0;
|
||||
const increaseAmount = parseFloat(increaseAmountInput.value) || 0;
|
||||
const decreasePercent = parseFloat(decreasePercentInput.value) || 0;
|
||||
const decreaseAmount = parseFloat(decreaseAmountInput.value) || 0;
|
||||
|
||||
// Подсчитываем сколько полей заполнено
|
||||
const filledCount = (increasePercent > 0 ? 1 : 0) +
|
||||
(increaseAmount > 0 ? 1 : 0) +
|
||||
(decreasePercent > 0 ? 1 : 0) +
|
||||
(decreaseAmount > 0 ? 1 : 0);
|
||||
|
||||
const allInputs = [increasePercentInput, increaseAmountInput, decreasePercentInput, decreaseAmountInput];
|
||||
|
||||
if (filledCount === 0) {
|
||||
// Ничего не заполнено - все поля активны
|
||||
allInputs.forEach(input => {
|
||||
input.disabled = false;
|
||||
input.classList.remove('is-invalid');
|
||||
});
|
||||
} else if (filledCount === 1) {
|
||||
// Ровно одно поле заполнено - отключаем остальные
|
||||
allInputs.forEach(input => {
|
||||
const isCurrentField = (
|
||||
input === increasePercentInput && increasePercent > 0 ||
|
||||
input === increaseAmountInput && increaseAmount > 0 ||
|
||||
input === decreasePercentInput && decreasePercent > 0 ||
|
||||
input === decreaseAmountInput && decreaseAmount > 0
|
||||
);
|
||||
|
||||
input.disabled = !isCurrentField;
|
||||
input.classList.remove('is-invalid');
|
||||
});
|
||||
} else {
|
||||
// Несколько полей заполнено - помечаем как ошибку и очищаем лишние
|
||||
allInputs.forEach(input => {
|
||||
input.classList.add('is-invalid');
|
||||
});
|
||||
|
||||
// Оставляем только первое заполненное поле, остальные очищаем
|
||||
let foundFirst = false;
|
||||
if (increasePercent > 0 && !foundFirst) {
|
||||
foundFirst = true;
|
||||
} else if (foundFirst || increasePercent > 0) {
|
||||
increasePercentInput.value = '';
|
||||
}
|
||||
|
||||
if (increaseAmount > 0 && !foundFirst) {
|
||||
foundFirst = true;
|
||||
} else if (foundFirst || increaseAmount > 0) {
|
||||
increaseAmountInput.value = '';
|
||||
}
|
||||
|
||||
if (decreasePercent > 0 && !foundFirst) {
|
||||
foundFirst = true;
|
||||
} else if (foundFirst || decreasePercent > 0) {
|
||||
decreasePercentInput.value = '';
|
||||
}
|
||||
|
||||
if (decreaseAmount > 0 && !foundFirst) {
|
||||
foundFirst = true;
|
||||
} else if (foundFirst || decreaseAmount > 0) {
|
||||
decreaseAmountInput.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем обработчики для всех полей цены
|
||||
[increasePercentInput, increaseAmountInput, decreasePercentInput, decreaseAmountInput].forEach(input => {
|
||||
if (input) {
|
||||
input.addEventListener('input', calculateFinalPrice);
|
||||
input.addEventListener('change', calculateFinalPrice);
|
||||
input.addEventListener('input', () => {
|
||||
validateSingleAdjustment();
|
||||
calculateFinalPrice();
|
||||
});
|
||||
input.addEventListener('change', () => {
|
||||
validateSingleAdjustment();
|
||||
calculateFinalPrice();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -388,6 +388,25 @@
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
/* Стили для полей корректировки цены */
|
||||
#id_increase_percent:disabled,
|
||||
#id_increase_amount:disabled,
|
||||
#id_decrease_percent:disabled,
|
||||
#id_decrease_amount:disabled {
|
||||
background-color: #e9ecef;
|
||||
color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#id_increase_percent.is-invalid,
|
||||
#id_increase_amount.is-invalid,
|
||||
#id_decrease_percent.is-invalid,
|
||||
#id_decrease_amount.is-invalid {
|
||||
border-color: #dc3545;
|
||||
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||
}
|
||||
|
||||
/* Адаптивность */
|
||||
@media (max-width: 991px) {
|
||||
.col-lg-8, .col-lg-4 {
|
||||
@@ -585,11 +604,85 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
finalPriceDisplay.textContent = finalPrice.toFixed(2) + ' руб.';
|
||||
}
|
||||
|
||||
// Функция для управления состоянием полей (только одно может быть заполнено)
|
||||
function validateSingleAdjustment() {
|
||||
const increasePercent = parseFloat(increasePercentInput.value) || 0;
|
||||
const increaseAmount = parseFloat(increaseAmountInput.value) || 0;
|
||||
const decreasePercent = parseFloat(decreasePercentInput.value) || 0;
|
||||
const decreaseAmount = parseFloat(decreaseAmountInput.value) || 0;
|
||||
|
||||
// Подсчитываем сколько полей заполнено
|
||||
const filledCount = (increasePercent > 0 ? 1 : 0) +
|
||||
(increaseAmount > 0 ? 1 : 0) +
|
||||
(decreasePercent > 0 ? 1 : 0) +
|
||||
(decreaseAmount > 0 ? 1 : 0);
|
||||
|
||||
const allInputs = [increasePercentInput, increaseAmountInput, decreasePercentInput, decreaseAmountInput];
|
||||
|
||||
if (filledCount === 0) {
|
||||
// Ничего не заполнено - все поля активны
|
||||
allInputs.forEach(input => {
|
||||
input.disabled = false;
|
||||
input.classList.remove('is-invalid');
|
||||
});
|
||||
} else if (filledCount === 1) {
|
||||
// Ровно одно поле заполнено - отключаем остальные
|
||||
allInputs.forEach(input => {
|
||||
const isCurrentField = (
|
||||
input === increasePercentInput && increasePercent > 0 ||
|
||||
input === increaseAmountInput && increaseAmount > 0 ||
|
||||
input === decreasePercentInput && decreasePercent > 0 ||
|
||||
input === decreaseAmountInput && decreaseAmount > 0
|
||||
);
|
||||
|
||||
input.disabled = !isCurrentField;
|
||||
input.classList.remove('is-invalid');
|
||||
});
|
||||
} else {
|
||||
// Несколько полей заполнено - помечаем как ошибку и очищаем лишние
|
||||
allInputs.forEach(input => {
|
||||
input.classList.add('is-invalid');
|
||||
});
|
||||
|
||||
// Оставляем только первое заполненное поле, остальные очищаем
|
||||
let foundFirst = false;
|
||||
if (increasePercent > 0 && !foundFirst) {
|
||||
foundFirst = true;
|
||||
} else if (foundFirst || increasePercent > 0) {
|
||||
increasePercentInput.value = '';
|
||||
}
|
||||
|
||||
if (increaseAmount > 0 && !foundFirst) {
|
||||
foundFirst = true;
|
||||
} else if (foundFirst || increaseAmount > 0) {
|
||||
increaseAmountInput.value = '';
|
||||
}
|
||||
|
||||
if (decreasePercent > 0 && !foundFirst) {
|
||||
foundFirst = true;
|
||||
} else if (foundFirst || decreasePercent > 0) {
|
||||
decreasePercentInput.value = '';
|
||||
}
|
||||
|
||||
if (decreaseAmount > 0 && !foundFirst) {
|
||||
foundFirst = true;
|
||||
} else if (foundFirst || decreaseAmount > 0) {
|
||||
decreaseAmountInput.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем обработчики для всех полей цены
|
||||
[increasePercentInput, increaseAmountInput, decreasePercentInput, decreaseAmountInput].forEach(input => {
|
||||
if (input) {
|
||||
input.addEventListener('input', calculateFinalPrice);
|
||||
input.addEventListener('change', calculateFinalPrice);
|
||||
input.addEventListener('input', () => {
|
||||
validateSingleAdjustment();
|
||||
calculateFinalPrice();
|
||||
});
|
||||
input.addEventListener('change', () => {
|
||||
validateSingleAdjustment();
|
||||
calculateFinalPrice();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user