add: модели и админка (12) миксины, стили и патч для CodeMirror в админке...
This commit is contained in:
+107
-1
@@ -1,6 +1,112 @@
|
|||||||
|
# Кастомная конфигурация Django Admin для Hypn0.
|
||||||
|
# Регистрируем модели с удобным интерфейсом.
|
||||||
|
|
||||||
|
from django import forms
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.forms import Textarea
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from .models import TbHypn0Item, TbVote, TbBlogPost
|
from .models import (
|
||||||
|
TbHypn0Item,
|
||||||
|
TbVote,
|
||||||
|
TbBlogPost
|
||||||
|
)
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# МИКСИНЫ ДЛЯ АДМИНКИ
|
||||||
|
# ============================================================================
|
||||||
|
class RequestInFormMixin(admin.ModelAdmin):
|
||||||
|
"""
|
||||||
|
Миксин для передачи request объекта в форму.
|
||||||
|
|
||||||
|
Используется когда форма нуждается в доступе к request для проверки POST параметров
|
||||||
|
или другой информации о текущем HTTP-запросе.
|
||||||
|
|
||||||
|
Переопределяет get_form() и передает request в __init__ формы через kwargs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_form(self, request, obj=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Переопределяем get_form чтобы передать request в форму.
|
||||||
|
Создаем оборачивающий класс который передаст request в __init__.
|
||||||
|
"""
|
||||||
|
FormClass = super().get_form(request, obj, **kwargs)
|
||||||
|
|
||||||
|
# Сохраняем request в замыкании для доступа в классе
|
||||||
|
request_ref = request
|
||||||
|
|
||||||
|
class FormWithRequest(FormClass):
|
||||||
|
"""Оборачивающий класс, который передает request при инстанцировании"""
|
||||||
|
def __init__(form_instance, *args, **init_kwargs):
|
||||||
|
# Добавляем request в kwargs перед вызовом __init__ родителя
|
||||||
|
init_kwargs['request'] = request_ref
|
||||||
|
super().__init__(*args, **init_kwargs)
|
||||||
|
|
||||||
|
return FormWithRequest
|
||||||
|
|
||||||
|
|
||||||
|
class CodeMirrorFormMixin(forms.ModelForm):
|
||||||
|
"""
|
||||||
|
Миксин для форм с поддержкой CodeMirror редактора.
|
||||||
|
|
||||||
|
Предоставляет:
|
||||||
|
- Готовый Media класс с CSS и JS для CodeMirror
|
||||||
|
- Helper метод setup_codemirror_field() для конфигурации полей
|
||||||
|
- Базовые атрибуты для активации CodeMirror
|
||||||
|
|
||||||
|
Использование:
|
||||||
|
class MyForm(CodeMirrorFormMixin):
|
||||||
|
class Meta:
|
||||||
|
model = MyModel
|
||||||
|
fields = (...)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.setup_codemirror_field('field_name', 'json', 'codemirror-width-l')
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ===== MEDIA КЛАСС ДЛЯ CODEMIRROR =====
|
||||||
|
class Media:
|
||||||
|
"""Подключаем CSS и JS для CodeMirror редактора"""
|
||||||
|
css = {
|
||||||
|
'all': ('codemirror/codemirror-styles.css',) # Стили для CodeMirror
|
||||||
|
}
|
||||||
|
js = (
|
||||||
|
'codemirror/editor.js', # Основной CodeMirror
|
||||||
|
'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной
|
||||||
|
)
|
||||||
|
|
||||||
|
# ===== БАЗОВЫЕ АТРИБУТЫ CODEMIRROR =====
|
||||||
|
CODEMIRROR_ATTRS_BASE = {
|
||||||
|
'data-codemirror-editor': '1',
|
||||||
|
'data-width': '100%', # Ширина для патча (100% займет полную ширину)
|
||||||
|
}
|
||||||
|
|
||||||
|
def setup_codemirror_field(self, field_name: str, language: str = 'text', css_class: str = 'codemirror-width-l'):
|
||||||
|
"""
|
||||||
|
Конфигурирует поле для использования CodeMirror редактором.
|
||||||
|
|
||||||
|
Применяет Textarea виджет с нужными атрибутами и CSS классами для CodeMirror.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
field_name: Имя поля в форме (например, 's_label')
|
||||||
|
language: Язык для подсветки синтаксиса (text, json, html, url и т.д.)
|
||||||
|
css_class: CSS класс для управления размерами (codemirror-width-s, codemirror-width-l и т.д.)
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.setup_codemirror_field('s_label', 'text', 'codemirror-width-xl codemirror-no-lines')
|
||||||
|
self.setup_codemirror_field('j_metadata', 'json', 'codemirror-width-l')
|
||||||
|
"""
|
||||||
|
# Собираем атрибуты для поля
|
||||||
|
attrs = {
|
||||||
|
**self.CODEMIRROR_ATTRS_BASE,
|
||||||
|
'data-language': language,
|
||||||
|
'class': css_class,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Применяем Textarea виджет с атрибутами
|
||||||
|
self.fields[field_name].widget = Textarea(attrs=attrs)
|
||||||
|
|
||||||
|
|
||||||
@admin.register(TbHypn0Item)
|
@admin.register(TbHypn0Item)
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/* 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; /* Вычитание 13em подобрано. Это ширина бокового меню слева */
|
||||||
|
}
|
||||||
|
|
||||||
|
.codemirror-width-l > .cm-editor {
|
||||||
|
/* Большой (для текста, JSON, HTML) */
|
||||||
|
max-width: calc(75% - 13em) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codemirror-width-xl > .cm-editor {
|
||||||
|
/* Во всю ширину контейнера */
|
||||||
|
max-width: calc(100% - 13em) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ширина редактора зависит от видимости бокового меню Django Admin.
|
||||||
|
На экранах < 767px меню прячется, поэтому вычитание 13em неактуально.
|
||||||
|
Подобрано значение 3ex. */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.codemirror-width-m > .cm-editor,
|
||||||
|
.codemirror-width-l > .cm-editor,
|
||||||
|
.codemirror-width-xl > .cm-editor {
|
||||||
|
max-width: calc(100% - 3ex) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Новый класс для минимальной высоты --- */
|
||||||
|
.codemirror-min-height-2 .cm-editor {
|
||||||
|
min-height: 3.5em; /* 5 строк */
|
||||||
|
}
|
||||||
|
.codemirror-min-height-5 .cm-editor {
|
||||||
|
min-height: 8em; /* 5 строк */
|
||||||
|
}
|
||||||
|
.codemirror-min-height-10 .cm-editor {
|
||||||
|
min-height: 16em; /* 10 строк */
|
||||||
|
}
|
||||||
|
|
||||||
|
.codemirror-no-lines .cm-gutters {
|
||||||
|
/* Скрываем нумерацию строк */
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Скрыть оригинальный textarea */
|
||||||
|
textarea[data-codemirror-editor] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
});
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user