feat: Добавить трёхуровневую защиту от дубликатов имён товаров, категорий, тегов и комплектов
Реализована полная система обеспечения уникальности названий: 1. **Уровень БД (Model Constraints)** - добавлены UniqueConstraint для: - Product: уникальность имени среди активных товаров - ProductCategory: уникальность имени среди активных категорий - ProductTag: уникальность имени только для активных тегов (неактивные могут повторяться) - ProductKit: уникальность имени среди активных, непроизвременных комплектов 2. **Уровень формы (Form Validation)** - добавлены clean() методы для: - ProductForm, ProductKitForm, ProductCategoryForm, ProductTagForm - Валидация до попытки сохранения в БД - Сохранение введённых данных при ошибке валидации 3. **Уровень представления (IntegrityError Handling)** - добавлена обработка в views: - ProductCategoryCreateView, ProductCategoryUpdateView - ProductTagCreateView, ProductTagUpdateView - ProductKitCreateView, ProductKitUpdateView - create_tag_api: защита от race conditions с fallback поиском Три уровня защиты гарантируют: - Профилактика ошибок на уровне формы - Обработка исключительных ситуаций в views - Защита БД от одновременных запросов (race conditions) - Пользователь видит понятное сообщение об ошибке вместо 500 ошибки 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,30 @@ class ProductForm(forms.ModelForm):
|
||||
self.fields['unit'].widget.attrs.update({'class': 'form-control'})
|
||||
self.fields['is_active'].widget.attrs.update({'class': 'form-check-input'})
|
||||
|
||||
def clean(self):
|
||||
"""Валидация уникальности имени для активных товаров"""
|
||||
cleaned_data = super().clean()
|
||||
name = cleaned_data.get('name')
|
||||
|
||||
if name:
|
||||
# Проверяем уникальность имени среди активных товаров
|
||||
# Исключаем текущий товар при редактировании (self.instance.pk)
|
||||
existing = Product.objects.filter(
|
||||
name=name,
|
||||
is_deleted=False
|
||||
)
|
||||
|
||||
if self.instance.pk:
|
||||
existing = existing.exclude(pk=self.instance.pk)
|
||||
|
||||
if existing.exists():
|
||||
self.add_error('name',
|
||||
f'Товар с названием "{name}" уже существует. '
|
||||
f'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class ProductKitForm(forms.ModelForm):
|
||||
"""
|
||||
@@ -140,11 +164,29 @@ class ProductKitForm(forms.ModelForm):
|
||||
"""
|
||||
Валидация формы комплекта.
|
||||
Проверяет:
|
||||
1. Что если выбран тип корректировки, указано значение
|
||||
2. Что заполнено максимум одно поле корректировки (увеличение или уменьшение)
|
||||
1. Уникальность имени для активных комплектов
|
||||
2. Что если выбран тип корректировки, указано значение
|
||||
"""
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Проверяем уникальность имени среди активных комплектов
|
||||
name = cleaned_data.get('name')
|
||||
if name:
|
||||
existing = ProductKit.objects.filter(
|
||||
name=name,
|
||||
is_deleted=False,
|
||||
is_temporary=False
|
||||
)
|
||||
|
||||
if self.instance.pk:
|
||||
existing = existing.exclude(pk=self.instance.pk)
|
||||
|
||||
if existing.exists():
|
||||
self.add_error('name',
|
||||
f'Комплект с названием "{name}" уже существует. '
|
||||
f'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
|
||||
adjustment_type = cleaned_data.get('price_adjustment_type')
|
||||
adjustment_value = cleaned_data.get('price_adjustment_value')
|
||||
|
||||
@@ -335,6 +377,29 @@ class ProductCategoryForm(forms.ModelForm):
|
||||
is_active=True
|
||||
).exclude(pk__in=exclude_ids)
|
||||
|
||||
def clean(self):
|
||||
"""Валидация уникальности имени для активных категорий"""
|
||||
cleaned_data = super().clean()
|
||||
name = cleaned_data.get('name')
|
||||
|
||||
if name:
|
||||
# Проверяем уникальность имени среди активных категорий
|
||||
existing = ProductCategory.objects.filter(
|
||||
name=name,
|
||||
is_deleted=False
|
||||
)
|
||||
|
||||
if self.instance.pk:
|
||||
existing = existing.exclude(pk=self.instance.pk)
|
||||
|
||||
if existing.exists():
|
||||
self.add_error('name',
|
||||
f'Категория с названием "{name}" уже существует. '
|
||||
f'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def clean_slug(self):
|
||||
"""Преобразуем пустую строку в None для автогенерации slug"""
|
||||
slug = self.cleaned_data.get('slug')
|
||||
@@ -482,6 +547,30 @@ class ProductTagForm(forms.ModelForm):
|
||||
self.fields['slug'].required = False
|
||||
self.fields['is_active'].widget.attrs.update({'class': 'form-check-input'})
|
||||
|
||||
def clean(self):
|
||||
"""Валидация уникальности имени для активных тегов"""
|
||||
cleaned_data = super().clean()
|
||||
name = cleaned_data.get('name')
|
||||
is_active = cleaned_data.get('is_active', True)
|
||||
|
||||
if name and is_active:
|
||||
# Проверяем уникальность имени среди активных тегов
|
||||
existing = ProductTag.objects.filter(
|
||||
name=name,
|
||||
is_active=True
|
||||
)
|
||||
|
||||
if self.instance.pk:
|
||||
existing = existing.exclude(pk=self.instance.pk)
|
||||
|
||||
if existing.exists():
|
||||
self.add_error('name',
|
||||
f'Тег с названием "{name}" уже существует. '
|
||||
f'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def clean_slug(self):
|
||||
"""Разрешаем пустой slug - он сгенерируется в модели"""
|
||||
slug = self.cleaned_data.get('slug')
|
||||
|
||||
Reference in New Issue
Block a user