mod: админка (03) ImageAdmin (03) fin (пропатчено, подключили стили)

This commit is contained in:
2026-06-12 00:48:12 +03:00
parent c7049f7d8d
commit fed2a1c5a0
6 changed files with 197 additions and 15 deletions

View File

@@ -5,7 +5,7 @@
"private": true,
"type": "module",
"scripts": {
"build": "esbuild src/editor.js --bundle --format=esm --minify --outfile=${CM6_OUTPUT_DIR:-../public/static/codemirror}/editor.js"
"build": "esbuild src/editor.js --bundle --format=iife --minify --outfile=${CM6_OUTPUT_DIR:-../public/static/codemirror}/editor.js"
},
"devDependencies": {
"@babel/runtime": "7.29.7",
@@ -20,4 +20,4 @@
"@uiw/codemirror-theme-solarized": "4.25.10",
"esbuild": "0.28.0"
}
}
}

View File

@@ -2,6 +2,8 @@
# Регистрируем модели с удобным интерфейсом.
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
@@ -25,8 +27,6 @@ class TbImageAdminForm(forms.ModelForm):
required=False,
widget=forms.TextInput(attrs={
'placeholder': 'Введите alt-текст для картинки',
'class': 'vTextField',
'size': 200,
}),
label='ALT (новый)',
help_text='Текст для alt-атрибута картинки <tt>&lt;img alt="" .../&gt;</tt>.'
@@ -37,8 +37,6 @@ class TbImageAdminForm(forms.ModelForm):
required=False,
widget=forms.TextInput(attrs={
'placeholder': 'Введите title-описание картинки',
'class': 'vTextField',
'cols': 120,
}),
label='TITLE (новый)',
help_text='Текст для title-атрибута картинки <tt>&lt;img title="" .../&gt;</tt>.'
@@ -49,8 +47,6 @@ class TbImageAdminForm(forms.ModelForm):
required=False,
widget=forms.TextInput(attrs={
'placeholder': 'XXXX, Авторские права на изображение',
'class': 'vTextField',
'cols': 120,
}),
label='Copyright',
help_text='Авторские права на изображение (например: <tt>2025, Sergei Erjemin</tt>. Будет сохранён в filer_image.author'
@@ -64,6 +60,13 @@ class TbImageAdminForm(forms.ModelForm):
"""
При инициализации формы подгружаем текущие значения alt/caption из filer_image.
"""
# Атрибуты для активации CodeMirror редактора
codemirror_attrs = {
'data-codemirror-editor': '1',
'data-language': 'text',
'data-width': '100%', # Ширина для патча (100% займет полную ширину)
}
super().__init__(*args, **kwargs)
# Если редактируем существующую запись, получаем текущие значения из filer
@@ -71,27 +74,70 @@ class TbImageAdminForm(forms.ModelForm):
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, без необходимости отдельного редактирования фiler.
прямо в админке 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',
@@ -101,8 +147,8 @@ class ImageAdmin(admin.ModelAdmin):
('Метаданные filer (SEO для картинок)', {
'fields': ('_display_filer_alt_text', '_display_filer_caption', 'filer_alt_text', 'filer_caption',
'filer_copyright'),
'description': 'Редактируемые поля для заполнения Alt текста и описания в filer. Если не заполнить,'
' текущие значения останутся без изменений (или не будут заполнены при создании).',
'description': 'Редактируемые поля для заполнения ALT-, TITLE- и ©-текста в filer. Если не заполнить,'
' текущие значения останутся без изменений (и не будут заполнены при создании).',
# 'classes': ('collapse',),
}),
('Служебная информация', {

View File

@@ -314,9 +314,9 @@ class TbImage(models.Model):
# Доверие данным (для парсеров и API)
null=True,
blank=True,
default=None,
default=10.0,
verbose_name='Достоверность',
help_text='Уверенность (для автоматических данных) 0.0 - 1.0, насколько уверены, что это правильное изображение',
help_text='Уверенность (для автоматических данных) 0.0 - 10.0, насколько уверены, что это правильное изображение',
)
s_img_copyright = models.CharField(
# Авторские права и лицензия (по идее -- ненужное поле. Можно в filer использовать `obj.image.author`.

View File

@@ -0,0 +1,78 @@
/**
* CodeMirror 6 - Height Control Patch
*
* Этот патч перехватывает создание CodeMirror редакторов и копирует
* атрибут data-height с textarea на создаваемый wrapper div.
*
* Использование в admin.py:
* textarea widget: attrs={'data-height': '50px', 'data-codemirror-editor': '1'}
*
* Загружается ПОСЛЕ editor.js через Media в Admin класс.
*/
(function () {
'use strict';
/**
* Применяем высоту к уже созданным wrapper'ам
* (CodeMirror уже загрузился и создал обёртки)
*/
function applyHeightsToExistingWrappers() {
// Находим все textarea'ы с data-codemirror-editor
document.querySelectorAll('textarea[data-codemirror-editor]').forEach((textarea) => {
// Соседний div (wrapper, созданный CodeMirror)
const wrapper = textarea.previousElementSibling;
// Проверяем что это действительно wrapper CodeMirror
if (wrapper && wrapper.classList && wrapper.classList.contains('cm6-editor-wrapper')) {
// Копируем data-height на style если указано
if (textarea.dataset.height) {
wrapper.style.height = textarea.dataset.height;
}
// Копируем data-width на style если указано (для ширины)
if (textarea.dataset.width) {
wrapper.style.width = textarea.dataset.width;
wrapper.style.minWidth = textarea.dataset.width;
// Также устанавливаем ширину на сам .cm-editor внутри wrapper
// Используем !important чтобы переопределить flex: 1 1 auto из CSS
const cmEditor = wrapper.querySelector('.cm-editor');
// if (cmEditor) {
// cmEditor.style.cssText = `width: ${textarea.dataset.width} !important; flex: 0 0 auto;`;
// }
}
// Копируем классы с textarea если они есть
// (например cm6-editor-wrapper-xs, cm6-editor-wrapper-sm и т.д.)
if (textarea.className) {
// Добавляем классы из textarea (сохраняя cm6-editor-wrapper)
textarea.className.split(' ').forEach((cls) => {
if (cls && cls !== 'cm6-editor-wrapper') {
wrapper.classList.add(cls);
}
});
}
}
});
}
// Применяем высоты когда DOM готов
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', applyHeightsToExistingWrappers);
} else {
// DOM уже загружен
applyHeightsToExistingWrappers();
}
// Также применяем для динамически добавленных элементов (если загружают CodeMirror после инициализации)
const observer = new MutationObserver(() => {
applyHeightsToExistingWrappers();
});
observer.observe(document, {
childList: true,
subtree: true,
});
})();

View File

@@ -0,0 +1,58 @@
/* CodeMirror 6 - Стили для правильного отображения */
:root {
color-scheme: light dark;
--brdr: light-dark(#333, #ccc);
--accent-mode-color: light-dark(#222, #eee);
}
.cm6-editor-wrapper {
/* Основной wrapper, созданный CodeMirror */
flex-direction: column;
overflow: hidden;
}
.cm-editor {
/* Сам редактор внутри wrapper - растягиваем на всю доступную высоту */
flex: 1 1 auto;
font-family: monospace;
line-height: 1;
border: 1px solid var(--brdr);;
}
/*
Утилитарные классы для управления шириной.
Мы ищем редактор .cm-editor ВНУТРИ элемента с нашим классом.
*/
.codemirror-width-s > .cm-editor {
/* Маленький (для чисел, коротких ID) */
max-width: 8em !important;
}
.codemirror-width-m .cm-editor {
/* Средний (для URL, коротких строк) */
max-width: calc(50% - 13em) !important;
}
.codemirror-width-l > .cm-editor {
/* Большой (для текста, JSON, HTML) */
max-width: calc(75% - 13em) !important;
}
.codemirror-width-xl > .cm-editor {
/* Во всю ширину контейнера */
max-width: calc(100% - 13em) !important;
}
/* --- Новое правило для скрытия номеров строк --- */
/* Если у обертки есть наш класс, находим внутри панель с номерами и скрываем ее */
.codemirror-no-lines .cm-gutters {
display: none !important;
}
/* Скрыть оригинальный textarea */
textarea[data-codemirror-editor] {
display: none !important;
}

File diff suppressed because one or more lines are too long