diff --git a/lpon_site/frontend/admin.py b/lpon_site/frontend/admin.py
index 8dddced..02fe363 100644
--- a/lpon_site/frontend/admin.py
+++ b/lpon_site/frontend/admin.py
@@ -1,23 +1,207 @@
# Кастомная конфигурация Django Admin для LPON сайта.
# Регистрируем модели с удобным интерфейсом.
+from django import forms
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 (
TbImage, TbArticle, TbArtist, TbItem, TbLabel, TbSeller,
TbOffer, TbSource, TbOfferHistory, TbMusicStyle, TbFormat
)
# ============================================================================
-# ModelAdmin классы для каждой модели
+# Кастомная форма для админки TbImage
# ============================================================================
-class ImageAdmin(admin.ModelAdmin):
- """Админ для изображений"""
- list_display = ('id', 'l_img_source', 'l_img_reality', 'i_img_sort', 't_img_created')
- list_filter = ('l_img_source', 'l_img_reality', 't_img_created')
- ordering = ('-t_img_created', 'i_img_sort')
- readonly_fields = ('t_img_created', 't_img_updated')
+class TbImageAdminForm(forms.ModelForm):
+ """
+ Кастомная форма для TbImage в админке.
+ Добавляет виртуальные поля для редактирования метаданных filer_image
+ (default_alt_text и default_caption), которые не хранятся в TbImage, но есть в filer_image
+ """
+ # Виртуальные поля для заполнения метаданных 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-атрибута картинки <img alt="" .../>.'
+ ' Будет сохранён в 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-атрибута картинки <img title="" .../>.'
+ ' Будет сохранён в 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(
+ '
',
+ thumbnail.url,
+ obj.image.name or 'картинка' # Alt текст для доступности
+ )
+ except Exception as e:
+ # Если ошибка при генерации миниатюры (нет файла, ошибка формата и т.д.)
+ return mark_safe('(ошибка)')
+
+ # Если картинка не привязана
+ return mark_safe('(нет картинки)')
+
+ # Установляем название столбца в админке
+ 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):
"""Админ для статей"""
diff --git a/lpon_site/frontend/models.py b/lpon_site/frontend/models.py
index 260d9e6..ca5f825 100644
--- a/lpon_site/frontend/models.py
+++ b/lpon_site/frontend/models.py
@@ -275,9 +275,9 @@ class TbImage(models.Model):
image = FilerImageField(
# Файл через django_filer
- null=True,
- blank=True,
- on_delete=models.SET_NULL,
+ null=False,
+ blank=False,
+ on_delete=models.DO_NOTHING,
verbose_name='Файл изображения',
help_text='Файл изображения, загруженный через django_filer.',
)
@@ -299,14 +299,14 @@ class TbImage(models.Model):
s_img_src_url = models.URLField(
blank=True,
null=True,
- verbose_name='URL источника',
- help_text='Если изображение взято из внешнего источника (например, Discogs)',
+ verbose_name='URL',
+ help_text='URL источника, если изображение взято (в том числе и парсером) из внешнего источника (например, Discogs)',
)
i_img_sort = models.IntegerField(
# Порядок (сортировка) вывода
default=0,
db_index=True,
- verbose_name='Cортировка',
+ verbose_name='Сортировка',
help_text='Порядок отображения изображений. Чем меньше число, тем выше в списке. Можно использовать'
' для указания обложки (0), задника (1) и т.д.',
)
@@ -315,14 +315,15 @@ class TbImage(models.Model):
null=True,
blank=True,
default=None,
- verbose_name='Уверенность (для автоматических данных)',
- help_text='0.0 - 1.0, насколько уверены, что это правильное изображение',
+ verbose_name='Достоверность',
+ help_text='Уверенность (для автоматических данных) 0.0 - 1.0, насколько уверены, что это правильное изображение',
)
s_img_copyright = models.CharField(
- # Авторские права и лицензия
+ # Авторские права и лицензия (по идее -- ненужное поле. Можно в filer использовать `obj.image.author`.
max_length=255,
blank=True,
default='',
+ editable=False, # Поле не редактируется. Кандидат на удаление.
verbose_name='Авторские права / Лицензия',
help_text='Например: "© 2024 User" или "CC-BY"',
)