tmp: валидатор форм, парсера и моделей (06) борьба с интерфейсом
This commit is contained in:
@@ -12,6 +12,40 @@ from .models import (
|
|||||||
)
|
)
|
||||||
from .utils import validate_entity_for_admin_form
|
from .utils import validate_entity_for_admin_form
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# МИКСИНЫ ДЛЯ АДМИНКИ
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# АДМИНИСТРИРОВАНИЕ TbImage
|
# АДМИНИСТРИРОВАНИЕ TbImage
|
||||||
#
|
#
|
||||||
@@ -485,19 +519,23 @@ class LabelAdminForm(forms.ModelForm):
|
|||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
# Админ для лейбла (Label)
|
# Админ для лейбла (Label) через миксин
|
||||||
class LabelAdmin(admin.ModelAdmin):
|
class LabelAdmin(RequestInFormMixin, admin.ModelAdmin):
|
||||||
"""Админ для лейблов"""
|
"""Админ для лейблов с поддержкой передачи request в форму"""
|
||||||
form = LabelAdminForm # Используем кастомную форму с виджетами CodeMirror
|
form = LabelAdminForm # Используем кастомную форму с виджетами CodeMirror
|
||||||
|
|
||||||
# Подключаем JS через Media (правильный способ!)
|
# Подключаем JS через Media (правильный способ!)
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
'all': ('codemirror/codemirror-styles.css',) # Стили для CodeMirror
|
'all': (
|
||||||
|
'codemirror/codemirror-styles.css', # Стили для CodeMirror
|
||||||
|
'css/validation-override.css', # Стили для обхода валидации
|
||||||
|
)
|
||||||
}
|
}
|
||||||
js = (
|
js = (
|
||||||
'codemirror/editor.js', # Основной CodeMirror
|
'codemirror/editor.js', # Основной CodeMirror
|
||||||
'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной
|
'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной
|
||||||
|
'js/form-field-watcher.js', # Вотчер для отслеживания изменений полей формы
|
||||||
)
|
)
|
||||||
|
|
||||||
list_display = ('id', 's_label', 't_label_created')
|
list_display = ('id', 's_label', 't_label_created')
|
||||||
@@ -526,26 +564,6 @@ class LabelAdmin(admin.ModelAdmin):
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ================
|
# ================
|
||||||
|
|||||||
@@ -774,7 +774,10 @@ class TbLabel(models.Model):
|
|||||||
|
|
||||||
# ===== УПРАВЛЕНИЕ СИНОНИМАМИ =====
|
# ===== УПРАВЛЕНИЕ СИНОНИМАМИ =====
|
||||||
# Обновляем список синонимов в метаданных (универсальный хелпер для всех моделей)
|
# Обновляем список синонимов в метаданных (универсальный хелпер для всех моделей)
|
||||||
|
print("DEBUG save: ДО update_synonyms_in_metadata")
|
||||||
|
print("DEBUG save: j_label_metadata ДО:", self.j_label_metadata)
|
||||||
update_synonyms_in_metadata(self, 's_label', 'j_label_metadata')
|
update_synonyms_in_metadata(self, 's_label', 'j_label_metadata')
|
||||||
|
print("DEBUG save: j_label_metadata ПОСЛЕ:", self.j_label_metadata)
|
||||||
|
|
||||||
# ===== СОЗДАНИЕ СВЯЗАННОЙ СТАТЬИ =====
|
# ===== СОЗДАНИЕ СВЯЗАННОЙ СТАТЬИ =====
|
||||||
# Если статья не привязана (но может быть пустой из-за blank=True)
|
# Если статья не привязана (но может быть пустой из-за blank=True)
|
||||||
|
|||||||
@@ -334,7 +334,16 @@ def validate_entity_for_admin_form(form_instance, cleaned_data,
|
|||||||
|
|
||||||
# ПЕРЕД ВАЛИДАЦИЕЙ: проверяем, нажата ли submit-кнопка с измененным value='ignore_validate'
|
# ПЕРЕД ВАЛИДАЦИЕЙ: проверяем, нажата ли submit-кнопка с измененным value='ignore_validate'
|
||||||
# Если пользователь нажал нашу кнопку подтверждения, она меняет value админских кнопок на 'ignore_validate'
|
# Если пользователь нажал нашу кнопку подтверждения, она меняет value админских кнопок на 'ignore_validate'
|
||||||
|
print("DEBUG validate: request.POST keys =", list(request.POST.keys()) if request else "NO REQUEST")
|
||||||
|
if request:
|
||||||
|
print("DEBUG validate: _save =", repr(request.POST.get('_save')))
|
||||||
|
print("DEBUG validate: _addanother =", repr(request.POST.get('_addanother')))
|
||||||
|
print("DEBUG validate: _continue =", repr(request.POST.get('_continue')))
|
||||||
|
check = any(request.POST.get(btn) == 'ignore_validate' for btn in ['_save', '_addanother', '_continue'])
|
||||||
|
print("DEBUG validate: check result =", check)
|
||||||
|
|
||||||
if request and any(request.POST.get(btn) == 'ignore_validate' for btn in ['_save', '_addanother', '_continue']):
|
if request and any(request.POST.get(btn) == 'ignore_validate' for btn in ['_save', '_addanother', '_continue']):
|
||||||
|
print("DEBUG validate: ПРОПУСКАЕМ ВАЛИДАЦИЮ")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Получаем класс модели из метаинформации формы
|
# Получаем класс модели из метаинформации формы
|
||||||
@@ -402,47 +411,26 @@ def validate_entity_for_admin_form(form_instance, cleaned_data,
|
|||||||
dup_list = ", ".join(dup_links)
|
dup_list = ", ".join(dup_links)
|
||||||
|
|
||||||
# Кнопка подтверждения создания несмотря на синонимы
|
# Кнопка подтверждения создания несмотря на синонимы
|
||||||
# При клике меняет value всех submit-кнопок на 'ignore_validate' и отправляет форму
|
# При клике добавляет класс force-ignore-validation ко всем submit-кнопкам
|
||||||
# Если пользователь потом меняет данные - вотчер вернет оригинальные значения
|
# Это активирует режим игнорирования валидации
|
||||||
|
# Затем пользователь должен нажать стандартную кнопку сохранения
|
||||||
confirmation_button = '''
|
confirmation_button = '''
|
||||||
<br><br>
|
<div class="confirmation-button-container" style="display: block; margin-top: 15px;">
|
||||||
<button type="button"
|
<br>
|
||||||
onclick="
|
<button type="button"
|
||||||
// Меняем value у всех submit-кнопок на 'ignore_validate'
|
onclick="
|
||||||
document.querySelectorAll('input[type=submit]').forEach(function(btn) {
|
document.querySelectorAll('input[type=submit]').forEach(function(btn) {
|
||||||
btn.value = 'ignore_validate';
|
btn.value = 'ignore_validate';
|
||||||
});
|
btn.classList.add('force-ignore-validation');
|
||||||
// Отправляем форму через первую найденную submit-кнопку
|
|
||||||
document.querySelector('input[type=submit]').click();
|
|
||||||
"
|
|
||||||
style="padding: 10px 15px; background: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
|
|
||||||
Я уверен, создать несмотря на синонимы
|
|
||||||
</button>
|
|
||||||
<em style="display: block; margin-top: 8px; color: #666; font-size: 12px;">
|
|
||||||
Форма будет переотправлена без проверки синонимов
|
|
||||||
</em>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Вотчер: если пользователь меняет данные в форме, отменяем флаг ignore_validate
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Сохраняем оригинальные значения submit-кнопок
|
|
||||||
let originalValues = {};
|
|
||||||
document.querySelectorAll('input[type=submit]').forEach(function(btn) {
|
|
||||||
originalValues[btn.name] = btn.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Отслеживаем изменения всех input/textarea полей в форме
|
|
||||||
let formInputs = document.querySelectorAll('input[type!=submit], textarea, select');
|
|
||||||
formInputs.forEach(function(input) {
|
|
||||||
input.addEventListener('change', function() {
|
|
||||||
// Если пользователь изменил данные, восстанавливаем оригинальные значения кнопок
|
|
||||||
document.querySelectorAll('input[type=submit]').forEach(function(btn) {
|
|
||||||
btn.value = originalValues[btn.name];
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
"
|
||||||
});
|
style="padding: 10px 15px; background: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">
|
||||||
</script>
|
Я уверен, создать несмотря на синонимы
|
||||||
|
</button>
|
||||||
|
<em style="display: block; margin-top: 8px; color: #666; font-size: 12px;">
|
||||||
|
Теперь нажмите кнопку сохранения чтобы создать лейбл
|
||||||
|
</em>
|
||||||
|
</div>
|
||||||
'''
|
'''
|
||||||
|
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
|
|||||||
22
public/static/css/validation-override.css
Normal file
22
public/static/css/validation-override.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* Стили для кнопок при обходе валидации
|
||||||
|
* Отображает пользователю что валидация была обойдена
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Кнопки в режиме обхода валидации (при появлении ошибки синонимов) */
|
||||||
|
input[type=submit].force-ignore-validation {
|
||||||
|
background-color: #f39c12 !important; /* Оранжевый/жёлтый цвет */
|
||||||
|
color: #fff;
|
||||||
|
border: 2px solid #e67e22;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit].force-ignore-validation:hover {
|
||||||
|
background-color: #e67e22 !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Скрываем красную кнопку подтверждения если она есть */
|
||||||
|
.confirmation-button-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
64
public/static/js/form-field-watcher.js
Normal file
64
public/static/js/form-field-watcher.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Вотчер для отслеживания изменений полей формы и сброса состояния submit-кнопок.
|
||||||
|
*
|
||||||
|
* Используется для валидации с поддержкой игнорирования:
|
||||||
|
* 1. Сохраняет оригинальные значения submit-кнопок при загрузке
|
||||||
|
* 2. Отслеживает изменения всех типов полей в форме:
|
||||||
|
* - Стандартные: input (все типы кроме submit), textarea, select
|
||||||
|
* - CodeMirror редакторы (div.codemirror)
|
||||||
|
* - Редактируемое содержимое (contenteditable элементы)
|
||||||
|
* 3. При изменении данных:
|
||||||
|
* - Восстанавливает оригинальные значения submit-кнопок
|
||||||
|
* - Скрывает сообщения об ошибках валидации (.errornote, .errorlist)
|
||||||
|
* 4. Это отменяет флаг 'ignore_validate' если пользователь редактирует данные
|
||||||
|
*
|
||||||
|
* Универсальное решение: работает для любых форм в админке, не только для лейблов.
|
||||||
|
* Селекторы легко расширяются для поддержки других типов полей.
|
||||||
|
*/
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Сохраняем оригинальные значения submit-кнопок администратора
|
||||||
|
let originalValues = {};
|
||||||
|
let submitButtons = document.querySelectorAll('input[type=submit]');
|
||||||
|
|
||||||
|
// Если нет submit-кнопок, выходим (не админская форма)
|
||||||
|
if (submitButtons.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запоминаем оригинальные значения каждой submit-кнопки
|
||||||
|
submitButtons.forEach(function(btn) {
|
||||||
|
originalValues[btn.name] = btn.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Отслеживаем изменения всех типов полей в форме
|
||||||
|
// Селекторы охватывают:
|
||||||
|
// - text input (кроме submit), textarea, select, checkbox, radio и т.д.
|
||||||
|
// - CodeMirror редакторы (div.codemirror)
|
||||||
|
// - contenteditable элементы
|
||||||
|
let formInputs = document.querySelectorAll('input:not([type=submit]), textarea, select, .codemirror, [contenteditable]');
|
||||||
|
|
||||||
|
// Функция которая срабатывает при любом изменении
|
||||||
|
function handleChange() {
|
||||||
|
// При изменении любого поля восстанавливаем оригинальные значения submit-кнопок
|
||||||
|
submitButtons.forEach(function(btn) {
|
||||||
|
btn.value = originalValues[btn.name];
|
||||||
|
// Удаляем класс force-ignore-validation при редактировании
|
||||||
|
btn.classList.remove('force-ignore-validation');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Скрываем сообщения об ошибках валидации
|
||||||
|
let errorNotes = document.querySelectorAll('.errornote, .errorlist');
|
||||||
|
errorNotes.forEach(function(errorElement) {
|
||||||
|
errorElement.style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
formInputs.forEach(function(input) {
|
||||||
|
// Слушаем оба события: 'change' для обычных input/select
|
||||||
|
// и 'input' для CodeMirror и других редакторов
|
||||||
|
input.addEventListener('change', handleChange);
|
||||||
|
input.addEventListener('input', handleChange);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Reference in New Issue
Block a user