mod: валидатор форм, парсера и моделей (14) все хелперы валидации и автоматического создания статей подключены исполнителям (модель TbArtist, формам ArtistAdminForm и админке ArtistAdmin)

This commit is contained in:
2026-07-01 21:12:27 +03:00
parent 429d2fb417
commit 9522d3fe69
2 changed files with 145 additions and 38 deletions
+102 -31
View File
@@ -4,6 +4,7 @@
from typing import Any from typing import Any
from django import forms from django import forms
from django.forms import Textarea from django.forms import Textarea
from django.http import HttpRequest
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
@@ -336,7 +337,13 @@ class ImageAdmin(admin.ModelAdmin):
_display_title_text.short_description = 'TITLE из filer' _display_title_text.short_description = 'TITLE из filer'
def save_model(self, request, obj, form, change): def save_model(
self,
request: HttpRequest,
obj: TbImage,
form: forms.ModelForm,
change: bool,
) -> None:
""" """
Переопределяем save_model для обновления метаданных filer_image. Переопределяем save_model для обновления метаданных filer_image.
@@ -448,6 +455,9 @@ class ArtistAdminForm(CodeMirrorFormMixin):
""" """
При инициализации формы подгружаем CodeMirror редактор При инициализации формы подгружаем CodeMirror редактор
""" """
# Извлекаем request из kwargs если он есть
self.request = kwargs.pop('request', None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Конфигурируем поля для CodeMirror # Конфигурируем поля для CodeMirror
@@ -456,8 +466,36 @@ class ArtistAdminForm(CodeMirrorFormMixin):
self.setup_codemirror_field('j_artist_metadata', language='json', self.setup_codemirror_field('j_artist_metadata', language='json',
css_class='codemirror-width-l codemirror-min-height-5') css_class='codemirror-width-l codemirror-min-height-5')
def clean(self):
"""
Валидируем форму: проверяем на совпадения (дубликаты) основного поля s_artist.
Используем GET параметр ignore_validate для пропуска валидации при переотправке.
"""
# Получаем очищенные данные формы (может быть None, но обычно это dict)
cleaned_data: dict[str, Any] | None = super().clean()
# Если clean() вернул None, возвращаем пустой dict (для совместимости)
if cleaned_data is None:
cleaned_data = {}
# После проверки выше, очищены данные гарантированно dict[str, Any]
assert isinstance(cleaned_data, dict), "cleaned_data должен быть словарём"
# Используем универсальный хелпер для проверки дубликатов
# Модель берется автоматически из self.Meta.model
# Передаем request для проверки GET параметра ignore_validate
validate_entity_for_admin_form(
self,
cleaned_data,
main_field_name='s_artist',
metadata_field_name='j_artist_metadata',
request=self.request,
)
return cleaned_data
# Админка для TbArtist с кастомной формой ArtistAdminForm # Админка для TbArtist с кастомной формой ArtistAdminForm
class ArtistAdmin(admin.ModelAdmin): class ArtistAdmin(RequestInFormMixin, admin.ModelAdmin):
"""Админ для артистов""" """Админ для артистов"""
form = ArtistAdminForm # Используем кастомную форму с CodeMirror form = ArtistAdminForm # Используем кастомную форму с CodeMirror
@@ -480,6 +518,41 @@ class ArtistAdmin(admin.ModelAdmin):
}), }),
) )
# Дополнительный CSS для валидации и вотчер для отслеживания изменений
class Media:
css = {'all': ('css/validation-override.css', )} # Стили для обхода валидации
js = ('js/form-field-watcher.js', ) # Вотчер для отслеживания изменений полей формы
def save_model(
self,
request: HttpRequest,
obj: TbArtist,
form: forms.ModelForm,
change: bool,
) -> None:
"""
Переопределяем save_model для добавления информативных сообщений в админку.
Максимально поджарый код - все сложности с получением старого значения
и определением типа операции делает хелпер generate_admin_save_message().
"""
# Стандартное сохранение записи через Django
# (в т.ч. автоматическое создание связанной статьи в методе save модели TbLabel)
super().save_model(request, obj, form, change)
# Генерируем и отправляем информативное сообщение об сохранении
# Хелпер сам:
# - определяет тип операции (create vs update)
# - формирует нужное сообщение (success vs warning)
generate_admin_save_message(
request=request,
obj=obj,
is_new=not change, # Django: change=False для новых, True для существующих
related_article=obj.k_artist_to_article,
obj_field_name='s_artist',
article_title_field='s_article_title',
)
# ================ # ================
# АДМИН-ПАНЕЛЬ ДЛЯ ЛЕЙБЛОВ/ИЗДАТЕЛЕЙ # АДМИН-ПАНЕЛЬ ДЛЯ ЛЕЙБЛОВ/ИЗДАТЕЛЕЙ
@@ -544,23 +617,38 @@ class LabelAdmin(RequestInFormMixin, admin.ModelAdmin):
"""Админ для лейблов с поддержкой передачи request в форму""" """Админ для лейблов с поддержкой передачи request в форму"""
form = LabelAdminForm # Используем кастомную форму с CodeMirror form = LabelAdminForm # Используем кастомную форму с CodeMirror
# Дополнительный CSS для валидации и вотчер для отслеживания изменений
class Media:
css = {
'all': (
'css/validation-override.css', # Стили для обхода валидации
)
}
js = (
'js/form-field-watcher.js', # Вотчер для отслеживания изменений полей формы
)
list_display = ('id', 's_label', 't_label_created') list_display = ('id', 's_label', 't_label_created')
list_display_links = ('id', 's_label',) list_display_links = ('id', 's_label',)
search_fields = ('s_label',) search_fields = ('s_label',)
readonly_fields = ('t_label_created', 't_label_updated') readonly_fields = ('t_label_created', 't_label_updated')
def save_model(self, request, obj, form, change): fieldsets = (
('Основные данные о лейбле/издателе', {
'fields': ('s_label', 'j_label_metadata',),
}),
('Связанная публикация', {
'fields': ('k_label_to_article', ),
'description': get_related_article_description(TbLabel),
# 'classes': ('collapse',),
}),
('Служебная информация', {
'fields': ('t_label_created', 't_label_updated'),
'classes': ('collapse',),
}),
)
# Дополнительный CSS для валидации и вотчер для отслеживания изменений
class Media:
css = {'all': ('css/validation-override.css', )} # Стили для обхода валидации
js = ('js/form-field-watcher.js', ) # Вотчер для отслеживания изменений полей формы
def save_model(
self,
request: HttpRequest,
obj: TbLabel,
form: forms.ModelForm,
change: bool,
) -> None:
""" """
Переопределяем save_model для добавления информативных сообщений в админку. Переопределяем save_model для добавления информативных сообщений в админку.
@@ -584,22 +672,6 @@ class LabelAdmin(RequestInFormMixin, admin.ModelAdmin):
article_title_field='s_article_title', article_title_field='s_article_title',
) )
fieldsets = (
('Основные данные о лейбле/издателе', {
'fields': ('s_label', 'j_label_metadata',),
}),
('Связанная публикация', {
'fields': ('k_label_to_article', ),
'description': get_related_article_description(TbLabel),
# 'classes': ('collapse',),
}),
('Служебная информация', {
'fields': ('t_label_created', 't_label_updated'),
'classes': ('collapse',),
}),
)
# ================ # ================
# АДМИН-ПАНЕЛЬ ДЛЯ ПРОДАВЦА/SELLER # АДМИН-ПАНЕЛЬ ДЛЯ ПРОДАВЦА/SELLER
@@ -754,4 +826,3 @@ admin.site.register(TbOfferHistory, OfferHistoryAdmin)
# Кастомизация админ-сайта через ready() в apps.py (переименование через verbose_name) # Кастомизация админ-сайта через ready() в apps.py (переименование через verbose_name)
# ============================================================================ # ============================================================================
+36
View File
@@ -614,6 +614,42 @@ class TbArtist(models.Model):
def __str__(self): def __str__(self):
return f"artist {self.id:0>4}: {self.s_artist}" return f"artist {self.id:0>4}: {self.s_artist}"
def save(self, *args, **kwargs):
"""
Переопределяем save для управления синонимами исполнителей и создания связанной статьи.
При сохранении исполнителя (создание и обновление):
1. Управляем синонимами:
- Для новых исполнителей: добавляем текущий s_artist в SYNONYM
- При изменении s_artist: добавляем как старый, так и новый s_artist в SYNONYM
- При редактировании: используем j_artist_metadata из формы (приоритет админу)
2. Если статья не привязана - создаём новую статью исполнителя автоматически
3. Генерируем технический заголовок и slug для статьи
"""
# ===== ВАЛИДАЦИЯ НА ДУБЛИКАТЫ =====
# Проверяем ДО работы с синонимами и метаданными!
# Страховка: защита от прямого вызова save() минуя админку или (в будущем) парсер
validate_and_raise_for_duplicates(self, 's_artist', 'j_artist_metadata')
# ===== УПРАВЛЕНИЕ СИНОНИМАМИ =====
# Обновляем список синонимов в метаданных (универсальный хелпер для всех моделей)
update_synonyms_in_metadata(self, 's_artist', 'j_artist_metadata')
# ===== СОЗДАНИЕ ИЛИ ПОЛУЧЕНИЕ СВЯЗАННОЙ СТАТЬИ =====
# Используем универсальный хелпер для создания/поиска статьи
# Хелпер сам проверит через обратный FK, не дублирует статьи даже если админ переименовал
article = create_or_get_related_article(
self,
TbArticle.ArticleType.ARTIST,
's_artist',
'j_artist_metadata',
'k_artist_to_article' # ← Явно передаем имя FK поля (избегаем "магии")
)
self.k_artist_to_article = article
# Вызываем оригинальный save родительского класса
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = 'Исполнитель' verbose_name = 'Исполнитель'
verbose_name_plural = 'Исполнители' verbose_name_plural = 'Исполнители'