mod: валидатор форм, парсера и моделей (14) все хелперы валидации и автоматического создания статей подключены исполнителям (модель TbArtist, формам ArtistAdminForm и админке ArtistAdmin)
This commit is contained in:
+109
-38
@@ -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,46 +617,11 @@ 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):
|
|
||||||
"""
|
|
||||||
Переопределяем 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_label_to_article,
|
|
||||||
obj_field_name='s_label',
|
|
||||||
article_title_field='s_article_title',
|
|
||||||
)
|
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Основные данные о лейбле/издателе', {
|
('Основные данные о лейбле/издателе', {
|
||||||
'fields': ('s_label', 'j_label_metadata',),
|
'fields': ('s_label', 'j_label_metadata',),
|
||||||
@@ -599,6 +637,40 @@ class LabelAdmin(RequestInFormMixin, admin.ModelAdmin):
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Дополнительный 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 для добавления информативных сообщений в админку.
|
||||||
|
|
||||||
|
Максимально поджарый код - все сложности с получением старого значения
|
||||||
|
и определением типа операции делает хелпер 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_label_to_article,
|
||||||
|
obj_field_name='s_label',
|
||||||
|
article_title_field='s_article_title',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ================
|
# ================
|
||||||
@@ -754,4 +826,3 @@ admin.site.register(TbOfferHistory, OfferHistoryAdmin)
|
|||||||
# Кастомизация админ-сайта через ready() в apps.py (переименование через verbose_name)
|
# Кастомизация админ-сайта через ready() в apps.py (переименование через verbose_name)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 = 'Исполнители'
|
||||||
|
|||||||
Reference in New Issue
Block a user