# Кастомная конфигурация Django Admin для LPON сайта. # Регистрируем модели с удобным интерфейсом. from django import forms from django.contrib import admin 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 ) # ============================================================================ # Кастомная форма для админки TbImage # ============================================================================ 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): """Админ для статей""" list_display = ('id', 's_article_title', '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') prepopulated_fields = {'slug': ('s_article_title',)} readonly_fields = ('t_article_created', 't_article_updated') filter_horizontal = ('k_article_to_styles',) class MusicStyleAdmin(admin.ModelAdmin): """Админ для музыкальных стилей""" list_display = ('id', 's_style_name', 's_style_slug') search_fields = ('s_style_name', 's_style_slug') readonly_fields = ('s_style_slug',) class FormatAdmin(admin.ModelAdmin): """Админ для форматов""" list_display = ('id', 's_format', 's_format_slug') search_fields = ('s_format',) readonly_fields = ('s_format_slug',) class ArtistAdmin(admin.ModelAdmin): """Админ для артистов""" list_display = ('id', 's_artist', 't_artist_created') search_fields = ('s_artist',) readonly_fields = ('t_artist_created', 't_artist_updated') class ItemAdmin(admin.ModelAdmin): """Админ для товаров""" list_display = ('id', 's_item', 't_item_date', 't_item_created') list_filter = ('t_item_date', 't_item_created') search_fields = ('s_item',) filter_horizontal = ('k_item_to_artist',) readonly_fields = ('t_item_created', 't_item_updated') class LabelAdmin(admin.ModelAdmin): """Админ для лейблов""" list_display = ('id', 's_label', 't_label_created') search_fields = ('s_label',) readonly_fields = ('t_label_created', 't_label_updated') class SellerAdmin(admin.ModelAdmin): """Админ для продавцов""" list_display = ('id', 's_seller', 'l_seller_type', 't_seller_created') list_filter = ('l_seller_type',) search_fields = ('s_seller',) readonly_fields = ('t_seller_created', 't_seller_updated') class SourceAdmin(admin.ModelAdmin): """Админ для источников""" list_display = ('id', 's_source_name', 'k_source_to_seller', 'l_source_type', 'l_source_currency', 't_source_data') list_filter = ('l_source_type', 'l_source_currency', 't_source_data') search_fields = ('s_source_name',) readonly_fields = ('t_source_created', 't_source_updated') class OfferAdmin(admin.ModelAdmin): """Админ для предложений""" list_display = ('id', 's_offer', 'k_offer_to_item', 'f_offer_price', 'i_offer_quantity', 'i_offer_views') list_filter = ('l_offer_condition_media', 'l_offer_condition_sleeve', 't_offer_created') search_fields = ('s_offer',) filter_horizontal = ('k_offer_to_format', 'k_offer_to_image') readonly_fields = ('s_offer_skip32', 't_offer_created', 't_offer_updated', 'i_offer_views', 'i_offer_favorites') class OfferHistoryAdmin(admin.ModelAdmin): """Админ для истории изменений офферов""" list_display = ('id', 'k_history_to_offer', 'f_history_price', 'i_history_quantity', 't_history_created') list_filter = ('t_history_created',) readonly_fields = ('t_history_created',) # ============================================================================ # Регистрация моделей в дефолтном admin.site # ============================================================================ admin.site.register(TbImage, ImageAdmin) admin.site.register(TbArticle, ArticleAdmin) admin.site.register(TbMusicStyle, MusicStyleAdmin) admin.site.register(TbFormat, FormatAdmin) admin.site.register(TbArtist, ArtistAdmin) admin.site.register(TbItem, ItemAdmin) admin.site.register(TbLabel, LabelAdmin) admin.site.register(TbSeller, SellerAdmin) admin.site.register(TbSource, SourceAdmin) admin.site.register(TbOffer, OfferAdmin) admin.site.register(TbOfferHistory, OfferHistoryAdmin) # User и Group уже зарегистрированы auth приложением # ============================================================================ # Кастомизация админ-сайта через ready() в apps.py (переименование через verbose_name) # ============================================================================