Как «подружить» Django Admin и типограф etpgrf: Виртуальные поля для настроек
Многие контент-проекты сталкиваются с дилеммой: хочется красивой типографики (правильные кавычки «ёлочки», длинные тире —, неразрывные пробелы ). Самое очевидное решение — переопределить метод 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
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="Формат спецсимволов"
)
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 их не сериализует и не пытается сохранить в БД, схема данных не меняется, а сами значения живут лишь в момент сохранения и влияют только на обработку текста.