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')
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 5.0.10 on 2025-11-15 10:37
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orders', '0002_initial'),
|
||||
('products', '0002_photoprocessingstatus'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='producttag',
|
||||
name='name',
|
||||
field=models.CharField(max_length=100, verbose_name='Название'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='product',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('is_deleted', False)), fields=('name',), name='unique_active_product_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='productcategory',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('is_deleted', False)), fields=('name',), name='unique_active_category_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='productkit',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('is_deleted', False), ('is_temporary', False)), fields=('name',), name='unique_active_kit_name'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='producttag',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('is_active', True)), fields=('name',), name='unique_active_tag_name'),
|
||||
),
|
||||
]
|
||||
@@ -2,6 +2,7 @@
|
||||
Модели категорий и тегов для товаров и комплектов.
|
||||
"""
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.contrib.auth import get_user_model
|
||||
@@ -49,6 +50,14 @@ class ProductCategory(models.Model):
|
||||
models.Index(fields=['is_deleted']),
|
||||
models.Index(fields=['is_deleted', 'created_at']),
|
||||
]
|
||||
constraints = [
|
||||
# Уникальное имя для активных категорий (исключаем удалённые)
|
||||
models.UniqueConstraint(
|
||||
fields=['name'],
|
||||
condition=Q(is_deleted=False),
|
||||
name='unique_active_category_name'
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
@@ -127,7 +136,7 @@ class ProductTag(models.Model):
|
||||
"""
|
||||
Свободные теги для фильтрации и поиска.
|
||||
"""
|
||||
name = models.CharField(max_length=100, unique=True, verbose_name="Название")
|
||||
name = models.CharField(max_length=100, verbose_name="Название")
|
||||
slug = models.SlugField(max_length=100, unique=True, verbose_name="URL-идентификатор")
|
||||
is_active = models.BooleanField(default=True, verbose_name="Активен", db_index=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания", null=True)
|
||||
@@ -142,6 +151,14 @@ class ProductTag(models.Model):
|
||||
indexes = [
|
||||
models.Index(fields=['is_active']),
|
||||
]
|
||||
constraints = [
|
||||
# Уникальное имя для активных тегов (неактивные могут быть переиспользованы)
|
||||
models.UniqueConstraint(
|
||||
fields=['name'],
|
||||
condition=Q(is_active=True),
|
||||
name='unique_active_tag_name'
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"""
|
||||
from decimal import Decimal
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
@@ -110,6 +111,15 @@ class ProductKit(BaseProductEntity):
|
||||
models.Index(fields=['is_temporary']),
|
||||
models.Index(fields=['order']),
|
||||
]
|
||||
constraints = [
|
||||
# Уникальное имя для активных комплектов (исключаем удалённые)
|
||||
# Примечание: временные комплекты могут иметь дубли имён (создаются для заказов)
|
||||
models.UniqueConstraint(
|
||||
fields=['name'],
|
||||
condition=Q(is_deleted=False, is_temporary=False),
|
||||
name='unique_active_kit_name'
|
||||
),
|
||||
]
|
||||
|
||||
@property
|
||||
def actual_price(self):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Модель Product - базовый товар (цветок, упаковка, аксессуар).
|
||||
"""
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
||||
from .base import BaseProductEntity
|
||||
from .categories import ProductCategory, ProductTag
|
||||
@@ -101,6 +102,14 @@ class Product(BaseProductEntity):
|
||||
models.Index(fields=['in_stock']),
|
||||
models.Index(fields=['sku']),
|
||||
]
|
||||
constraints = [
|
||||
# Уникальное имя для активных товаров (исключаем удалённые)
|
||||
models.UniqueConstraint(
|
||||
fields=['name'],
|
||||
condition=Q(is_deleted=False),
|
||||
name='unique_active_product_name'
|
||||
),
|
||||
]
|
||||
|
||||
@property
|
||||
def actual_price(self):
|
||||
|
||||
@@ -681,6 +681,7 @@ def create_tag_api(request):
|
||||
|
||||
try:
|
||||
import json
|
||||
from django.db import IntegrityError
|
||||
from ..models import ProductTag
|
||||
|
||||
data = json.loads(request.body)
|
||||
@@ -700,12 +701,14 @@ def create_tag_api(request):
|
||||
}, status=400)
|
||||
|
||||
# Проверка уникальности (регистронезависимо)
|
||||
if ProductTag.objects.filter(name__iexact=name).exists():
|
||||
# Примечание: это проверка перед созданием, но race condition все еще возможна
|
||||
if ProductTag.objects.filter(name__iexact=name, is_active=True).exists():
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Тег "{name}" уже существует'
|
||||
}, status=400)
|
||||
|
||||
try:
|
||||
# Создание тега (slug будет сгенерирован автоматически в модели)
|
||||
tag = ProductTag.objects.create(
|
||||
name=name,
|
||||
@@ -723,6 +726,34 @@ def create_tag_api(request):
|
||||
'kits_count': 0
|
||||
}
|
||||
})
|
||||
except IntegrityError as e:
|
||||
# Защита от race condition: если 2 запроса одновременно попытались создать тег
|
||||
error_msg = str(e).lower()
|
||||
if 'unique_active_tag_name' in error_msg or ('name' in error_msg and 'duplicate' in error_msg):
|
||||
# Тег был создан параллельным запросом, получаем его
|
||||
tag = ProductTag.objects.get(name__iexact=name, is_active=True)
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'tag': {
|
||||
'id': tag.id,
|
||||
'name': tag.name,
|
||||
'slug': tag.slug,
|
||||
'is_active': tag.is_active,
|
||||
'products_count': tag.products.count(),
|
||||
'kits_count': tag.kits.count()
|
||||
}
|
||||
})
|
||||
elif 'slug' in error_msg:
|
||||
# Конфликт slug, это редко должно происходить но обработаем
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Тег с названием "{name}" не может быть создан (конфликт идентификатора). Пожалуйста, попробуйте другое название.'
|
||||
}, status=400)
|
||||
else:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Ошибка при создании тега: нарушение уникальности'
|
||||
}, status=500)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({
|
||||
|
||||
@@ -8,6 +8,7 @@ from django.views.generic import ListView, CreateView, DetailView, UpdateView, D
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.shortcuts import redirect
|
||||
from django.db.models import Q
|
||||
from django.db import IntegrityError
|
||||
|
||||
from ..models import ProductCategory, ProductCategoryPhoto
|
||||
from ..forms import ProductCategoryForm
|
||||
@@ -174,6 +175,7 @@ class ProductCategoryCreateView(LoginRequiredMixin, CreateView):
|
||||
success_url = reverse_lazy('products:category-list')
|
||||
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
# Сохраняем категорию
|
||||
self.object = form.save()
|
||||
messages.success(self.request, f'Категория "{self.object.name}" создана успешно.')
|
||||
@@ -186,6 +188,34 @@ class ProductCategoryCreateView(LoginRequiredMixin, CreateView):
|
||||
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
except IntegrityError as e:
|
||||
# Обработка нарушения уникальности
|
||||
error_msg = str(e).lower()
|
||||
if 'unique_active_category_name' in error_msg or ('name' in error_msg and 'duplicate' in error_msg):
|
||||
messages.error(
|
||||
self.request,
|
||||
f'Ошибка: категория с названием "{form.instance.name}" уже существует. '
|
||||
'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
elif 'slug' in error_msg or 'unique' in error_msg:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка: категория с таким названием уже существует. '
|
||||
'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
elif 'sku' in error_msg:
|
||||
messages.error(
|
||||
self.request,
|
||||
f'Ошибка: категория с артикулом "{form.cleaned_data.get("sku", "")}" уже существует. '
|
||||
'Пожалуйста, используйте другой артикул.'
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка при сохранении категории. Пожалуйста, проверьте введённые данные.'
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, 'Пожалуйста, исправьте ошибки в форме.')
|
||||
return super().form_invalid(form)
|
||||
@@ -223,6 +253,7 @@ class ProductCategoryUpdateView(LoginRequiredMixin, UpdateView):
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
# Сохраняем категорию
|
||||
self.object = form.save()
|
||||
messages.success(self.request, f'Категория "{self.object.name}" обновлена успешно.')
|
||||
@@ -235,6 +266,34 @@ class ProductCategoryUpdateView(LoginRequiredMixin, UpdateView):
|
||||
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
except IntegrityError as e:
|
||||
# Обработка нарушения уникальности
|
||||
error_msg = str(e).lower()
|
||||
if 'unique_active_category_name' in error_msg or ('name' in error_msg and 'duplicate' in error_msg):
|
||||
messages.error(
|
||||
self.request,
|
||||
f'Ошибка: категория с названием "{form.instance.name}" уже существует. '
|
||||
'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
elif 'slug' in error_msg or 'unique' in error_msg:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка: категория с таким названием уже существует. '
|
||||
'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
elif 'sku' in error_msg:
|
||||
messages.error(
|
||||
self.request,
|
||||
f'Ошибка: категория с артикулом "{form.cleaned_data.get("sku", "")}" уже существует. '
|
||||
'Пожалуйста, используйте другой артикул.'
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка при сохранении категории. Пожалуйста, проверьте введённые данные.'
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, 'Пожалуйста, исправьте ошибки в форме.')
|
||||
return super().form_invalid(form)
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMix
|
||||
from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
|
||||
from django.urls import reverse_lazy
|
||||
from django.shortcuts import redirect
|
||||
from django.db import transaction
|
||||
from django.db import transaction, IntegrityError
|
||||
|
||||
from ..models import ProductKit, ProductCategory, ProductTag, ProductKitPhoto, Product, ProductVariantGroup
|
||||
from ..forms import ProductKitForm, KitItemFormSetCreate, KitItemFormSetUpdate
|
||||
@@ -210,6 +210,27 @@ class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateVi
|
||||
)
|
||||
|
||||
return redirect('products:productkit-list')
|
||||
except IntegrityError as e:
|
||||
# Обработка нарушения уникальности в БД
|
||||
error_msg = str(e).lower()
|
||||
if 'unique_active_kit_name' in error_msg or ('name' in error_msg and 'duplicate' in error_msg):
|
||||
messages.error(
|
||||
self.request,
|
||||
f'Ошибка: комплект с названием "{form.instance.name}" уже существует. '
|
||||
'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
elif 'slug' in error_msg or 'unique' in error_msg:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка: комплект с таким названием уже существует. '
|
||||
'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка при сохранении комплекта. Пожалуйста, проверьте введённые данные.'
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
except Exception as e:
|
||||
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
|
||||
import traceback
|
||||
@@ -391,6 +412,27 @@ class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateVi
|
||||
return redirect('products:productkit-update', pk=self.object.pk)
|
||||
else:
|
||||
return redirect('products:productkit-list')
|
||||
except IntegrityError as e:
|
||||
# Обработка нарушения уникальности в БД
|
||||
error_msg = str(e).lower()
|
||||
if 'unique_active_kit_name' in error_msg or ('name' in error_msg and 'duplicate' in error_msg):
|
||||
messages.error(
|
||||
self.request,
|
||||
f'Ошибка: комплект с названием "{form.instance.name}" уже существует. '
|
||||
'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
elif 'slug' in error_msg or 'unique' in error_msg:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка: комплект с таким названием уже существует. '
|
||||
'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка при сохранении комплекта. Пожалуйста, проверьте введённые данные.'
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
except Exception as e:
|
||||
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
|
||||
import traceback
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
|
||||
from django.urls import reverse_lazy
|
||||
from django.db.models import Q, Count
|
||||
from django.db import IntegrityError
|
||||
|
||||
from ..models import ProductTag
|
||||
from ..forms import ProductTagForm
|
||||
@@ -85,10 +86,33 @@ class ProductTagCreateView(LoginRequiredMixin, CreateView):
|
||||
success_url = reverse_lazy('products:tag-list')
|
||||
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
response = super().form_valid(form)
|
||||
messages.success(self.request, f'Тег "{self.object.name}" успешно создан.')
|
||||
return response
|
||||
|
||||
except IntegrityError as e:
|
||||
# Обработка нарушения уникальности
|
||||
error_msg = str(e).lower()
|
||||
if 'unique_active_tag_name' in error_msg or ('name' in error_msg and 'duplicate' in error_msg):
|
||||
messages.error(
|
||||
self.request,
|
||||
f'Ошибка: активный тег с названием "{form.instance.name}" уже существует. '
|
||||
'Пожалуйста, используйте другое название или переименуйте существующий тег.'
|
||||
)
|
||||
elif 'slug' in error_msg or 'unique' in error_msg:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка: тег с таким названием уже существует. '
|
||||
'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка при сохранении тега. Пожалуйста, проверьте введённые данные.'
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class ProductTagUpdateView(LoginRequiredMixin, UpdateView):
|
||||
"""Редактирование существующего тега"""
|
||||
@@ -98,10 +122,33 @@ class ProductTagUpdateView(LoginRequiredMixin, UpdateView):
|
||||
success_url = reverse_lazy('products:tag-list')
|
||||
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
response = super().form_valid(form)
|
||||
messages.success(self.request, f'Тег "{self.object.name}" успешно обновлен.')
|
||||
return response
|
||||
|
||||
except IntegrityError as e:
|
||||
# Обработка нарушения уникальности
|
||||
error_msg = str(e).lower()
|
||||
if 'unique_active_tag_name' in error_msg or ('name' in error_msg and 'duplicate' in error_msg):
|
||||
messages.error(
|
||||
self.request,
|
||||
f'Ошибка: активный тег с названием "{form.instance.name}" уже существует. '
|
||||
'Пожалуйста, используйте другое название или переименуйте существующий тег.'
|
||||
)
|
||||
elif 'slug' in error_msg or 'unique' in error_msg:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка: тег с таким названием уже существует. '
|
||||
'Пожалуйста, используйте другое название.'
|
||||
)
|
||||
else:
|
||||
messages.error(
|
||||
self.request,
|
||||
'Ошибка при сохранении тега. Пожалуйста, проверьте введённые данные.'
|
||||
)
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class ProductTagDeleteView(LoginRequiredMixin, DeleteView):
|
||||
"""Удаление тега с подтверждением"""
|
||||
|
||||
Reference in New Issue
Block a user