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.forms import Textarea
|
||||
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)
|
||||
|
||||
@@ -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