mod: валидатор форм, парсера и моделей (28) подготовка миниатюр для списков вынесено в хелпер render_image_thumbnail и применен к ImageMetadataAdmin и ArticleAdmin
This commit is contained in:
+73
-30
@@ -2,11 +2,13 @@
|
|||||||
# Регистрируем модели с удобным интерфейсом.
|
# Регистрируем модели с удобным интерфейсом.
|
||||||
|
|
||||||
import etpgrf
|
import etpgrf
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms import Textarea
|
from django.forms import Textarea
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.html import format_html, mark_safe
|
from django.utils.html import format_html, mark_safe
|
||||||
from easy_thumbnails.files import get_thumbnailer
|
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_validators import validate_entity_for_admin_form, generate_admin_save_message
|
||||||
from .utils import make_slug
|
from .utils import make_slug
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# ЛОКАЛЬНЫЕ ХЕЛПЕРЫ ДЛЯ АДМИНКИ
|
# ЛОКАЛЬНЫЕ ХЕЛПЕРЫ ДЛЯ АДМИНКИ
|
||||||
@@ -54,6 +58,61 @@ def get_related_article_description(model_class):
|
|||||||
f' со всеми SEO-атрибутами и slag, но автоматика несовершенна.<br /> ')
|
f' со всеми SEO-атрибутами и slag, но автоматика несовершенна.<br /> ')
|
||||||
|
|
||||||
|
|
||||||
|
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(
|
||||||
|
'<img src="{}" width="{}" height="{}" alt="{}" style="object-fit: cover; "/>',
|
||||||
|
thumbnail.url, # ← Параметры отдельно
|
||||||
|
width//2,
|
||||||
|
height//2,
|
||||||
|
image_field.name or title
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# Если ошибка при генерации миниатюры (нет файла, ошибка формата и т.д.)
|
||||||
|
logger.exception(f"Ошибка при генерации миниатюры изображения: {e}")
|
||||||
|
return mark_safe('<span style="color: #ccc;">(ошибка)</span>')
|
||||||
|
|
||||||
|
# Если картинка не привязана
|
||||||
|
return mark_safe('<img width="20" height="20" alt="" style="background-color: #90909060;"/>')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# МИКСИНЫ ДЛЯ АДМИНКИ
|
# МИКСИНЫ ДЛЯ АДМИНКИ
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -276,37 +335,12 @@ class ImageMetadataAdmin(admin.ModelAdmin):
|
|||||||
def image_thumbnail(self, obj):
|
def image_thumbnail(self, obj):
|
||||||
"""
|
"""
|
||||||
Отображает миниатюру картинки (40x40) в списке и в list_display_links.
|
Отображает миниатюру картинки (40x40) в списке и в list_display_links.
|
||||||
Использует easy_thumbnails для автоматического создания и кэширования миниатюр.
|
Использует universal helper render_image_thumbnail().
|
||||||
"""
|
"""
|
||||||
if obj and obj.image:
|
return render_image_thumbnail(obj.image if obj else None, size=(40, 40), title='картинка')
|
||||||
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(
|
|
||||||
'<img src="{}" width="40" height="40" alt="{}" '
|
|
||||||
'style="border-radius: 4px; object-fit: cover; cursor: pointer;"/>',
|
|
||||||
thumbnail.url,
|
|
||||||
obj.image.name or 'картинка' # Alt текст для доступности
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
# Если ошибка при генерации миниатюры (нет файла, ошибка формата и т.д.)
|
|
||||||
return mark_safe('<span style="color: #ccc;">(ошибка)</span>')
|
|
||||||
|
|
||||||
# Если картинка не привязана
|
|
||||||
return mark_safe('<span style="color: #ccc;">(нет картинки)</span>')
|
|
||||||
|
|
||||||
# Установляем название столбца в админке
|
# Установляем название столбца в админке
|
||||||
image_thumbnail.short_description = 'Миниатюра (40x40)'
|
image_thumbnail.short_description = 'img 40x40'
|
||||||
|
|
||||||
def _display_alt_text(self, obj):
|
def _display_alt_text(self, obj):
|
||||||
"""
|
"""
|
||||||
@@ -1063,8 +1097,8 @@ class ArticleAdmin(RequestInFormMixin, admin.ModelAdmin):
|
|||||||
"""Админ для статей с поддержкой передачи request в форму"""
|
"""Админ для статей с поддержкой передачи request в форму"""
|
||||||
form = ArticleAdminForm # Используем кастомную форму с CodeMirror
|
form = ArticleAdminForm # Используем кастомную форму с CodeMirror
|
||||||
|
|
||||||
list_display = ('id', 's_article_title', 'l_article_type', 'b_article_published', 't_article_created')
|
list_display = ('id', 'article_thumbnail', 's_article_title', 'l_article_type', 'b_article_published', 't_article_created')
|
||||||
list_display_links = ('id', 's_article_title',)
|
list_display_links = ('id', 'article_thumbnail', 's_article_title',)
|
||||||
list_filter = ('l_article_type', 'b_article_published', 't_article_created')
|
list_filter = ('l_article_type', 'b_article_published', 't_article_created')
|
||||||
search_fields = ('s_article_title', 'slug')
|
search_fields = ('s_article_title', 'slug')
|
||||||
prepopulated_fields = {'slug': ('s_article_title',)}
|
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', )}
|
css = {'all': (*ArticleAdminForm.Media.css['all'], 'css/validation-override.css', )}
|
||||||
js = (*ArticleAdminForm.Media.js, 'js/form-field-watcher.js', )
|
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):
|
def save_model(self, request, obj, form, change):
|
||||||
"""
|
"""
|
||||||
Переопределяем save_model для применения типографа.
|
Переопределяем save_model для применения типографа.
|
||||||
|
|||||||
Reference in New Issue
Block a user