diff --git a/lpon_site/frontend/admin.py b/lpon_site/frontend/admin.py
index 7a1c6e9..57107ca 100644
--- a/lpon_site/frontend/admin.py
+++ b/lpon_site/frontend/admin.py
@@ -709,7 +709,7 @@ class LabelAdmin(RequestInFormMixin, admin.ModelAdmin):
# Обогащение Media из формы (CodeMirrorFormMixin)
# Используем наследование для расширения, а не перекрытия
- # Это гарантирует что все CSS/JS из формы будут загружены
+ # Это гарантирует, что все CSS/JS из формы будут загружены
class Media(LabelAdminForm.Media):
css = {'all': (*LabelAdminForm.Media.css['all'], 'css/validation-override.css', )}
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',
css_class='codemirror-width-l codemirror-min-height-5')
-#
+
class SourceAdmin(admin.ModelAdmin):
"""Админ для источников"""
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_links = ('id', 's_item',)
list_filter = ('t_item_date', 't_item_created')
search_fields = ('s_item',)
filter_horizontal = ('k_item_to_artist', 'k_item_to_style')
readonly_fields = ('t_item_created', 't_item_updated')
+# ============================================================================
+# Остальные ModelAdmin классы
+# ============================================================================
+
class OfferAdmin(admin.ModelAdmin):
"""Админ для предложений"""
list_display = ('id', 's_offer', 'k_offer_to_item', 'f_offer_price', 'i_offer_quantity', 'i_offer_views')
diff --git a/lpon_site/frontend/models.py b/lpon_site/frontend/models.py
index 29adafb..1f3f586 100644
--- a/lpon_site/frontend/models.py
+++ b/lpon_site/frontend/models.py
@@ -557,7 +557,7 @@ class TbMusicStyle(models.Model):
blank=True,
verbose_name='Метаданные',
help_text='В основном список синонимов/вариантов названия из Discogs, MusicBrainz и т.д. для матчинга.'
- ' Пример: {"SYNONYM": ["rock", "Rock Music", "Rock & Roll", "Hard Rock"]}.',
+ ' Пример: {"SYN_EN": ["rock", "Rock Music", "Rock & Roll", "Hard Rock"]}.',
)
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="Дата обновления")
@@ -571,8 +571,8 @@ class TbMusicStyle(models.Model):
При сохранении музыкального стиля (создание и обновление):
1. Управляем синонимами:
- - Для новых музыкальных стилей: добавляем текущий s_style_name в SYNONYM
- - При изменении s_style_name: добавляем как старый, так и новый s_style_name в SYNONYM
+ - Для новых музыкальных стилей: добавляем текущий s_style_name в SYN_EN
+ - При изменении s_style_name: добавляем как старый, так и новый s_style_name в SYN_EN
- При редактировании: используем j_style_metadata из формы (приоритет админу)
2. Если статья не привязана - создаём новую статью музыкального стиля автоматически
3. Генерируем технический заголовок и slug для статьи
@@ -641,7 +641,7 @@ class TbArtist(models.Model):
null=True,
verbose_name='Метаданные JSON',
help_text='В основном список синонимов/вариантов названия из Discogs, MusicBrainz и т.д. для матчинга.'
- ' Пример: {"SYNONYM": ["The Beatles", "Beatles", "Beatles, The"]}.',
+ ' Пример: {"SYN_EN": ["The Beatles", "Beatles", "Beatles, The"]}.',
)
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="Дата обновления",
@@ -656,8 +656,8 @@ class TbArtist(models.Model):
При сохранении исполнителя (создание и обновление):
1. Управляем синонимами:
- - Для новых исполнителей: добавляем текущий s_artist в SYNONYM
- - При изменении s_artist: добавляем как старый, так и новый s_artist в SYNONYM
+ - Для новых исполнителей: добавляем текущий s_artist в SYN_EN
+ - При изменении s_artist: добавляем как старый, так и новый s_artist в SYN_EN
- При редактировании: используем j_artist_metadata из формы (приоритет админу)
2. Если статья не привязана - создаём новую статью исполнителя автоматически
3. Генерируем технический заголовок и slug для статьи
@@ -780,6 +780,42 @@ class TbItem(models.Model):
def __str__(self):
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:
verbose_name = 'Товар в каталоге (релиз, носитель, аксессуар)'
verbose_name_plural = 'Товары в каталоге'
@@ -826,7 +862,7 @@ class TbLabel(models.Model):
null=True,
verbose_name='Метаданные',
help_text='JSON: страна лейбла, официальный сайт и т.д. Включая список синонимов/вариантов названия для матчинга.'
- ' Пример: {"SYNONYM": ["Island", "Island Records", "Vertigo France"]}.'
+ ' Пример: {"SYN_EN": ["Island", "Island Records", "Vertigo France"]}.'
,
)
t_label_created = models.DateTimeField(auto_now_add=True, editable=False, verbose_name="Дата создания",)
@@ -841,8 +877,8 @@ class TbLabel(models.Model):
При сохранении лейбла (создание и обновление):
1. Управляем синонимами лейбла:
- - Для новых лейблов: добавляем текущий s_label в SYNONYM
- - При изменении s_label: добавляем как старый, так и новый s_label в SYNONYM
+ - Для новых лейблов: добавляем текущий s_label в SYN_EN
+ - При изменении s_label: добавляем как старый, так и новый s_label в SYN_EN
- При редактировании: используем j_label_metadata из формы (приоритет админу)
2. Если статья не привязана - создаём новую автоматически
3. Генерируем технический заголовок и slug для статьи