Files
2018-lpon-site/lpon_site/frontend/admin.py

372 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Кастомная конфигурация Django Admin для LPON сайта.
# Регистрируем модели с удобным интерфейсом.
from django import forms
from django.db import models
from django.forms import TextInput, Textarea, URLField
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-текст для картинки',
}),
label='ALT (новый)',
help_text='Текст для alt-атрибута картинки <tt>&lt;img alt="" .../&gt;</tt>.'
' Будет сохранён в filer_image.default_alt_text'
)
filer_caption = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'placeholder': 'Введите title-описание картинки',
}),
label='TITLE (новый)',
help_text='Текст для title-атрибута картинки <tt>&lt;img title="" .../&gt;</tt>.'
' Будет сохранён в filer_image.default_caption'
)
filer_copyright = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={
'placeholder': 'XXXX, Авторские права на изображение',
}),
label='Copyright',
help_text='Авторские права на изображение (например: <tt>2025, Sergei Erjemin</tt>. Будет сохранён в filer_image.author'
)
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.
"""
# Атрибуты для активации CodeMirror редактора
codemirror_attrs = {
'data-codemirror-editor': '1',
'data-language': 'text',
'data-width': '100%', # Ширина для патча (100% займет полную ширину)
}
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 и заполняем виртуальные поля
# ALT-text
self.fields['filer_alt_text'].initial = filer_image.default_alt_text or ''
self.fields['filer_alt_text'].widget = Textarea(attrs={
'class': 'codemirror-width-m',
**codemirror_attrs,
})
self.fields['filer_caption'].initial = filer_image.default_caption or ''
self.fields['filer_caption'].widget = Textarea(attrs={
'class': 'codemirror-width-m',
**codemirror_attrs,
})
self.fields['filer_copyright'].initial = filer_image.author or ''
self.fields['filer_copyright'].widget = Textarea(attrs={
'class': 'codemirror-width-m',
**codemirror_attrs,
})
except Exception:
# Если ошибка при получении filer_image, просто оставляем пустые значения
pass
# s_img_src_url - поле URL источника (длинная строка)
self.fields['s_img_src_url'].widget = Textarea(attrs={
'class': 'codemirror-width-xl',
**codemirror_attrs,
})
# i_img_sort - поле сортировки (до четырех цифр)
self.fields['i_img_sort'].widget = Textarea(attrs={
'class': 'codemirror-width-s codemirror-no-lines',
**codemirror_attrs,
})
# f_img_confidence_score - поле confidence score (число с плавающей точкой)
self.fields['f_img_confidence_score'].widget = Textarea(attrs={
'class': 'codemirror-width-s codemirror-no-lines',
**codemirror_attrs,
})
class ImageAdmin(admin.ModelAdmin):
"""
Админ для изображений TbImage с поддержкой редактирования метаданных filer_image.
Позволяет пользователю заполнить default_alt_text и default_caption для картинки в filer
прямо в админке TbImage, без необходимости отдельного редактирования filer.
"""
form = TbImageAdminForm # Используем кастомную форму с виртуальными полями
# Подключаем JS через Media (правильный способ!)
class Media:
css = {
'all': ('codemirror/codemirror-styles.css',) # Стили для CodeMirror
}
js = (
'codemirror/editor.js', # Основной CodeMirror
'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной
)
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',
'filer_copyright'),
'description': 'Редактируемые поля для заполнения ALT-, TITLE- и ©-текста в 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), если было заполнено в форме
alt_text = form.cleaned_data.get('filer_alt_text', '').strip()
if alt_text: # Если пользователь что-то ввел
filer_image.default_alt_text = alt_text
# Обновляем caption (TITLE), если было заполнено в форме
caption = form.cleaned_data.get('filer_caption', '').strip()
if caption: # Если пользователь что-то ввел
filer_image.default_caption = caption
# Обновляем author (copyrughight), если было заполнено в форме
author = form.cleaned_data.get('filer_copyright', '').strip()
if author:
filer_image.author = author
# Сохраняем 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)
# ============================================================================