monor: валидатор форм, парсера и моделей (18) комментарии

This commit is contained in:
2026-07-07 11:26:47 +03:00
parent 67aad9ab11
commit 70137a6012
2 changed files with 86 additions and 14 deletions
+41 -5
View File
@@ -709,7 +709,7 @@ class LabelAdmin(RequestInFormMixin, admin.ModelAdmin):
# Обогащение Media из формы (CodeMirrorFormMixin) # Обогащение Media из формы (CodeMirrorFormMixin)
# Используем наследование для расширения, а не перекрытия # Используем наследование для расширения, а не перекрытия
# Это гарантирует что все CSS/JS из формы будут загружены # Это гарантирует, что все CSS/JS из формы будут загружены
class Media(LabelAdminForm.Media): class Media(LabelAdminForm.Media):
css = {'all': (*LabelAdminForm.Media.css['all'], 'css/validation-override.css', )} css = {'all': (*LabelAdminForm.Media.css['all'], 'css/validation-override.css', )}
js = (*LabelAdminForm.Media.js, 'js/form-field-watcher.js', ) js = (*LabelAdminForm.Media.js, 'js/form-field-watcher.js', )
@@ -858,7 +858,7 @@ class SourceAdminForm(CodeMirrorFormMixin):
self.setup_codemirror_field('j_source_metadata', language='json', self.setup_codemirror_field('j_source_metadata', language='json',
css_class='codemirror-width-l codemirror-min-height-5') css_class='codemirror-width-l codemirror-min-height-5')
#
class SourceAdmin(admin.ModelAdmin): class SourceAdmin(admin.ModelAdmin):
"""Админ для источников""" """Админ для источников"""
form = SourceAdminForm # Используем кастомную форму с CodeMirror form = SourceAdminForm # Используем кастомную форму с CodeMirror
@@ -871,19 +871,55 @@ class SourceAdmin(admin.ModelAdmin):
# ============================================================================ # ============================================================================
# Остальные ModelAdmin классы # АДМИНКА РЕЛИЗОВ/АЛЬБОМОВ/ТОВАРОВ
# ============================================================================ #
# Кастомная форма
class ItemAdminForm(CodeMirrorFormMixin):
"""
Кастомная форма для админки источников данных (TbItem).
Добавляет виджеты CodeMirror для текстовых полей
"""
class Meta:
model = TbItem
fields = ('id', 's_item', 'k_item_to_artist', 'k_item_to_style', 'k_item_to_article',
's_item_date', 't_item_date', 'i_discogs_master_id', 'j_item_metadata')
def __init__(self, *args, **kwargs):
"""
При инициализации формы подгружаем CodeMirror редактор
"""
# Извлекаем request из kwargs если он есть
self.request = kwargs.pop('request', None)
super().__init__(*args, **kwargs)
# Конфигурируем поля для CodeMirror
self.setup_codemirror_field('s_item', language='text',
css_class='codemirror-width-l codemirror-no-lines')
self.setup_codemirror_field('s_item_date', language='text',
css_class='codemirror-width-s codemirror-no-lines')
self.setup_codemirror_field('i_discogs_master_id', language='text',
css_class='codemirror-width-l codemirror-no-lines')
self.setup_codemirror_field('j_item_metadata', language='json',
css_class='codemirror-width-l codemirror-min-height-5')
class ItemAdmin(admin.ModelAdmin): class ItemAdmin(RequestInFormMixin, admin.ModelAdmin):
"""Админ для товаров""" """Админ для товаров"""
form = ItemAdminForm # Используем кастомную форму с CodeMirror
list_display = ('id', 's_item', 't_item_date', 't_item_created') list_display = ('id', 's_item', 't_item_date', 't_item_created')
list_display_links = ('id', 's_item',)
list_filter = ('t_item_date', 't_item_created') list_filter = ('t_item_date', 't_item_created')
search_fields = ('s_item',) search_fields = ('s_item',)
filter_horizontal = ('k_item_to_artist', 'k_item_to_style') filter_horizontal = ('k_item_to_artist', 'k_item_to_style')
readonly_fields = ('t_item_created', 't_item_updated') readonly_fields = ('t_item_created', 't_item_updated')
# ============================================================================
# Остальные ModelAdmin классы
# ============================================================================
class OfferAdmin(admin.ModelAdmin): class OfferAdmin(admin.ModelAdmin):
"""Админ для предложений""" """Админ для предложений"""
list_display = ('id', 's_offer', 'k_offer_to_item', 'f_offer_price', 'i_offer_quantity', 'i_offer_views') list_display = ('id', 's_offer', 'k_offer_to_item', 'f_offer_price', 'i_offer_quantity', 'i_offer_views')
+45 -9
View File
@@ -557,7 +557,7 @@ class TbMusicStyle(models.Model):
blank=True, blank=True,
verbose_name='Метаданные', verbose_name='Метаданные',
help_text='В основном список синонимов/вариантов названия из Discogs, MusicBrainz и т.д. для матчинга.' help_text='В основном список синонимов/вариантов названия из Discogs, MusicBrainz и т.д. для матчинга.'
' Пример: <tt>{"SYNONYM": ["rock", "Rock Music", "Rock & Roll", "Hard Rock"]}</tt>.', ' Пример: <tt>{"SYN_EN": ["rock", "Rock Music", "Rock & Roll", "Hard Rock"]}</tt>.',
) )
t_style_created = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Дата создания") t_style_created = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Дата создания")
t_style_updated = models.DateTimeField(auto_now=True, editable=False, verbose_name="Дата обновления") t_style_updated = models.DateTimeField(auto_now=True, editable=False, verbose_name="Дата обновления")
@@ -571,8 +571,8 @@ class TbMusicStyle(models.Model):
При сохранении музыкального стиля (создание и обновление): При сохранении музыкального стиля (создание и обновление):
1. Управляем синонимами: 1. Управляем синонимами:
- Для новых музыкальных стилей: добавляем текущий s_style_name в SYNONYM - Для новых музыкальных стилей: добавляем текущий s_style_name в SYN_EN
- При изменении s_style_name: добавляем как старый, так и новый s_style_name в SYNONYM - При изменении s_style_name: добавляем как старый, так и новый s_style_name в SYN_EN
- При редактировании: используем j_style_metadata из формы (приоритет админу) - При редактировании: используем j_style_metadata из формы (приоритет админу)
2. Если статья не привязана - создаём новую статью музыкального стиля автоматически 2. Если статья не привязана - создаём новую статью музыкального стиля автоматически
3. Генерируем технический заголовок и slug для статьи 3. Генерируем технический заголовок и slug для статьи
@@ -641,7 +641,7 @@ class TbArtist(models.Model):
null=True, null=True,
verbose_name='Метаданные JSON', verbose_name='Метаданные JSON',
help_text='В основном список синонимов/вариантов названия из Discogs, MusicBrainz и т.д. для матчинга.' help_text='В основном список синонимов/вариантов названия из Discogs, MusicBrainz и т.д. для матчинга.'
' Пример: <tt>{"SYNONYM": ["The Beatles", "Beatles", "Beatles, The"]}</tt>.', ' Пример: <tt>{"SYN_EN": ["The Beatles", "Beatles", "Beatles, The"]}</tt>.',
) )
t_artist_created = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Дата создания",) t_artist_created = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Дата создания",)
t_artist_updated = models.DateTimeField(auto_now=True, editable=False, verbose_name="Дата обновления", t_artist_updated = models.DateTimeField(auto_now=True, editable=False, verbose_name="Дата обновления",
@@ -656,8 +656,8 @@ class TbArtist(models.Model):
При сохранении исполнителя (создание и обновление): При сохранении исполнителя (создание и обновление):
1. Управляем синонимами: 1. Управляем синонимами:
- Для новых исполнителей: добавляем текущий s_artist в SYNONYM - Для новых исполнителей: добавляем текущий s_artist в SYN_EN
- При изменении s_artist: добавляем как старый, так и новый s_artist в SYNONYM - При изменении s_artist: добавляем как старый, так и новый s_artist в SYN_EN
- При редактировании: используем j_artist_metadata из формы (приоритет админу) - При редактировании: используем j_artist_metadata из формы (приоритет админу)
2. Если статья не привязана - создаём новую статью исполнителя автоматически 2. Если статья не привязана - создаём новую статью исполнителя автоматически
3. Генерируем технический заголовок и slug для статьи 3. Генерируем технический заголовок и slug для статьи
@@ -780,6 +780,42 @@ class TbItem(models.Model):
def __str__(self): def __str__(self):
return f"Item {self.id:0>4}: {self.s_item}" return f"Item {self.id:0>4}: {self.s_item}"
def save(self, *args, **kwargs):
"""
Переопределяем save для управления синонимами релиза (альбома) или просто товара и создания связанной статьи.
При сохранении релиза/альбома (создание и обновление):
1. Управляем синонимами альбома/релиза/товара:
- Для новых альбомов/релиза/товара: добавляем в SYN_EN
- При изменении s_item: добавляем как старый, так и новый s_item в SYN_EN
- При редактировании: используем j_item_metadata из формы (приоритет админу)
2. Если статья не привязана - создаём новую автоматически
3. Генерируем технический заголовок и slug для статьи
"""
# ===== ВАЛИДАЦИЯ НА ДУБЛИКАТЫ =====
# Проверяем ДО работы с синонимами и метаданными!
# Страховка: защита от прямого вызова save() минуя админку или (в будущем) парсер
validate_and_raise_for_duplicates(self, 's_item', 'j_item_metadata')
# ===== УПРАВЛЕНИЕ СИНОНИМАМИ =====
# Обновляем список синонимов в метаданных (универсальный хелпер для всех моделей)
update_synonyms_in_metadata(self, 's_item', 'j_item_metadata')
# ===== СОЗДАНИЕ ИЛИ ПОЛУЧЕНИЕ СВЯЗАННОЙ СТАТЬИ =====
# Используем универсальный хелпер для создания/поиска статьи
# Хелпер сам проверит через обратный FK, не дублирует статьи даже если админ переименовал
article = create_or_get_related_article(
self,
TbArticle.ArticleType.ITEM,
's_item',
'j_item_metadata',
'k_item_to_article' # ← Явно передаем имя FK поля (избегаем "магии")
)
self.k_item_to_article = article
# Вызываем оригинальный save родительского класса
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = 'Товар в каталоге (релиз, носитель, аксессуар)' verbose_name = 'Товар в каталоге (релиз, носитель, аксессуар)'
verbose_name_plural = 'Товары в каталоге' verbose_name_plural = 'Товары в каталоге'
@@ -826,7 +862,7 @@ class TbLabel(models.Model):
null=True, null=True,
verbose_name='Метаданные', verbose_name='Метаданные',
help_text='JSON: страна лейбла, официальный сайт и т.д. Включая список синонимов/вариантов названия для матчинга.' help_text='JSON: страна лейбла, официальный сайт и т.д. Включая список синонимов/вариантов названия для матчинга.'
' Пример: <tt>{"SYNONYM": ["Island", "Island Records", "Vertigo France"]}</tt>.' ' Пример: <tt>{"SYN_EN": ["Island", "Island Records", "Vertigo France"]}</tt>.'
, ,
) )
t_label_created = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Дата создания",) t_label_created = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Дата создания",)
@@ -841,8 +877,8 @@ class TbLabel(models.Model):
При сохранении лейбла (создание и обновление): При сохранении лейбла (создание и обновление):
1. Управляем синонимами лейбла: 1. Управляем синонимами лейбла:
- Для новых лейблов: добавляем текущий s_label в SYNONYM - Для новых лейблов: добавляем текущий s_label в SYN_EN
- При изменении s_label: добавляем как старый, так и новый s_label в SYNONYM - При изменении s_label: добавляем как старый, так и новый s_label в SYN_EN
- При редактировании: используем j_label_metadata из формы (приоритет админу) - При редактировании: используем j_label_metadata из формы (приоритет админу)
2. Если статья не привязана - создаём новую автоматически 2. Если статья не привязана - создаём новую автоматически
3. Генерируем технический заголовок и slug для статьи 3. Генерируем технический заголовок и slug для статьи