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):
|
def clean(self):
|
||||||
"""
|
"""
|
||||||
Валидация формы комплекта.
|
Валидация формы комплекта.
|
||||||
Проверяет что если выбран тип корректировки, указано значение.
|
Проверяет:
|
||||||
|
1. Что если выбран тип корректировки, указано значение
|
||||||
|
2. Что заполнено максимум одно поле корректировки (увеличение или уменьшение)
|
||||||
"""
|
"""
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
|
|||||||
@@ -387,6 +387,25 @@
|
|||||||
color: #0d6efd;
|
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) {
|
@media (max-width: 991px) {
|
||||||
.col-lg-8, .col-lg-4 {
|
.col-lg-8, .col-lg-4 {
|
||||||
@@ -584,11 +603,85 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
finalPriceDisplay.textContent = finalPrice.toFixed(2) + ' руб.';
|
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 => {
|
[increasePercentInput, increaseAmountInput, decreasePercentInput, decreaseAmountInput].forEach(input => {
|
||||||
if (input) {
|
if (input) {
|
||||||
input.addEventListener('input', calculateFinalPrice);
|
input.addEventListener('input', () => {
|
||||||
input.addEventListener('change', calculateFinalPrice);
|
validateSingleAdjustment();
|
||||||
|
calculateFinalPrice();
|
||||||
|
});
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
validateSingleAdjustment();
|
||||||
|
calculateFinalPrice();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -388,6 +388,25 @@
|
|||||||
color: #0d6efd;
|
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) {
|
@media (max-width: 991px) {
|
||||||
.col-lg-8, .col-lg-4 {
|
.col-lg-8, .col-lg-4 {
|
||||||
@@ -585,11 +604,85 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
finalPriceDisplay.textContent = finalPrice.toFixed(2) + ' руб.';
|
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 => {
|
[increasePercentInput, increaseAmountInput, decreasePercentInput, decreaseAmountInput].forEach(input => {
|
||||||
if (input) {
|
if (input) {
|
||||||
input.addEventListener('input', calculateFinalPrice);
|
input.addEventListener('input', () => {
|
||||||
input.addEventListener('change', calculateFinalPrice);
|
validateSingleAdjustment();
|
||||||
|
calculateFinalPrice();
|
||||||
|
});
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
validateSingleAdjustment();
|
||||||
|
calculateFinalPrice();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user