diff --git a/dicquo/web/admin.py b/dicquo/web/admin.py index 01e23f6..28d3ac6 100644 --- a/dicquo/web/admin.py +++ b/dicquo/web/admin.py @@ -1,10 +1,72 @@ # -*- coding: utf-8 -*- from django.contrib import admin +from django import forms from web.models import TbDictumAndQuotes, TbAuthor, TbImages, TbOrigin +try: + from etpgrf.typograph import Typographer + from etpgrf.layout import LayoutProcessor + from etpgrf.hyphenation import Hyphenator +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 + + +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_sanitize = forms.BooleanField( + label="Санитайзер (HTML)", + initial=False, + required=False, + help_text="Очищать HTML теги перед обработкой" + ) + etp_mode = forms.ChoiceField( + label="Режим вывода", + choices=[('mixed', 'Смешанный (Mixed)'), ('unicode', 'Юникод (Unicode)'), ('mnemonic', 'Мнемоники')], + initial='mixed', + required=False, + help_text="Формат спецсимволов" + ) + + class Meta: + model = TbDictumAndQuotes + fields = '__all__' + # Register your models here. class AdmDictumAndQuotesAdmin(admin.ModelAdmin): + form = DictumAdminForm search_fields = ['id', 'szIntro', 'szContent', ] list_display = ('id', 'szIntro', 'szContent', 'tag_list', 'iViewCounter', 'dtEdited', ) list_display_links = ('id', 'szIntro', 'szContent', ) @@ -13,12 +75,109 @@ class AdmDictumAndQuotesAdmin(admin.ModelAdmin): actions_on_top = False actions_on_bottom = True actions_selection_counter = True - # погасить кнопку "Добавить" в интерфейсе админки - # def has_add_permission(self, request): - # return False - # fieldsets = ( - # (None, {'fields': ('szIntro', 'iViewCounter', 'tags',)}), - # ) + + fieldsets = ( + (None, { + 'fields': ('szIntro', 'szContent', 'kAuthor', 'kOrigin', 'kImages', 'tags', 'bIsChecked') + }), + ('Настройки типографа (Etpgrf)', { + 'classes': ('collapse',), + 'fields': ( + ('etp_language', 'etp_mode'), + ('etp_quotes', 'etp_sanitize'), + ('etp_hyphenation', 'etp_hanging_punctuation'), + ), + 'description': 'Настройки применяются при сохранении. Результат записывается в скрытые HTML-поля.' + }), + ('HTML Результат (ReadOnly)', { + 'classes': ('collapse',), + 'fields': ('szIntroHTML', 'szContentHTML'), + }), + ('Служебное', { + 'classes': ('collapse',), + 'fields': ('iViewCounter', 'imFileOG', 'bTypograph') # bTypograph kept for compatibility + }) + ) + readonly_fields = ('szIntroHTML', 'szContentHTML', 'iViewCounter') + + 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. Читаем Sanitizer + sanitizer_enabled = form.cleaned_data.get('etp_sanitize', False) + sanitizer_option = None + if sanitizer_enabled: + sanitizer_option = 'etp' + + # 5. Читаем Hanging Punctuation + hanging_val = form.cleaned_data.get('etp_hanging_punctuation', 'no') + hanging_option = None + if hanging_val != 'no': + hanging_option = hanging_val + + # 6. Собираем общие опции + 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'), + 'sanitizer': sanitizer_option, + } + + # Инициализируем типограф с настройками из формы + 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') + + # Обрабатываем интро + if obj.szIntro: + obj.szIntroHTML = t.process(obj.szIntro) + + except Exception as e: + # Fallback if processing fails + self.message_user(request, f"Ошибка типографа: {e}", level='ERROR') + if not obj.szContentHTML: obj.szContentHTML = obj.szContent + if not obj.szIntroHTML: obj.szIntroHTML = obj.szIntro + + super().save_model(request, obj, form, change) def get_queryset(self, request): return super().get_queryset(request).prefetch_related('tags') @@ -64,3 +223,4 @@ admin.site.register(TbDictumAndQuotes, AdmDictumAndQuotesAdmin) admin.site.register(TbOrigin, AdmOrigin) admin.site.register(TbImages, AdmImages) admin.site.register(TbAuthor, AdmAuthor) + diff --git a/dicquo/web/migrations/0003_alter_tbdictumandquotes_btypograph_and_more.py b/dicquo/web/migrations/0003_alter_tbdictumandquotes_btypograph_and_more.py new file mode 100644 index 0000000..783a9ce --- /dev/null +++ b/dicquo/web/migrations/0003_alter_tbdictumandquotes_btypograph_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 6.0.2 on 2026-02-18 19:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('web', '0002_tbdictumandquotes_bischecked'), + ] + + operations = [ + migrations.AlterField( + model_name='tbdictumandquotes', + name='bTypograph', + field=models.BooleanField(db_index=True, default=True, help_text='Применять типографику?', verbose_name='Типографировать'), + ), + migrations.AlterField( + model_name='tbdictumandquotes', + name='szContent', + field=models.TextField(default='', help_text='Не обязательно.', max_length=640, verbose_name='Изречение'), + ), + migrations.AlterField( + model_name='tbdictumandquotes', + name='szContentHTML', + field=models.TextField(blank=True, default='', help_text='Содержание цитаты, афоризма, высказывания…
Свёрстано в HTML по правилам типографики', verbose_name='Изречение HTML'), + ), + migrations.AlterField( + model_name='tbdictumandquotes', + name='szIntroHTML', + field=models.TextField(blank=True, default='', help_text='Автор и, если необходимо, краткая справка
Вступление перед цитатой, в HTML по правилам типографики', verbose_name='Вступление HTML'), + ), + ] diff --git a/dicquo/web/models.py b/dicquo/web/models.py index 8c3bf4b..79a434a 100644 --- a/dicquo/web/models.py +++ b/dicquo/web/models.py @@ -8,8 +8,6 @@ except ImportError: def en_typus(text): return text def ru_typus(text): return text from pathlib import Path -# import urllib3 -import json import pytils @@ -243,22 +241,11 @@ class TbAuthor(models.Model): return self.__str__() def save(self, *args, **kwargs): - # http = urllib3.PoolManager() - # последовательно - # Используем типограф typus (https://github.com/byashimov/typus) - # Используем типограф Eugene Spearance (http://www.typograf.ru/) - # Используем типограф Муравьева (http://mdash.ru/api.v1.php) - self.szAuthor = ru_typus(self.szAuthor) - # resp = http.request("POST", - # "http://www.typograf.ru/webservice/", - # fields={"text": self.szAuthor.encode('cp1251')}) - # self.szAuthorHTML = resp.data.decode('cp1251') - # # print(self.szContentHTML) - # resp = http.request("POST", - # "http://mdash.ru/api.v1.php", - # fields={"text": self.szAuthorHTML.encode('utf-8')}) - # self.szAuthorHTML = json.loads(resp.data)["result"] - # # print(self.szContentHTML) + # Типографирование перенесено в админку (через библиотеку etpgrf) + # Здесь оставляем только базовое сохранение + if not self.szAuthorHTML and self.szAuthor: + # Если HTML пуст, временно заполняем его оригиналом (или можно вызвать etpgrf с дефолтами) + self.szAuthorHTML = self.szAuthor super(TbAuthor, self).save(*args, **kwargs) class Meta: @@ -294,25 +281,25 @@ class TbDictumAndQuotes(models.Model): blank=True, verbose_name=u"Вступление HTML", help_text=u"Автор и, если необходимо, краткая справка
" - u"Вступление перед цитатой, в HTML по правилам типографики" + u" Вступление перед цитатой, в HTML по правилам типографики" ) szContent = models.TextField( - max_length=256, + max_length=640, default="", - verbose_name=u"Высказывание", - help_text=u"Не обязательно. Вступление перед цитатой." + verbose_name=u"Изречение", + help_text=u"Не обязательно." ) szContentHTML = models.TextField( default="", blank=True, verbose_name=u"Изречение HTML", help_text=u"Содержание цитаты, афоризма, высказывания…
" - u"Свертано в HTML по правилам типографики" + u" Свёрстано в HTML по правилам типографики" ) bTypograph = models.BooleanField( default=True, db_index=True, - verbose_name=u"Типографить", + verbose_name=u"Типографировать", help_text=u"Применять типографику?" ) bIsChecked = models.BooleanField( @@ -399,41 +386,12 @@ class TbDictumAndQuotes(models.Model): return self.__str__() def save(self, *args, **kwargs): - # http = urllib3.PoolManager() - # последовательно - # Используем типограф typus (https://github.com/byashimov/typus) - # Используем типограф Eugene Spearance (http://www.typograf.ru/) - # Используем типограф Муравьева (http://mdash.ru/api.v1.php) - # if self.szIntro != "" and self.szIntro != ru_typus(self.szIntro): - # # сравнение self.szIntro != ru_typus(self.szIntro) нужно для избежания повторных обращений - # # к типографам при обновлении щетчиков просмотра - # self.szIntro = ru_typus(self.szIntro) - # resp = http.request("POST", - # "http://www.typograf.ru/webservice/", - # fields={"text": self.szIntro.replace("\u202f", " ").replace("\u2009", " ").encode('cp1251')}) - # self.szIntroHTML = resp.data.decode('cp1251') - # # print(self.szIntroHTML) - # resp = http.request("POST", - # "http://mdash.ru/api.v1.php", - # fields={"text": self.szIntroHTML.encode('utf-8')}) - # self.szIntroHTML = json.loads(resp.data)["result"] - # # print(self.szIntroHTML) - # else: - # self.szIntroHTML = "" - # if self.szContent != ru_typus(self.szContent): - # # self.szContent != ru_typus(self.szContent) нужно для избежания повторных обращений - # # к типографам при обновлении щетчиков просмотра - # self.szContent = ru_typus(self.szContent) - # resp = http.request("POST", - # "http://www.typograf.ru/webservice/", - # fields={"text": self.szContent.replace("\u202f", " ").replace("\u2009", " ").encode('cp1251')}) - # self.szContentHTML = resp.data.decode('cp1251') - # print(self.szContentHTML) - # resp = http.request("POST", - # "http://mdash.ru/api.v1.php", - # fields={"text": self.szContentHTML.encode('utf-8')}) - # self.szContentHTML = json.loads(resp.data)["result"] - # # print(self.szContentHTML) + # Типографирование (szContent -> szContentHTML, szIntro -> szIntroHTML) + # перенесено в админку для управления параметрами (язык, переносы и т.д.) + if not self.szContentHTML and self.szContent: + self.szContentHTML = self.szContent + if not self.szIntroHTML and self.szIntro: + self.szIntroHTML = self.szIntro super(TbDictumAndQuotes, self).save(*args, **kwargs) class Meta: diff --git a/public/static/css/dicquo.css b/public/static/css/dicquo.css index b093a66..ccd2bde 100644 --- a/public/static/css/dicquo.css +++ b/public/static/css/dicquo.css @@ -205,3 +205,20 @@ header { margin-bottom: 2vh; } } + +/* --- ВЫРАВНИВАНИЕ СИМВОЛОВ ВИСЯЧЕЙ ПУНКТУАЦИИ (Hanging Punctuation) ТИПОГРАФА ETPGRF --- */ +/* --- ЛЕВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по левому краю) --- */ +.etp-laquo { margin-left: -0.44em; } /* « */ +.etp-ldquo, .etp-bdquo { margin-left: -0.4em; } /* “ „ */ +.etp-lsquo { margin-left: -0.22em; } /* ‘ */ +.etp-lpar, .etp-lsqb, .etp-lcub { margin-left: -0.25em; } /* ( [ { */ + +/* --- ПРАВЫЕ ВИСЯЧИЕ СИМВОЛЫ (выравнивание по правому краю) --- */ +/* Общая механика: "вырываем" символ из потока для идеального выравнивания текста */ +[class^="etp-r"], [class*=" etp-r"] { position: absolute; } +/* Точечная настройка смещения для каждого символа */ +.etp-raquo { right: -0.44em; } /* » */ +.etp-rdquo { right: -0.4em; } /* ” */ +.etp-rsquo { right: -0.22em; } /* ’ */ +.etp-rpar, .etp-rsqb, .etp-rcub { right: -0.25em; } /* ) ] } */ +.etp-r-dot, .etp-r-comma, .etp-r-colon { right: -0.15em; } /* . , : */