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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user