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 для статьи