2025, Nov 06 12:01
Надежные слаги в Django и Parler: post_save, slugify и unidecode
Разбираем, почему slugify ломает кириллицу в Django с Parler и как сделать стабильные слаги title-pk через post_save и транслитерацию unidecode. Надежно.
Создать аккуратные и предсказуемые слаги в многоязычном проекте Django кажется простым, пока в игру не вступает Parler. Типичный шаблон вроде “title-pk” легко превращается в “-None”, “-9” или вовсе в один числовой хвост, когда заголовки содержат не‑ASCII символы. Ниже — короткий разбор, что именно ломается, как это быстро подтвердить простым дебагом и как исправить проблему надежно, не перекраивая модели.
Настройка и неожиданные симптомы
Модель хранит переведённый title и обычный slug. Наша цель — сформировать slug вида “title-pk”, используя украинский перевод заголовка.
from django.db import models
from django.utils.translation import gettext_lazy as _
from parler.models import TranslatableModel, TranslatedFields
from django.utils.text import slugify
class EventEntry(TranslatableModel):
translations = TranslatedFields(
title=models.CharField(max_length=255, verbose_name=_("Event title")),
body=models.TextField(verbose_name=_("Event body")),
)
slug = models.SlugField(blank=True, max_length=255, verbose_name=_("Event slug"))
def save(self, *args, **kwargs):
if not self.slug:
uk_label = self.safe_translation_getter("title", language_code="uk")
pre_slug = slugify(uk_label)
self.slug = f"{pre_slug}-{self.pk}"
super().save(*args, **kwargs)
На первый взгляд всё выглядело корректно, но система упорно выдавала значения вроде “-None” или “-pk”. Интуиция верная: pk недоступен до первоначальной вставки. Казалось логичным поменять порядок и сначала вызвать super().save(), но даже тогда логи показывали пустую основу, когда заголовок был на кириллице.
import logging
class EventEntry(TranslatableModel):
translations = TranslatedFields(
title=models.CharField(max_length=255, verbose_name=_("Event title")),
body=models.TextField(verbose_name=_("Event body")),
)
slug = models.SlugField(blank=True, max_length=255, verbose_name=_("Event slug"))
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if not self.slug:
uk_label = self.safe_translation_getter("title", language_code="uk")
pre_slug = slugify(uk_label)
logging.debug("[DEBUG] Event pre_slug: %s", pre_slug)
self.slug = f"{pre_slug}-{self.pk}"
logging.debug("[DEBUG] Event slug: %s", self.slug)
super().save(*args, **kwargs)
[DEBUG] Event pre_slug:
[DEBUG] Event slug: -9
Что на самом деле происходит
Тут пересекаются два факта. Во‑первых, если генерировать slug внутри save() до первого сохранения, вы получаете pk = None. Во‑вторых, даже если сперва вызвать super().save(), основа из заголовка может схлопнуться в пустую строку, потому что slugify() не транслитерирует не‑ASCII символы. С кириллическим заголовком slugify() вычистит всё слово, оставив лишь хвост -pk. Поэтому в дебаге основа оказалась пустой, а slug — наподобие “-9”. Отдельный трейс вокруг сигнала показал тот же эффект в другой форме.
[DEBUG] base_slug: тест-11
[DEBUG] instance.slug: 11
Сырая основа включала кириллическое слово и id, но после slugify() буквы исчезли, и выжил только номер.
Рабочее решение: post_save и транслитерация
Надёжный подход — дождаться, пока объект будет сохранён и у него появится первичный ключ, затем перед slugify() транслитерировать исходную строку в ASCII. Здесь связка post_save плюс unidecode решает задачу чисто.
import logging
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.text import slugify
from unidecode import unidecode
@receiver(post_save, sender=EventEntry)
def build_slug_post_save(sender, instance, **kwargs):
if not instance.slug:
uk_label = instance.safe_translation_getter("title", language_code="uk")
if uk_label:
raw = f"{uk_label}-{instance.pk}"
final_slug = slugify(unidecode(raw))[:255]
logging.debug("[DEBUG] instance.slug: %s", final_slug)
instance.slug = final_slug
instance.save(update_fields=["slug"])
Такая последовательность гарантирует валидный pk и превращает кириллицу в безопасную латиницу перед обработкой slugify(), возвращая ожидаемое “test-11” из “тест-11”.
Почему это важно
В многоязычных API генерация слага находится на стыке моделирования данных и семантики URL. Parler откладывает переведённое содержимое в собственное хранилище; первый вызов save() у вашей модели не гарантирует готовность переведённого поля, а slugify() не спасёт не‑ASCII буквы. Без явной транслитерации и правильного жизненного хука ваши слаги тихо деградируют, ломают ожидания и становятся хрупкими при маршрутизации.
Практические рекомендации
Всегда проверяйте, что у вас реально есть в рантайме. Простейшие логи в стиле print сразу покажут, держите ли вы None, пустую строку или уже очищенное после slugify() значение. Генерируйте slug после того, как строка уже появилась в базе и pk определён, и обязательно транслитерируйте перед slugify(), чтобы сохранить смысл из не‑ASCII заголовков. С парой post_save и unidecode формат “title-pk” остаётся стабильным для разных языков.