""" Сервис для генерации уникальных slug для моделей. Централизует логику транслитерации и обеспечения уникальности. """ from django.utils.text import slugify from unidecode import unidecode class SlugService: """ Статический сервис для генерации уникальных slug. Используется моделями Product, ProductKit, ProductCategory, ProductTag. """ @staticmethod def _get_base_queryset(model_class): """ Получает базовый queryset для модели. Использует all_objects если модель имеет мягкое удаление, иначе objects. Это важно для проверки уникальности - нужно проверять ALL объекты, включая удаленные, иначе могут быть дубли между deleted и active товарами. Args: model_class: Класс модели Returns: QuerySet для проверки уникальности """ # Если модель использует мягкое удаление (все наследники BaseProductEntity), # используем all_objects для проверки ALL записей if hasattr(model_class, 'all_objects'): return model_class.all_objects return model_class.objects @staticmethod def generate_unique_slug(name, model_class, instance_pk=None): """ Генерирует уникальный slug из названия с транслитерацией кириллицы. Args: name (str): Исходное название для генерации slug model_class (Model): Класс модели для проверки уникальности instance_pk (int, optional): ID текущего экземпляра (для исключения при обновлении) Returns: str: Уникальный slug Example: >>> SlugService.generate_unique_slug("Роза красная", Product, None) 'roza-krasnaya' >>> SlugService.generate_unique_slug("Роза красная", Product, None) # если уже существует 'roza-krasnaya-1' """ # Транслитерируем кириллицу в латиницу, затем применяем slugify transliterated_name = unidecode(name) base_slug = slugify(transliterated_name) # Обеспечиваем уникальность slug = base_slug counter = 1 queryset = SlugService._get_base_queryset(model_class) while True: # Проверяем существование slug, исключая текущий экземпляр если это обновление query = queryset.filter(slug=slug) if instance_pk: query = query.exclude(pk=instance_pk) if not query.exists(): break # Если slug занят, добавляем счетчик slug = f"{base_slug}-{counter}" counter += 1 return slug @staticmethod def get_next_available_slug(base_slug, model_class, instance_pk=None, start_counter=1, max_attempts=100): """ Получает следующий доступный slug с числовым суффиксом. Используется при retry обработке IntegrityError. Args: base_slug (str): Базовый slug (например, "test-product") model_class: Класс модели для проверки уникальности instance_pk (int, optional): ID текущего экземпляра start_counter (int): Начальное значение счетчика (по умолчанию 1) max_attempts (int): Максимум попыток (по умолчанию 100) Returns: str: Доступный slug с суффиксом (например, "test-product-1") Raises: ValueError: Если не найден доступный slug после max_attempts попыток """ queryset = SlugService._get_base_queryset(model_class) for counter in range(start_counter, start_counter + max_attempts): slug = f"{base_slug}-{counter}" query = queryset.filter(slug=slug) if instance_pk: query = query.exclude(pk=instance_pk) if not query.exists(): return slug # Если все попытки исчерпаны raise ValueError( f"Не удалось сгенерировать уникальный slug для '{base_slug}' " f"после {max_attempts} попыток" ) @staticmethod def transliterate(text): """ Транслитерирует текст (кириллицу в латиницу). Args: text (str): Текст для транслитерации Returns: str: Транслитерированный текст Example: >>> SlugService.transliterate("Привет мир") 'Privet mir' """ return unidecode(text)