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, "private": true,
"type": "module", "type": "module",
"scripts": { "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": { "devDependencies": {
"@babel/runtime": "7.29.7", "@babel/runtime": "7.29.7",
@@ -20,4 +20,4 @@
"@uiw/codemirror-theme-solarized": "4.25.10", "@uiw/codemirror-theme-solarized": "4.25.10",
"esbuild": "0.28.0" "esbuild": "0.28.0"
} }
} }

View File

@@ -2,6 +2,8 @@
# Регистрируем модели с удобным интерфейсом. # Регистрируем модели с удобным интерфейсом.
from django import forms from django import forms
from django.db import models
from django.forms import TextInput, Textarea, URLField
from django.contrib import admin from django.contrib import admin
from django.utils.html import format_html, mark_safe from django.utils.html import format_html, mark_safe
from easy_thumbnails.files import get_thumbnailer from easy_thumbnails.files import get_thumbnailer
@@ -25,8 +27,6 @@ class TbImageAdminForm(forms.ModelForm):
required=False, required=False,
widget=forms.TextInput(attrs={ widget=forms.TextInput(attrs={
'placeholder': 'Введите alt-текст для картинки', 'placeholder': 'Введите alt-текст для картинки',
'class': 'vTextField',
'size': 200,
}), }),
label='ALT (новый)', label='ALT (новый)',
help_text='Текст для alt-атрибута картинки <tt>&lt;img alt="" .../&gt;</tt>.' help_text='Текст для alt-атрибута картинки <tt>&lt;img alt="" .../&gt;</tt>.'
@@ -37,8 +37,6 @@ class TbImageAdminForm(forms.ModelForm):
required=False, required=False,
widget=forms.TextInput(attrs={ widget=forms.TextInput(attrs={
'placeholder': 'Введите title-описание картинки', 'placeholder': 'Введите title-описание картинки',
'class': 'vTextField',
'cols': 120,
}), }),
label='TITLE (новый)', label='TITLE (новый)',
help_text='Текст для title-атрибута картинки <tt>&lt;img title="" .../&gt;</tt>.' help_text='Текст для title-атрибута картинки <tt>&lt;img title="" .../&gt;</tt>.'
@@ -49,8 +47,6 @@ class TbImageAdminForm(forms.ModelForm):
required=False, required=False,
widget=forms.TextInput(attrs={ widget=forms.TextInput(attrs={
'placeholder': 'XXXX, Авторские права на изображение', 'placeholder': 'XXXX, Авторские права на изображение',
'class': 'vTextField',
'cols': 120,
}), }),
label='Copyright', label='Copyright',
help_text='Авторские права на изображение (например: <tt>2025, Sergei Erjemin</tt>. Будет сохранён в filer_image.author' help_text='Авторские права на изображение (например: <tt>2025, Sergei Erjemin</tt>. Будет сохранён в filer_image.author'
@@ -64,6 +60,13 @@ class TbImageAdminForm(forms.ModelForm):
""" """
При инициализации формы подгружаем текущие значения alt/caption из filer_image. При инициализации формы подгружаем текущие значения alt/caption из filer_image.
""" """
# Атрибуты для активации CodeMirror редактора
codemirror_attrs = {
'data-codemirror-editor': '1',
'data-language': 'text',
'data-width': '100%', # Ширина для патча (100% займет полную ширину)
}
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Если редактируем существующую запись, получаем текущие значения из filer # Если редактируем существующую запись, получаем текущие значения из filer
@@ -71,27 +74,70 @@ class TbImageAdminForm(forms.ModelForm):
try: try:
filer_image = self.instance.image filer_image = self.instance.image
# Получаем текущие значения из filer и заполняем виртуальные поля # Получаем текущие значения из filer и заполняем виртуальные поля
# ALT-text
self.fields['filer_alt_text'].initial = filer_image.default_alt_text or '' 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'].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'].initial = filer_image.author or ''
self.fields['filer_copyright'].widget = Textarea(attrs={
'class': 'codemirror-width-m',
**codemirror_attrs,
})
except Exception: except Exception:
# Если ошибка при получении filer_image, просто оставляем пустые значения # Если ошибка при получении filer_image, просто оставляем пустые значения
pass 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): class ImageAdmin(admin.ModelAdmin):
""" """
Админ для изображений TbImage с поддержкой редактирования метаданных filer_image. Админ для изображений TbImage с поддержкой редактирования метаданных filer_image.
Позволяет пользователю заполнить default_alt_text и default_caption для картинки в filer Позволяет пользователю заполнить default_alt_text и default_caption для картинки в filer
прямо в админке TbImage, без необходимости отдельного редактирования фiler. прямо в админке TbImage, без необходимости отдельного редактирования filer.
""" """
form = TbImageAdminForm # Используем кастомную форму с виртуальными полями 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 = ('id', 'image_thumbnail', 'image', '_display_filer_alt_text', 'i_img_sort', 't_img_created')
list_display_links = ('id', 'image_thumbnail', 'image') list_display_links = ('id', 'image_thumbnail', 'image')
list_filter = ('l_img_source', 'l_img_reality', 't_img_created') list_filter = ('l_img_source', 'l_img_reality', 't_img_created')
ordering = ('image', 'i_img_sort') ordering = ('image', 'i_img_sort')
readonly_fields = ('t_img_created', 't_img_updated', '_display_filer_alt_text', '_display_filer_caption') readonly_fields = ('t_img_created', 't_img_updated', '_display_filer_alt_text', '_display_filer_caption')
fieldsets = ( fieldsets = (
('Изображение', { ('Изображение', {
'fields': ('image', 'l_img_source', 'l_img_reality', 's_img_src_url', 'i_img_sort', '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 для картинок)', { ('Метаданные filer (SEO для картинок)', {
'fields': ('_display_filer_alt_text', '_display_filer_caption', 'filer_alt_text', 'filer_caption', 'fields': ('_display_filer_alt_text', '_display_filer_caption', 'filer_alt_text', 'filer_caption',
'filer_copyright'), 'filer_copyright'),
'description': 'Редактируемые поля для заполнения Alt текста и описания в filer. Если не заполнить,' 'description': 'Редактируемые поля для заполнения ALT-, TITLE- и ©-текста в filer. Если не заполнить,'
' текущие значения останутся без изменений (или не будут заполнены при создании).', ' текущие значения останутся без изменений (и не будут заполнены при создании).',
# 'classes': ('collapse',), # 'classes': ('collapse',),
}), }),
('Служебная информация', { ('Служебная информация', {

View File

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