From b3d47aca5248acee17a225c54b47929b7cc11d88 Mon Sep 17 00:00:00 2001 From: erjemin Date: Wed, 15 Jul 2026 18:23:50 +0300 Subject: [PATCH] =?UTF-8?q?mod:=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=20=D1=84=D0=BE=D1=80=D0=BC,=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D1=81=D0=B5=D1=80=D0=B0=20=D0=B8=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B5=D0=B9=20(28)=20=D0=BF=D0=BE=D0=B4=D0=B3?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=B2=D0=BA=D0=B0=20=D0=BC=D0=B8=D0=BD=D0=B8?= =?UTF-8?q?=D0=B0=D1=82=D1=8E=D1=80=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=BA=D0=BE=D0=B2=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D0=B2=20=D1=85=D0=B5=D0=BB=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=20render=5Fimage=5Fthumbnail=20=D0=B8=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=20=D0=BA=20ImageMetadataAd?= =?UTF-8?q?min=20=D0=B8=20ArticleAdmin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lpon_site/frontend/admin.py | 103 +++++++++++++++++++++++++----------- 1 file changed, 73 insertions(+), 30 deletions(-) diff --git a/lpon_site/frontend/admin.py b/lpon_site/frontend/admin.py index 11a1cfe..624acb1 100644 --- a/lpon_site/frontend/admin.py +++ b/lpon_site/frontend/admin.py @@ -2,11 +2,13 @@ # Регистрируем модели с удобным интерфейсом. import etpgrf +import logging from typing import Any from django import forms from django.forms import Textarea from django.http import HttpRequest from django.contrib import admin +from django.conf import settings from django.core.exceptions import ValidationError from django.utils.html import format_html, mark_safe from easy_thumbnails.files import get_thumbnailer @@ -17,6 +19,8 @@ from .models import ( from .utils_validators import validate_entity_for_admin_form, generate_admin_save_message from .utils import make_slug +logger = logging.getLogger(__name__) + # ============================================================================ # ЛОКАЛЬНЫЕ ХЕЛПЕРЫ ДЛЯ АДМИНКИ @@ -54,6 +58,61 @@ def get_related_article_description(model_class): f' со всеми SEO-атрибутами и slag, но автоматика несовершенна.
 ') +def render_image_thumbnail(image_field, size=(40, 40), title='миниатюра'): + """ + Универсальный хелпер для отображения миниатюры изображения в админке. + + Используется в ModelAdmin.list_display для показа превью картинок. + Использует easy_thumbnails для автоматического создания и кэширования миниатюр. + Параметры качества и формата берутся из settings (THUMBNAIL_FORMAT, THUMBNAIL_QUALITY). + + Args: + image_field: Объект изображения (FilerImageField или ImageFieldFile) или None + size: Кортеж (ширина, высота) для миниатюры. По умолчанию (40, 40) + title: Описание картинки в alt атрибуте (по умолчанию 'миниатюра') + + Returns: + str: HTML-строка с тегом img или сообщение об ошибке + + Примеры: + # В методе ModelAdmin: + def image_thumbnail(self, obj): + return render_image_thumbnail(obj.image, size=(40, 40), title='Изображение') + """ + if image_field: + try: + # Получаем thumbnailer для картинки через easy_thumbnails + thumbnailer = get_thumbnailer(image_field.file) + + # Генерируем или получаем уже созданную миниатюру + # Параметры берутся из django.conf.settings (THUMBNAIL_FORMAT, THUMBNAIL_QUALITY) + # Это гарантирует единообразие со всеми остальными миниатюрами в проекте + thumbnail = thumbnailer.get_thumbnail({ + 'size': size, # Размер миниатюры + 'crop': 'smart', # Умное обрезание для сохранения центра + 'quality': getattr(settings, 'THUMBNAIL_QUALITY', 80), # Качество из settings (по умолчанию 80) + }) + + # Возвращаем HTML тег img с миниатюрой + # format_html автоматически экранирует опасные символы + width, height = size + return format_html( + '{}', + thumbnail.url, # ← Параметры отдельно + width//2, + height//2, + image_field.name or title + ) + except Exception as e: + # Если ошибка при генерации миниатюры (нет файла, ошибка формата и т.д.) + logger.exception(f"Ошибка при генерации миниатюры изображения: {e}") + return mark_safe('(ошибка)') + + # Если картинка не привязана + return mark_safe('') + + + # ============================================================================ # МИКСИНЫ ДЛЯ АДМИНКИ # ============================================================================ @@ -276,37 +335,12 @@ class ImageMetadataAdmin(admin.ModelAdmin): def image_thumbnail(self, obj): """ Отображает миниатюру картинки (40x40) в списке и в list_display_links. - Использует easy_thumbnails для автоматического создания и кэширования миниатюр. + Использует universal helper render_image_thumbnail(). """ - if obj and obj.image: - try: - # Получаем thumbnailer для картинки через easy_thumbnails - thumbnailer = get_thumbnailer(obj.image.file) - - # Генерируем или получаем уже созданную миниатюру размером 40x40 - thumbnail = thumbnailer.get_thumbnail({ - 'size': (40, 40), # Размер миниатюры: 40x40 пикселей - 'crop': 'smart', # Умное обрезание для сохранения центра картинки - 'quality': 95, # Качество JPEG/WebP (95% для хорошего вида) - }) - - # Возвращаем HTML тег img с миниатюрой - # format_html автоматически экранирует опасные символы - return format_html( - '{}', - thumbnail.url, - obj.image.name or 'картинка' # Alt текст для доступности - ) - except Exception as e: - # Если ошибка при генерации миниатюры (нет файла, ошибка формата и т.д.) - return mark_safe('(ошибка)') - - # Если картинка не привязана - return mark_safe('(нет картинки)') + return render_image_thumbnail(obj.image if obj else None, size=(40, 40), title='картинка') # Установляем название столбца в админке - image_thumbnail.short_description = 'Миниатюра (40x40)' + image_thumbnail.short_description = 'img 40x40' def _display_alt_text(self, obj): """ @@ -1063,8 +1097,8 @@ class ArticleAdmin(RequestInFormMixin, admin.ModelAdmin): """Админ для статей с поддержкой передачи request в форму""" form = ArticleAdminForm # Используем кастомную форму с CodeMirror - list_display = ('id', 's_article_title', 'l_article_type', 'b_article_published', 't_article_created') - list_display_links = ('id', 's_article_title',) + list_display = ('id', 'article_thumbnail', 's_article_title', 'l_article_type', 'b_article_published', 't_article_created') + list_display_links = ('id', 'article_thumbnail', 's_article_title',) list_filter = ('l_article_type', 'b_article_published', 't_article_created') search_fields = ('s_article_title', 'slug') prepopulated_fields = {'slug': ('s_article_title',)} @@ -1112,6 +1146,15 @@ class ArticleAdmin(RequestInFormMixin, admin.ModelAdmin): css = {'all': (*ArticleAdminForm.Media.css['all'], 'css/validation-override.css', )} js = (*ArticleAdminForm.Media.js, 'js/form-field-watcher.js', ) + def article_thumbnail(self, obj): + """ + Отображает миниатюру изображения статьи (40x40) в списке. + Использует universal helper render_image_thumbnail(). + """ + return render_image_thumbnail(obj.k_article_to_image if obj else None, size=(40, 40), title='статья') + + article_thumbnail.short_description = 'img' + def save_model(self, request, obj, form, change): """ Переопределяем save_model для применения типографа.