mod: админка (03) ImageAdmin (03) fin (пропатчено, подключили стили)
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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><img alt="" .../></tt>.'
|
help_text='Текст для alt-атрибута картинки <tt><img alt="" .../></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><img title="" .../></tt>.'
|
help_text='Текст для title-атрибута картинки <tt><img title="" .../></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',),
|
||||||
}),
|
}),
|
||||||
('Служебная информация', {
|
('Служебная информация', {
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
78
public/static/codemirror/codemirror-patch.js
Normal file
78
public/static/codemirror/codemirror-patch.js
Normal 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,
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
58
public/static/codemirror/codemirror-styles.css
Normal file
58
public/static/codemirror/codemirror-styles.css
Normal 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
Reference in New Issue
Block a user