-
-
+

Разберём задачу на конкретном примере. У меня есть сайт с цитатами (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)

+

Сейчас, когда у нас есть форма с дополни­тельными полями, нам нужно:

+
    +
  1. Переопре­делить метод save_model в нашем классе админки.
  2. +
  3. Внутри этого метода прочитать значения виртуальных полей из формы (LayoutProcessor, Hyphenator и другие).
  4. +
  5. Инициали­зировать Typographer с этими настройками.
  6. +
  7. Обработать текст из полей szContent, сохранив результат в szContentHTML.
  8. +
+
+    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 их не сериализует и не пытается сохранить в БД, схема данных не меняется, а сами значения живут лишь в момент сохранения и влияют только на обработку текста.

- # Декоратор @admin.display позволяет настроить название колонки и сортировку - @admin.display(description='Заголовок', ordering='title') - def clean_title(self, obj): - # Декодируем HTML-сущности (  -> U+00A0) - return html.unescape(obj.title) -
-

Как это работает?

-
    -
  • Метод clean_title получает объект модели.
  • -
  • html.unescape(obj.title) превращает HTML-мнемоники в Unicode:   в символ неразрывного пробела (U+00A0); ­ — в мягкий перенос (U+00AD) и так далее.
  • -
  • Unicode не маскируется адимнкой Django, символы выглядят как обычный текст (или вообще не видны), поэтому список становится чистым и читаемым (и даже, более того, в списке уже будут типогра­фированные поля title).
  • -
  • При этом сортировка по колонке (ordering='title') продолжает работать по ориги­нальному полю в базе данных.
  • -
-

Теперь админка выглядит опрятно, а типографика на сайте остается безупречной!

-
{% endblock %}