diff --git a/lpon_site/frontend/admin.py b/lpon_site/frontend/admin.py index 783363a..0a77f37 100644 --- a/lpon_site/frontend/admin.py +++ b/lpon_site/frontend/admin.py @@ -12,6 +12,40 @@ from .models import ( ) 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 # @@ -485,19 +519,23 @@ class LabelAdminForm(forms.ModelForm): return cleaned_data -# Админ для лейбла (Label) -class LabelAdmin(admin.ModelAdmin): - """Админ для лейблов""" +# Админ для лейбла (Label) через миксин +class LabelAdmin(RequestInFormMixin, admin.ModelAdmin): + """Админ для лейблов с поддержкой передачи request в форму""" form = LabelAdminForm # Используем кастомную форму с виджетами CodeMirror # Подключаем JS через Media (правильный способ!) class Media: css = { - 'all': ('codemirror/codemirror-styles.css',) # Стили для CodeMirror + 'all': ( + 'codemirror/codemirror-styles.css', # Стили для CodeMirror + 'css/validation-override.css', # Стили для обхода валидации + ) } js = ( 'codemirror/editor.js', # Основной CodeMirror 'codemirror/codemirror-patch.js', # Патч для управления высотой/шириной + 'js/form-field-watcher.js', # Вотчер для отслеживания изменений полей формы ) 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 - - # ================ diff --git a/lpon_site/frontend/models.py b/lpon_site/frontend/models.py index 00866a0..86a9846 100644 --- a/lpon_site/frontend/models.py +++ b/lpon_site/frontend/models.py @@ -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') + print("DEBUG save: j_label_metadata ПОСЛЕ:", self.j_label_metadata) # ===== СОЗДАНИЕ СВЯЗАННОЙ СТАТЬИ ===== # Если статья не привязана (но может быть пустой из-за blank=True) diff --git a/lpon_site/frontend/utils.py b/lpon_site/frontend/utils.py index ea77572..c6153c8 100644 --- a/lpon_site/frontend/utils.py +++ b/lpon_site/frontend/utils.py @@ -334,7 +334,16 @@ def validate_entity_for_admin_form(form_instance, cleaned_data, # ПЕРЕД ВАЛИДАЦИЕЙ: проверяем, нажата ли submit-кнопка с измененным 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']): + print("DEBUG validate: ПРОПУСКАЕМ ВАЛИДАЦИЮ") return # Получаем класс модели из метаинформации формы @@ -402,47 +411,26 @@ def validate_entity_for_admin_form(form_instance, cleaned_data, dup_list = ", ".join(dup_links) # Кнопка подтверждения создания несмотря на синонимы - # При клике меняет value всех submit-кнопок на 'ignore_validate' и отправляет форму - # Если пользователь потом меняет данные - вотчер вернет оригинальные значения + # При клике добавляет класс force-ignore-validation ко всем submit-кнопкам + # Это активирует режим игнорирования валидации + # Затем пользователь должен нажать стандартную кнопку сохранения confirmation_button = ''' -

- - - Форма будет переотправлена без проверки синонимов - - - + " + style="padding: 10px 15px; background: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;"> + Я уверен, создать несмотря на синонимы + + + Теперь нажмите кнопку сохранения чтобы создать лейбл + + ''' raise ValidationError( diff --git a/public/static/css/validation-override.css b/public/static/css/validation-override.css new file mode 100644 index 0000000..f3b9c44 --- /dev/null +++ b/public/static/css/validation-override.css @@ -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; +} + diff --git a/public/static/js/form-field-watcher.js b/public/static/js/form-field-watcher.js new file mode 100644 index 0000000..6756aca --- /dev/null +++ b/public/static/js/form-field-watcher.js @@ -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); + }); +}); +