Чистые, типографированные заголовки в админке Django: убираем HTML-мнемоники
+Как «подружить» Django Admin и типограф etpgrf: Виртуальные поля для настроек
Библиотека типографа etpgrf написана на Python, и потому его использование в Django — наиболее распространенный случай. Когда в Django вы храните в базе данных типографированный текст (с HTML-мнемониками: неразрывными пробелами , мягкими переносами ­ и тому подобное), то стандартный список в админке Django превращается в не очень удобочитаемую кашу из спецсимволов. Рассказываем, как это исправить за пару строк кода.
Многие контент-проекты сталкиваются с дилеммой: хочется красивой типографики (правильные кавычки «ёлочки», длинные тире —, неразрывные пробелы ). Самое очевидное решение — переопределить метод safe(), но каждый блок текста — заглоаок, тизер, статья — требуют разных настроек. В заголовках хочестя иметь «висячую пунктуацию» и запретить переносы в словах, для основного текста публикации — наоборот, одна публикация на английском и нужны кавычки “лапки”, другая на русском — и нужны «ёлочки».
Хранить все настройки в базе данных (добавляя десятки полей bool в модель) — плохая идея. Это «засоряет» схему данных параметрами отображения, раздувает базу «мусорной информацией» и некрасиво с точки зрения архитектуры.
Решение: Если у вас Django в качестве бэкенда, то наилучший подход — использовать «Виртуальные поля» (Virtual Fields) в Django Admin: Добавить настройки типографа прямо в форму редактирования админки, применить эти настройки при сохранении, и забыть о них, сохранив в базу только готовый, красивый HTML.
++
Разберём задачу на конкретном примере. У меня есть сайт с цитатами (DQ – коллекция цитат. Место для вдумчивого чтения, в нем большое внимание уделяется типографике, а значит при размещении цитат через иметь возможность не только редактировать текст, но и управлять настройками типографа. В проекте есть модель Dictum с полем content (исходный текст) и content_html (типографированный HTML для вывода на сайте). Я хочу, чтобы редактор мог управлять настройками типографа (язык, кавычки, переносы) прямо в админке, не меняя структуру БД.
Инструменты
+-
+
- Django 6.0 (или любая актуальная версия) – «движок». +
- etpgrf — библиотека типографики. +
Реализация
+Идея в том, чтобы создать в админке дополнительные «Виртуальные поля», которых нет в модели. Эти поля будут использоваться только для настройки типографа при сохранении. Например, можно добавить выпадающий список для выбора языка (русский, английский), галочку для включения обработки кавычек, и т. д. При сохранении мы будем читать эти поля, настраивать типограф и сохранять результат в базу.
+Шаг 1. Добавляем необходимые импорты модулей etpgrf-типографа в admin.py
+from django.contrib import admin +# Импортируем классы из нашей библиотеки типографики etpgrf +try: + from etpgrf.typograph import Typographer + from etpgrf.layout import LayoutProcessor + from etpgrf.hyphenation import Hyphenator + from etpgrf.sanitizer import Sanitizer +except ImportError: + # Заглушки на случай, если библиотека не установлена + class Typographer: + def __init__(self, **kwargs): pass + def process(self, text): return text + class LayoutProcessor: + def __init__(self, **kwargs): pass + class Hyphenator: + def __init__(self, **kwargs): pass+
Шаг 2. Создаем кастомную форму
+Вместо стандартной формы админки (в admin.py, мы определим свою, унаследовав её от forms.ModelForm. В ней мы добавим поля, которых нет в модели:
+from django import forms +from .models import TbDictumAndQuotes -Проблема
-Представьте, что у вас есть модель
-Postв которой хранятся записи блога, и вы прогнали заголовки через типограф (например, через наш онлайн-типограф или с помощью библиотеки etpgrf). В базе данных заголовок выглядит так:Политика безопасности и конфи­денциаль­ности-На сайте это будет отлично выглядеть для браузера, но в панели администратора или контент-менеджера это ужасно, так как Django «маскирует»
-&и все HTML-мнемоники будут видны во всей красе. В списке объектов (Change List) будет виден «сырой» HTML-код, и читать такое сложно, особенно если мнемоник много.Решение
-Нам нужно, чтобы в списке (
-list_display) отображался «чистый» текст, а в форме редактирования оставался исходный HTML (чтобы его было легче править).Для этого мы добавим в класс
-ModelAdminспециальный метод, который будет декодировать HTML-мнемоники перед выводом в админку.Шаг 1. Импортируем модуль html
-В Python есть встроенная библиотека
-html, которая умеет делатьunescape— превращать мнемоники, например или­, unicode. превратится в неразрывный пробел, а­в мягкий перенос.Шаг 2. Создаем метод в админке
-В файле
-admin.py:from django.contrib import admin -import html -from .models import Post +class DictumAdminForm(forms.ModelForm): + # Виртуальные поля для настройки типографа + etp_language = forms.ChoiceField( + label="Язык типографики", + choices=[('ru', 'Русский'), ('en', 'English'), ('ru,en', 'Ru + En')], + initial='ru', + required=False + ) + etp_quotes = forms.BooleanField( + label="Обработка кавычек", + initial=True, + required=False, + help_text="Заменять прямые кавычки на «ёлочки» или “лапки”" + ) + etp_hanging_punctuation = forms.ChoiceField( + label="Висячая пунктуация", + choices=[('no', 'Нет'), ('left', 'Слева'), ('right', 'Справа'), ('both', 'Обе стороны')], + initial='left', + required=False, + help_text="Выносить кавычки за границу текстового блока" + ) + etp_hyphenation = forms.BooleanField( + label="Переносы", + initial=True, + required=False, + help_text="Расставлять мягкие переносы (­)" + ) + etp_mode = forms.ChoiceField( + label="Режим вывода", + choices=[('mixed', 'Смешанный (Mixed)'), ('unicode', 'Юникод (Unicode)'), ('mnemonic', 'Мнемоники')], + initial='mixed', + required=False, + help_text="Формат спецсимволов" + ) -@admin.register(Post) -class PostAdmin(admin.ModelAdmin): - # В list_display указываем не поле 'title', а наш метод 'clean_title' - list_display = ('clean_title', 'is_published', 'published_at') + class Meta: + model = TbDictumAndQuotes + fields = '__all__'+Шаг 3. Настраиваем ModelAdmin
+Теперь, там же в admin.py, подключаем эту форму к нашему классу админки. Главный трюк — использовать
+fieldsets, чтобы сгруппировать эти новые поля в отдельный, сворачиваемый блок «Настройки типографа».+class AdmDictumAndQuotesAdmin(admin.ModelAdmin): + form = DictumAdminForm # Подключаем нашу форму + # ... другие настройки админки (list_display, search_fields и т.д.) ... + # ... + # ... - # ... остальные настройки ... + # Группировка "виртуальных полей" типографа в отдельный блок + fieldsets = ( + # Основные поля модели (можно оставить как есть, или сгруппировать по своему усмотрению). + # Поле szContent может быть в этом же блоке, так как мы его как раз типографируем. + # (None, { + # 'fields': ( ... 'szContent', ... ) + # }), + ('Настройки типографа (Etpgrf)', { + 'classes': ('collapse',), + 'fields': ( + ('etp_language', 'etp_mode'), + ('etp_hyphenation', 'etp_hanging_punctuation'), + ), + 'description': 'Настройки применяются при сохранении. Результат записывается в скрытые HTML-поля.' + }), + # ... другие fieldsets, если нужно ... + # ... + # ... Например, можно добавить отдельный блок для отображения результата типографирования (только для чтения) ... + # ('HTML Результат (ReadOnly)', { + # 'classes': ('collapse',), + # 'fields': ('szContentHTML',), + # }), + ) + + # ...+Шаг 4. Перехват сохранения (save_model)
+Сейчас, когда у нас есть форма с дополнительными полями, нам нужно:
++
+- Переопределить метод
+save_modelв нашем классе админки.- Внутри этого метода прочитать значения виртуальных полей из формы (
+LayoutProcessor,Hyphenatorи другие).- Инициализировать
+Typographerс этими настройками.- Обработать текст из полей
+szContent, сохранив результат вszContentHTML.+ def save_model(self, request, obj, form, change): + # 1. Читаем базовые настройки языка и режима из формы + langs = form.cleaned_data.get('etp_language', 'ru').split(',') + + # 2. Собираем LayoutProcessor + layout_option = False + # Включаем layout по умолчанию с базовыми настройками (инициалы, юниты) + layout_option = LayoutProcessor( + langs=langs, + process_initials_and_acronyms=True, + process_units=True + ) + + # 3. Собираем Hyphenator (переносы слов) + hyphenation_enabled = form.cleaned_data.get('etp_hyphenation', True) + hyphenation_option = False + if hyphenation_enabled: + hyphenation_option = Hyphenator( + langs=langs, + max_unhyphenated_len=12 + ) + + # 4. Читаем Hanging Punctuation (висячая пунктуация) + hanging_val = form.cleaned_data.get('etp_hanging_punctuation', 'no') + hanging_option = None + if hanging_val != 'no': + hanging_option = hanging_val + + # 5. Собираем все настройки типографа в словарь + options = { + 'langs': langs, + 'process_html': True, + 'quotes': form.cleaned_data.get('etp_quotes', True), + 'layout': layout_option, + 'unbreakables': True, + 'hyphenation': hyphenation_option, + 'symbols': True, + 'hanging_punctuation': hanging_option, + 'mode': form.cleaned_data.get('etp_mode', 'mixed'), + } + + # Инициализируем типограф с настройками из формы + try: + # DEBUG: Проверка, какой класс используется + if Typographer.__module__ == __name__: # Если класс определен в этом же файле (заглушка) + self.message_user(request, "ВНИМАНИЕ: Используется заглушка Typographer! Библиотека etpgrf не найдена.", level='WARNING') + + t = Typographer(**options) + + # Обрабатываем контент + if obj.szContent: + # В онлайн-типографе используется .process(text) + old_html = obj.szContentHTML or "" + processed = t.process(obj.szContent) + obj.szContentHTML = processed + + # DEBUG: Проверка изменений + if processed != old_html and processed != obj.szContent: + self.message_user(request, f"Типограф: szContentHTML обновлен (len changed: {len(old_html)} -> {len(processed)})", level='INFO') + + + except Exception as e: + # Возникла ошибка при обработке типографом, сохраняем оригинальный текст и показываем сообщение об ошибке + self.message_user(request, f"Ошибка типографа: {e}", level='ERROR') + if not obj.szContentHTML: obj.szContentHTML = obj.szContent + + super().save_model(request, obj, form, change)+Шаг 5. Очистка модели
+Так как логика обработки и сохранения поля
szContentHTML«переехала» в админку, нам нужно убрать логику его записи из методаsave()модели внутри models.py. +Теперь метод
+save()в models.py должен быть максимально простым:+class TbDictumAndQuotes(models.Model): + # ... поля ... + # ... + # ... + + def save(self, *args, **kwargs): + # Типографирование поля szContentHTML перенесено в админку (через admin.save_model). + # Здесь оставляем только базовую подстраховку: если HTML пуст, заполняем оригиналом. + if not self.szContentHTML and self.szContent: + self.szContentHTML = self.szContent + + super(TbDictumAndQuotes, self).save(*args, **kwargs)+Результат
+Теперь редактор видит обычную админку, пишет текст, открывает блок «Настройки типографа», выбирает нужные опции (например, «Включить висячую пунктуацию слева») и нажимает «Сохранить».
+При сохранении Django читает эти настройки, инициализирует типограф, обрабатывает текст и сохраняет в базу уже готовый HTML. В итоге:
+
-
+
- Чистый исходный текст в поле
szContent(для правки в будущем).
+ - Готовый, красивый HTML в поле
szContentHTML(с , висячими кавычками и т. п., который можно сразу выводить на сайте.
+
При этом таблица базы данных не замусорена колонками is_hanging_punctuation_enabled, которые нужны только в момент сохранения. Кроме того, это абсолютно безопасно: «виртуальные поля» существуют только в форме админки и не являются полями модели — Django их не сериализует и не пытается сохранить в БД, схема данных не меняется, а сами значения живут лишь в момент сохранения и влияют только на обработку текста.
Как это работает?
--
-
- Метод
clean_titleполучает объект модели.
- html.unescape(obj.title)превращает HTML-мнемоники в Unicode: в символ неразрывного пробела (U+00A0);­— в мягкий перенос (U+00AD) и так далее.
-- Unicode не маскируется адимнкой Django, символы выглядят как обычный текст (или вообще не видны), поэтому список становится чистым и читаемым (и даже, более того, в списке уже будут типографированные поля
title).
- - При этом сортировка по колонке (
ordering='title') продолжает работать по оригинальному полю в базе данных.
-
Теперь админка выглядит опрятно, а типографика на сайте остается безупречной!
-