mod: валидатор форм, парсера и моделей (19) автоматически устанавливаем дефолтные метаданные к связанным картинкам. Переопрелен метод save для модели TbArticle и проброс него request из ArticleAdmin через переопределенный save_model...

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-07-10 23:52:54 +03:00
parent 286dd8eddc
commit ee45d3a2fa
3 changed files with 88 additions and 14 deletions
+24 -5
View File
@@ -914,10 +914,6 @@ class ItemAdmin(RequestInFormMixin, admin.ModelAdmin):
readonly_fields = ('t_item_created', 't_item_updated')
# ============================================================================
# Остальные ModelAdmin классы
# ============================================================================
# ============================================================================
# АДМИНКА СТАТЕЙ
#
@@ -929,7 +925,7 @@ class ItemAdmin(RequestInFormMixin, admin.ModelAdmin):
class ArticleAdminForm(CodeMirrorFormMixin):
"""
Кастомная форма для админки статей (TbArticle).
Добавляет виджеты CodeMirror для текстовых полей
Добавляет виджеты CodeMirror для текстовых полей.
"""
class Meta:
model = TbArticle
@@ -998,7 +994,30 @@ class ArticleAdmin(admin.ModelAdmin):
}),
)
def save_model(self, request, obj, form, change):
"""
Переопределяем save_model чтобы передать username в модель.
Username используется в TbArticle.save() для установки IMG_FROM в метаданные картинки.
Args:
request: HTTP-запрос (содержит info о пользователе)
obj: инстанция TbArticle для сохранения
form: валидированная форма
change: True если редактирование, False если создание
"""
# Сохраняем username текущего пользователя
if request and request.user:
obj._admin_username = request.user.username
# Вызываем родительский save_model который вызовет obj.save()
super().save_model(request, obj, form, change)
# ============================================================================
# Остальные ModelAdmin классы
# Их еще предстоит обжужукать (натянуть CodeMirror, обвесить валидаторами, переопределить save_model и т.д.)
# ============================================================================
class OfferAdmin(admin.ModelAdmin):
"""Админ для предложений"""
+53 -9
View File
@@ -240,14 +240,22 @@
# - M2M использует числовые FK (INT) вместо строк
# - Slug'и как UNIQUE indexed fields (не primary_key) для экономии места
import datetime
import logging
from django.db import models
from django.db.models import F
from filer.fields.image import FilerImageField
from filer.fields.file import FilerFileField
from frontend.utils import make_slug, update_synonyms_in_metadata, create_or_get_related_article
from frontend.utils_validators import validate_and_raise_for_duplicates
import datetime
import logging
from lpon_site.settings import (
KEY_IMAGE_TYPE, # Ключ, о том, какой тип изображения (реальное или абстрактное)
VALUE_IMAGE_ABSTRACT, # Реальная картинка (актуально, для фото товаров в TbOffer)
KEY_IMAGE_FROM, # Ключ, о том, как получено изображение
VALUE_IMAGE_FROM_USER, # Картинка загружена пользователем через админку
KEY_IMAGE_URL, # Ключ, о том, откуда получена картинка (URL источника)
KEY_IMAGE_NOTE, # Ключ, для заметок о картинке (например, "обложка", "задник", "вкладка" и т.д.)
)
logger = logging.getLogger(__name__)
@@ -295,15 +303,19 @@ class TbImageMetadata(models.Model):
)
j_img_metadata = models.JSONField(
# Гибкие дополнительные данные о изображении
# Пример: {"source": "discogs", "reality": "abstract", "confidence": 0.95, ...}
default=dict,
# Гибкие дополнительные данные об изображении с предзаполненной структурой
# Пример: {"IMG_IS": "abstract", "IMG_FROM": "ivan", "IMG_URL": None, "IMG_NOTE": "обложка"}
default={
KEY_IMAGE_TYPE: VALUE_IMAGE_ABSTRACT,
KEY_IMAGE_FROM: VALUE_IMAGE_FROM_USER,
KEY_IMAGE_URL: None,
KEY_IMAGE_NOTE: '???', # Автогенерация через парсер или админку
},
blank=True,
null=True,
null=False, # Изменено с True на False т.к. теперь всегда есть структура default
verbose_name='Метаданные',
help_text='JSON с дополнительными данными: источник (парсер, админка), тип (реальное/абстрактное), '
'достоверность, URL источника и т.д. Гибкое хранилище, позволяет добавлять новые поля '
'без миграций БД.',
help_text='JSON с дополнительными данными: тип (реальное/абстрактное), источник (парсер/админка), '
'URL источника и заметки. Предзаполнено структурой для удобства.',
)
# Связь с TbOffer (часть M2M через эту промежуточную таблицу)
@@ -488,6 +500,7 @@ class TbArticle(models.Model):
def save(self, *args, **kwargs):
"""
Автоматически генерируем slug на основе заголовка статьи.
Если привязана картинка и метаданных нет — создаем TbImageMetadata с тегами IMG_IS и IMG_FROM.
Вызывается при каждом сохранении записи (создание или обновление).
"""
# Если slug не установлен (новая запись) — генерируем его из названия
@@ -505,8 +518,39 @@ class TbArticle(models.Model):
self.slug = slug
# Сохраняем статью в БД (нужно чтобы потом привязать метаданные картинки)
super().save(*args, **kwargs)
# Если картинка привязана и метаданных для неё нет — создаем их
if self.k_article_to_image:
# Получаем существующие метаданные (если есть)
metadata = TbImageMetadata.objects.filter(image=self.k_article_to_image).first()
adding_metadata = {}
if not metadata or not KEY_IMAGE_TYPE in (metadata.j_img_metadata or {}):
adding_metadata[KEY_IMAGE_TYPE] = VALUE_IMAGE_ABSTRACT
if not metadata or not KEY_IMAGE_FROM in (metadata.j_img_metadata or {}):
# Получаем username из контекста админки (установлен в ArticleAdmin.save_model())
# Fallback на 'unknown' если username недоступен (например, при программном создании)
username = getattr(self, '_admin_username', 'unknown')
adding_metadata[KEY_IMAGE_FROM] = username
if not metadata or not KEY_IMAGE_URL in (metadata.j_img_metadata or {}):
adding_metadata[KEY_IMAGE_URL] = None
if not metadata or not KEY_IMAGE_NOTE in (metadata.j_img_metadata or {}):
adding_metadata[KEY_IMAGE_NOTE] = None
if not metadata:
TbImageMetadata.objects.create(
image=self.k_article_to_image,
i_img_sort=0,
j_img_metadata=adding_metadata
)
else:
# Обновляем только недостающие ключи в j_img_metadata
if adding_metadata:
metadata.j_img_metadata.update(adding_metadata)
metadata.save(update_fields=['j_img_metadata'])
# Примечание: m_offer остаётся NULL т.к. это статья, а не оффер
class Meta:
verbose_name = 'Статья'
verbose_name_plural = 'Статьи'
+11
View File
@@ -320,6 +320,17 @@ SLUG_MAX_LENGTH = 60
KEY_SYNONYM_EN = 'SYN_EN' # Ключ для хранения синонимов на английском языке (для поиска по синонимам) и ДЛЯ ПАРСЕРОВ
KEY_SYNONYM_RU = 'SYN_RU' # Ключ для хранения синонимов на русском языке (для представления на фронтенд)
# Ключи и значения для метаданных картинок
KEY_IMAGE_TYPE = 'IMG_IS' # Ключ для хранения типа картинки
VALUE_IMAGE_ABSTRACT = 'abstract' # Абстрактная картинка (лого лейбла, фото исполнителя, типовая обложка альбома...)
VALUE_IMAGE_REAL = 'real' # Реальная картинка (актуально, для фото товаров в TbOffer)
KEY_IMAGE_FROM = 'IMG_FROM' # Ключ, о том, как получено изображение
VALUE_IMAGE_FROM_USER = '_adm' # Картинка загружена пользователем через админку
VALUE_IMAGE_FROM_PARSE = 'parse' # Картинка получена через парсер (например, с сайта Discogs, Bandcamp и т.д.)
KEY_IMAGE_URL = 'IMG_URL' # Ключ для хранения URL оригинальной картинки (обычно это должен прописать парсер)
KEY_IMAGE_NOTE = 'IMG_NOTE' # Ключ для хранения заметки о картинке
# ДЛЯ ВАЛИДАЦИИ (поиска похожих исполнителей, стилей и т.д. (используется в TbLabel.matching_type, TbSeller.matching_type и т.д.)
VALIDATE_KEY__MATCH_TYPE = 'MATCH_TYPE'
VALIDATE_KEY__MODEL = 'MODEL'