# Кастомная конфигурация Django Admin для Hypn0. # Регистрируем модели с удобным интерфейсом. from django import forms from django.contrib import admin from django.forms import Textarea from django.utils.html import format_html from django.http import HttpRequest from .models import ( TbHypn0Item, TbVote, TbBlogPost ) # ============================================================================ # МИКСИНЫ ДЛЯ АДМИНКИ # ============================================================================ class RequestInFormMixin(admin.ModelAdmin): """ Миксин для передачи request объекта в форму. Используется когда форма нуждается в доступе к request для проверки POST параметров или другой информации о текущем HTTP-запросе. Переопределяет get_form() и передает request в __init__ формы через kwargs. """ def get_form(self, request: HttpRequest, obj=None, **kwargs): """ Переопределяем get_form чтобы передать request в форму. Создаем оборачивающий класс который передаст request в __init__. """ FormClass = super().get_form(request, obj, **kwargs) # Сохраняем request в замыкании для доступа в классе request_ref = request class FormWithRequest(FormClass): """Оборачивающий класс, который передает request при инстанцировании""" def __init__(form_instance, *args, **init_kwargs): # Добавляем request в kwargs перед вызовом __init__ родителя init_kwargs['request'] = request_ref super().__init__(*args, **init_kwargs) return FormWithRequest class CodeMirrorFormMixin(forms.ModelForm): """ Миксин для форм с поддержкой CodeMirror редактора. Предоставляет: - Готовый Media класс с CSS и JS для CodeMirror - Helper метод setup_codemirror_field() для конфигурации полей - Базовые атрибуты для активации CodeMirror Использование: class MyForm(CodeMirrorFormMixin): class Meta: model = MyModel fields = (...) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setup_codemirror_field('field_name', 'json', 'codemirror-width-l') """ # ===== MEDIA КЛАСС ДЛЯ CODEMIRROR ===== class Media: """Подключаем CSS и JS для CodeMirror редактора""" css = { 'all': ('codemirror/codemirror-styles.css',) # Стили для CodeMirror } js = ( 'codemirror/editor.js', # Основной CodeMirror 'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной ) # ===== БАЗОВЫЕ АТРИБУТЫ CODEMIRROR ===== CODEMIRROR_ATTRS_BASE = { 'data-codemirror-editor': '1', 'data-width': '100%', # Ширина для патча (100% займет полную ширину) } def setup_codemirror_field(self, field_name: str, language: str = 'text', css_class: str = 'codemirror-width-l'): """ Конфигурирует поле для использования CodeMirror редактором. Применяет Textarea виджет с нужными атрибутами и CSS классами для CodeMirror. Args: field_name: Имя поля в форме (например, 's_label') language: Язык для подсветки синтаксиса (text, json, html, url и т.д.) css_class: CSS класс для управления размерами (codemirror-width-s, codemirror-width-l и т.д.) Пример: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setup_codemirror_field('s_label', 'text', 'codemirror-width-xl codemirror-no-lines') self.setup_codemirror_field('j_metadata', 'json', 'codemirror-width-l') """ # Собираем атрибуты для поля attrs = { **self.CODEMIRROR_ATTRS_BASE, 'data-language': language, 'class': css_class, } # Применяем Textarea виджет с атрибутами self.fields[field_name].widget = Textarea(attrs=attrs) @admin.register(TbHypn0Item) class TbHypn0ItemAdmin(admin.ModelAdmin): """ Панель управления гипно-картинами (модерация, просмотр статистики, редактирование). Создание новых картин через админку заблокировано (создание только на фронтенде). """ list_display = ( "id", "s_hash_id", "title_preview", "i_level", "i_likes_count", "i_claims_count", "i_views_count", "f_score", "is_public", "d_created_at", ) list_display_links = ("id", "s_hash_id", "title_preview") list_filter = ("i_level", "is_public", "d_created_at") search_fields = ("s_hash_id", "s_title", "s_promo_title", "s_promo_url") readonly_fields = ( "id", "s_hash_id", "i_file_size", "i_likes_count", "i_views_count", "i_claims_count", "i_promo_clicks", "d_created_at", "d_updated_at", "svg_preview", ) fieldsets = ( ( "Основная информация", { "fields": ( "id", "s_hash_id", "s_title", "file_svg", "svg_preview", "i_file_size", "is_public", ) }, ), ( "Модерация и Smart Retention", { "fields": ( "i_level", "f_score", ), "description": "Уровень модерации и рейтинг 'гравитации' для ротации и очистки хранилища.", }, ), ( "Метрики популярности", { "fields": ( "i_likes_count", "i_claims_count", "i_views_count", ), }, ), ( "Промо-блок (Спонсоры и Авторы)", { "fields": ( "s_promo_url", "s_promo_title", "i_promo_clicks", ), "classes": ("collapse",), }, ), ( "Метаданные генерации", { "fields": ("j_metadata",), "classes": ("collapse",), }, ), ( "Временные метки", { "fields": ("d_created_at", "d_updated_at"), "classes": ("collapse",), }, ), ) def has_add_permission(self, request): # Создание картин возможно только с фронтенда (требуется обработка растра и согласие пользователя) return False @admin.display(description="Название (HTML)") def title_preview(self, obj): return format_html(obj.s_title) @admin.display(description="Превью SVG") def svg_preview(self, obj): if obj.file_svg: return format_html( '
' '' '
', obj.file_svg.url, ) return "Нет файла" # ---- # АДМИНКА ЛАЙКОВ/ДИСЛАЙКОВ @admin.register(TbVote) class TbVoteAdmin(admin.ModelAdmin): """ Просмотр анонимного журнала голосов и отпечатков (только для чтения). """ list_display = ("id", "k_item", "i_direction", "fingerprint_short", "d_created_at") list_filter = ("i_direction", "d_created_at") search_fields = ("s_fingerprint", "k_item__s_hash_id", "k_item__s_title") readonly_fields = ("id", "k_item", "i_direction", "s_fingerprint", "d_created_at") ordering = ("-d_created_at",) def has_add_permission(self, request): return False def has_change_permission(self, request, obj=None): return False @admin.display(description="Отпечаток (хэш)") def fingerprint_short(self, obj): return f"{obj.s_fingerprint[:12]}..." # ---- # АДМИНКА БЛОГА # Кастомная форма class BlogPostAdminForm(CodeMirrorFormMixin): """ Кастомная форма для админки потов в блог (TbBlogPost). Добавляет виджеты CodeMirror для текстовых полей. """ # Виртуальные поля для настройки типографа etp_enable = forms.BooleanField( label="Включить типограф", initial=False, required=False, help_text="Включить автоматическую типографику для HTML полей (заголовок, тизер, контент)   " ) 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_hyphenation = forms.BooleanField( label="Расставлять переносы", initial=True, required=False, help_text="Расставлять мягкие переносы (­)   
" "в словах длиннее 14 символов   " ) etp_sanitize = forms.BooleanField( label="Очистка HTML", initial=False, required=False, help_text="Удалять весь HTML из исходного текста   " ) etp_hanging_punctuation = forms.BooleanField( label="Висячая пунктуация", initial=False, required=False, help_text="Выносить пунктуацию в начало строк
   " "(только для заголовков... для тизера и контента отключается автоматически)   " ) etp_mode = forms.ChoiceField( label="Режим вывода", choices=[('mixed', 'Смешанный (Mixed)'), ('unicode', 'Юникод (Unicode)'), ('mnemonic', 'Мнемоники')], initial='mixed', required=False, help_text="Формат спецсимволов (например, кавычек, тире, многоточий) в HTML: смешанный, юникод" " или мнемоники   " ) class Meta: model = TbBlogPost fields = ( # Виртуальные поля для настройки типографа 'etp_enable', 'etp_language', 'etp_quotes', 'etp_hyphenation', 'etp_sanitize', 'etp_hanging_punctuation', 'etp_mode', # Остальные поля модели TbArticle 's_title', 'slug', 's_teaser', 's_content', 'f_cover_img', 'is_published', 'd_published_at', ) def __init__(self, *args, **kwargs): """ При инициализации формы подгружаем CodeMirror редактор. Получаем request из kwargs, переданных из get_form_kwargs в AdminClass. """ # Извлекаем request из kwargs если он есть self.request = kwargs.pop('request', None) super().__init__(*args, **kwargs) # Конфигурируем поля для CodeMirror self.setup_codemirror_field('s_title', language='text', css_class='codemirror-width-l codemirror-no-lines') self.setup_codemirror_field('slug', language='text', css_class='codemirror-width-l codemirror-no-lines') self.setup_codemirror_field('s_teaser', language='html', css_class='codemirror-width-xl codemirror-min-height-5') self.setup_codemirror_field('s_content', language='html', css_class='codemirror-width-xl codemirror-min-height-10') @admin.register(TbBlogPost) class TbBlogPostAdmin(RequestInFormMixin, admin.ModelAdmin): """ Управление статьями блога, документацией и инфо-страницами (HTML + etpgrf). """ form = BlogPostAdminForm list_display = ("id", "s_title_display", "slug", "is_published", "d_published_at", "d_created_at") list_display_links = ("id", "s_title_display") list_filter = ("is_published", "d_published_at", "d_created_at") search_fields = ("s_title", "slug", "s_teaser", "s_content") prepopulated_fields = {"slug": ("s_title",)} readonly_fields = ("id", "d_created_at", "d_updated_at") fieldsets = ( ("Атрибуты публикации", { "fields": ("is_published", "slug", "d_published_at", ) }), ("Основные поля", { "fields": ("s_title", "f_cover_img", "s_teaser", "s_content", ), "description": "В полях допускается HTML, поддерживается чистая верстка, настройки типографа etpgrf в" " следующем блоке.", }), ('Типограф', { 'fields': (('etp_enable',), ('etp_language', 'etp_mode'), ('etp_quotes', 'etp_hyphenation', 'etp_sanitize', 'etp_hanging_punctuation')), # 'classes': ('collapse',), 'description': 'Типограф применяется при сохранении и срабатывает на HTML-поля (ЗАГОЛОВОК, ТИЗЕР СТАТЬИ' ' и СТАТЬЯ). Если выключить — HTML будет сохранен без изменений.', }), ("Системные даты", { "fields": ("id", "d_created_at", "d_updated_at", ), "classes": ("collapse",), }), ) @admin.display(description="Заголовок") def s_title_display(self, obj): return format_html(obj.s_title)