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:
2025-11-15 13:49:52 +03:00
parent 0b41c6815c
commit 079bd23829
9 changed files with 386 additions and 45 deletions

View File

@@ -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')