mod: админка (01) ImageAdmin (01) +виртуальные поля из filer
This commit is contained in:
@@ -1,23 +1,207 @@
|
|||||||
# Кастомная конфигурация Django Admin для LPON сайта.
|
# Кастомная конфигурация Django Admin для LPON сайта.
|
||||||
# Регистрируем модели с удобным интерфейсом.
|
# Регистрируем модели с удобным интерфейсом.
|
||||||
|
|
||||||
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.models import User, Group
|
from django.utils.html import format_html, mark_safe
|
||||||
|
from easy_thumbnails.files import get_thumbnailer
|
||||||
from .models import (
|
from .models import (
|
||||||
TbImage, TbArticle, TbArtist, TbItem, TbLabel, TbSeller,
|
TbImage, TbArticle, TbArtist, TbItem, TbLabel, TbSeller,
|
||||||
TbOffer, TbSource, TbOfferHistory, TbMusicStyle, TbFormat
|
TbOffer, TbSource, TbOfferHistory, TbMusicStyle, TbFormat
|
||||||
)
|
)
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# ModelAdmin классы для каждой модели
|
# Кастомная форма для админки TbImage
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
class ImageAdmin(admin.ModelAdmin):
|
class TbImageAdminForm(forms.ModelForm):
|
||||||
"""Админ для изображений"""
|
"""
|
||||||
list_display = ('id', 'l_img_source', 'l_img_reality', 'i_img_sort', 't_img_created')
|
Кастомная форма для TbImage в админке.
|
||||||
list_filter = ('l_img_source', 'l_img_reality', 't_img_created')
|
Добавляет виртуальные поля для редактирования метаданных filer_image
|
||||||
ordering = ('-t_img_created', 'i_img_sort')
|
(default_alt_text и default_caption), которые не хранятся в TbImage, но есть в filer_image
|
||||||
readonly_fields = ('t_img_created', 't_img_updated')
|
"""
|
||||||
|
# Виртуальные поля для заполнения метаданных filer_image
|
||||||
|
filer_alt_text = forms.CharField(
|
||||||
|
max_length=255,
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(attrs={
|
||||||
|
'placeholder': 'Введите alt-текст для картинки',
|
||||||
|
'class': 'vTextField',
|
||||||
|
'size': 200,
|
||||||
|
}),
|
||||||
|
label='ALT (новый)',
|
||||||
|
help_text='Текст для alt-атрибута картинки <tt><img alt="" .../></tt>.'
|
||||||
|
' Будет сохранён в filer_image.default_alt_text'
|
||||||
|
)
|
||||||
|
filer_caption = forms.CharField(
|
||||||
|
max_length=255,
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(attrs={
|
||||||
|
'placeholder': 'Введите title-описание картинки',
|
||||||
|
'class': 'vTextField',
|
||||||
|
'cols': 120,
|
||||||
|
}),
|
||||||
|
label='TITLE (новый)',
|
||||||
|
help_text='Текст для title-атрибута картинки <tt><img title="" .../></tt>.'
|
||||||
|
' Будет сохранён в filer_image.default_caption'
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = TbImage
|
||||||
|
fields = ('image', 'l_img_source', 'l_img_reality', 's_img_src_url', 'i_img_sort', 'f_img_confidence_score')
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
При инициализации формы подгружаем текущие значения alt/caption из filer_image.
|
||||||
|
"""
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Если редактируем существующую запись, получаем текущие значения из filer
|
||||||
|
if self.instance and self.instance.pk and hasattr(self.instance, 'image') and self.instance.image:
|
||||||
|
try:
|
||||||
|
filer_image = self.instance.image
|
||||||
|
# Получаем текущие значения из filer и заполняем виртуальные поля
|
||||||
|
self.fields['filer_alt_text'].initial = filer_image.default_alt_text or ''
|
||||||
|
self.fields['filer_caption'].initial = filer_image.default_caption or ''
|
||||||
|
except Exception:
|
||||||
|
# Если ошибка при получении filer_image, просто оставляем пустые значения
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ImageAdmin(admin.ModelAdmin):
|
||||||
|
"""
|
||||||
|
Админ для изображений TbImage с поддержкой редактирования метаданных filer_image.
|
||||||
|
|
||||||
|
Позволяет пользователю заполнить default_alt_text и default_caption для картинки в filer
|
||||||
|
прямо в админке TbImage, без необходимости отдельного редактирования фiler.
|
||||||
|
"""
|
||||||
|
form = TbImageAdminForm # Используем кастомную форму с виртуальными полями
|
||||||
|
list_display = ('id', 'image_thumbnail', 'image', '_display_filer_alt_text', 'i_img_sort', 't_img_created')
|
||||||
|
list_display_links = ('id', 'image_thumbnail', 'image')
|
||||||
|
list_filter = ('l_img_source', 'l_img_reality', 't_img_created')
|
||||||
|
ordering = ('image', 'i_img_sort')
|
||||||
|
readonly_fields = ('t_img_created', 't_img_updated', '_display_filer_alt_text', '_display_filer_caption')
|
||||||
|
fieldsets = (
|
||||||
|
('Изображение', {
|
||||||
|
'fields': ('image', 'l_img_source', 'l_img_reality', 's_img_src_url', 'i_img_sort',
|
||||||
|
'f_img_confidence_score'),
|
||||||
|
'description': 'Основные данные об изображении и источнике',
|
||||||
|
}),
|
||||||
|
('Метаданные filer (SEO для картинок)', {
|
||||||
|
'fields': ('_display_filer_alt_text', '_display_filer_caption', 'filer_alt_text', 'filer_caption'),
|
||||||
|
'description': 'Редактируемые поля для заполнения Alt текста и описания в filer. Если не заполнить,'
|
||||||
|
' текущие значения останутся без изменений (или не будут заполнены при создании).',
|
||||||
|
# 'classes': ('collapse',),
|
||||||
|
}),
|
||||||
|
('Служебная информация', {
|
||||||
|
'fields': ('t_img_created', 't_img_updated'),
|
||||||
|
'classes': ('collapse',),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def image_thumbnail(self, obj):
|
||||||
|
"""
|
||||||
|
Отображает миниатюру картинки (40x40) в списке и в list_display_links.
|
||||||
|
Использует easy_thumbnails для автоматического создания и кэширования миниатюр.
|
||||||
|
"""
|
||||||
|
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(
|
||||||
|
'<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)'
|
||||||
|
|
||||||
|
def _display_filer_alt_text(self, obj):
|
||||||
|
"""
|
||||||
|
Display-метод для отображения текущего alt-текста в filer (read-only).
|
||||||
|
Показывает, какой текст сейчас установлен в filer_image.
|
||||||
|
"""
|
||||||
|
if obj and obj.pk and obj.image:
|
||||||
|
try:
|
||||||
|
current = obj.image.default_alt_text or '(пусто)'
|
||||||
|
return f'ALT: {current}'
|
||||||
|
except Exception:
|
||||||
|
return '(ошибка при получении)'
|
||||||
|
return '(новая запись, значение будет установлено после сохранения)'
|
||||||
|
|
||||||
|
_display_filer_alt_text.short_description = 'ALT из filer'
|
||||||
|
|
||||||
|
def _display_filer_caption(self, obj):
|
||||||
|
"""
|
||||||
|
Display-метод для отображения текущего caption в filer (read-only).
|
||||||
|
Показывает, какой текст сейчас установлен в filer_image.
|
||||||
|
"""
|
||||||
|
if obj and obj.pk and obj.image:
|
||||||
|
try:
|
||||||
|
current = obj.image.default_caption or '(пусто)'
|
||||||
|
return f'TITLE: {current}'
|
||||||
|
except Exception:
|
||||||
|
return '(ошибка при получении)'
|
||||||
|
return '(новая запись, значение будет установлено после сохранения)'
|
||||||
|
|
||||||
|
_display_filer_caption.short_description = 'TITLE из filer'
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
"""
|
||||||
|
Переопределяем save_model для обновления метаданных filer_image.
|
||||||
|
|
||||||
|
Если пользователь заполнил виртуальные поля filer_alt_text или filer_caption,
|
||||||
|
их значения сохраняются в соответствующие поля filer_image.
|
||||||
|
Если поля не заполнены, текущие значения в filer остаются без изменений.
|
||||||
|
"""
|
||||||
|
# Сначала сохраняем саму запись TbImage
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
# Работаем с meta-данными filer только если картинка привязана
|
||||||
|
if obj.image:
|
||||||
|
try:
|
||||||
|
filer_image = obj.image
|
||||||
|
|
||||||
|
# Обновляем alt_text, если было заполнено в форме
|
||||||
|
alt_text = form.cleaned_data.get('filer_alt_text', '').strip()
|
||||||
|
if alt_text: # Если пользователь что-то ввел
|
||||||
|
filer_image.default_alt_text = alt_text
|
||||||
|
|
||||||
|
# Обновляем caption, если было заполнено в форме
|
||||||
|
caption = form.cleaned_data.get('filer_caption', '').strip()
|
||||||
|
if caption: # Если пользователь что-то ввел
|
||||||
|
filer_image.default_caption = caption
|
||||||
|
|
||||||
|
# Сохраняем filer_image с новыми метаданными
|
||||||
|
filer_image.save()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Логируем ошибку, но не прерываем процесс сохранения
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.error(f'Ошибка при сохранении метаданных filer_image: {e}')
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Остальные ModelAdmin классы
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
class ArticleAdmin(admin.ModelAdmin):
|
class ArticleAdmin(admin.ModelAdmin):
|
||||||
"""Админ для статей"""
|
"""Админ для статей"""
|
||||||
|
|||||||
@@ -275,9 +275,9 @@ class TbImage(models.Model):
|
|||||||
|
|
||||||
image = FilerImageField(
|
image = FilerImageField(
|
||||||
# Файл через django_filer
|
# Файл через django_filer
|
||||||
null=True,
|
null=False,
|
||||||
blank=True,
|
blank=False,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.DO_NOTHING,
|
||||||
verbose_name='Файл изображения',
|
verbose_name='Файл изображения',
|
||||||
help_text='Файл изображения, загруженный через django_filer.',
|
help_text='Файл изображения, загруженный через django_filer.',
|
||||||
)
|
)
|
||||||
@@ -299,14 +299,14 @@ class TbImage(models.Model):
|
|||||||
s_img_src_url = models.URLField(
|
s_img_src_url = models.URLField(
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name='URL источника',
|
verbose_name='URL',
|
||||||
help_text='Если изображение взято из внешнего источника (например, Discogs)',
|
help_text='URL источника, если изображение взято (в том числе и парсером) из внешнего источника (например, Discogs)',
|
||||||
)
|
)
|
||||||
i_img_sort = models.IntegerField(
|
i_img_sort = models.IntegerField(
|
||||||
# Порядок (сортировка) вывода
|
# Порядок (сортировка) вывода
|
||||||
default=0,
|
default=0,
|
||||||
db_index=True,
|
db_index=True,
|
||||||
verbose_name='Cортировка',
|
verbose_name='Сортировка',
|
||||||
help_text='Порядок отображения изображений. Чем меньше число, тем выше в списке. Можно использовать'
|
help_text='Порядок отображения изображений. Чем меньше число, тем выше в списке. Можно использовать'
|
||||||
' для указания обложки (0), задника (1) и т.д.',
|
' для указания обложки (0), задника (1) и т.д.',
|
||||||
)
|
)
|
||||||
@@ -315,14 +315,15 @@ class TbImage(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
default=None,
|
default=None,
|
||||||
verbose_name='Уверенность (для автоматических данных)',
|
verbose_name='Достоверность',
|
||||||
help_text='0.0 - 1.0, насколько уверены, что это правильное изображение',
|
help_text='Уверенность (для автоматических данных) 0.0 - 1.0, насколько уверены, что это правильное изображение',
|
||||||
)
|
)
|
||||||
s_img_copyright = models.CharField(
|
s_img_copyright = models.CharField(
|
||||||
# Авторские права и лицензия
|
# Авторские права и лицензия (по идее -- ненужное поле. Можно в filer использовать `obj.image.author`.
|
||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True,
|
blank=True,
|
||||||
default='',
|
default='',
|
||||||
|
editable=False, # Поле не редактируется. Кандидат на удаление.
|
||||||
verbose_name='Авторские права / Лицензия',
|
verbose_name='Авторские права / Лицензия',
|
||||||
help_text='Например: "© 2024 User" или "CC-BY"',
|
help_text='Например: "© 2024 User" или "CC-BY"',
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user