mod: валидатор форм, парсера и моделей (19) автоматически устанавливаем дефолтные метаданные к связанным картинкам. Переопрелен метод save для модели TbArticle и проброс него request из ArticleAdmin через переопределенный save_model...
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -914,10 +914,6 @@ class ItemAdmin(RequestInFormMixin, admin.ModelAdmin):
|
|||||||
readonly_fields = ('t_item_created', 't_item_updated')
|
readonly_fields = ('t_item_created', 't_item_updated')
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Остальные ModelAdmin классы
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# АДМИНКА СТАТЕЙ
|
# АДМИНКА СТАТЕЙ
|
||||||
#
|
#
|
||||||
@@ -929,7 +925,7 @@ class ItemAdmin(RequestInFormMixin, admin.ModelAdmin):
|
|||||||
class ArticleAdminForm(CodeMirrorFormMixin):
|
class ArticleAdminForm(CodeMirrorFormMixin):
|
||||||
"""
|
"""
|
||||||
Кастомная форма для админки статей (TbArticle).
|
Кастомная форма для админки статей (TbArticle).
|
||||||
Добавляет виджеты CodeMirror для текстовых полей
|
Добавляет виджеты CodeMirror для текстовых полей.
|
||||||
"""
|
"""
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TbArticle
|
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):
|
class OfferAdmin(admin.ModelAdmin):
|
||||||
"""Админ для предложений"""
|
"""Админ для предложений"""
|
||||||
|
|
||||||
|
|||||||
@@ -240,14 +240,22 @@
|
|||||||
# - M2M использует числовые FK (INT) вместо строк
|
# - M2M использует числовые FK (INT) вместо строк
|
||||||
# - Slug'и как UNIQUE indexed fields (не primary_key) для экономии места
|
# - Slug'и как UNIQUE indexed fields (не primary_key) для экономии места
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from filer.fields.image import FilerImageField
|
from filer.fields.image import FilerImageField
|
||||||
from filer.fields.file import FilerFileField
|
from filer.fields.file import FilerFileField
|
||||||
from frontend.utils import make_slug, update_synonyms_in_metadata, create_or_get_related_article
|
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
|
from frontend.utils_validators import validate_and_raise_for_duplicates
|
||||||
import datetime
|
from lpon_site.settings import (
|
||||||
import logging
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -295,15 +303,19 @@ class TbImageMetadata(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
j_img_metadata = models.JSONField(
|
j_img_metadata = models.JSONField(
|
||||||
# Гибкие дополнительные данные о изображении
|
# Гибкие дополнительные данные об изображении с предзаполненной структурой
|
||||||
# Пример: {"source": "discogs", "reality": "abstract", "confidence": 0.95, ...}
|
# Пример: {"IMG_IS": "abstract", "IMG_FROM": "ivan", "IMG_URL": None, "IMG_NOTE": "обложка"}
|
||||||
default=dict,
|
default={
|
||||||
|
KEY_IMAGE_TYPE: VALUE_IMAGE_ABSTRACT,
|
||||||
|
KEY_IMAGE_FROM: VALUE_IMAGE_FROM_USER,
|
||||||
|
KEY_IMAGE_URL: None,
|
||||||
|
KEY_IMAGE_NOTE: '???', # Автогенерация через парсер или админку
|
||||||
|
},
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=False, # Изменено с True на False т.к. теперь всегда есть структура default
|
||||||
verbose_name='Метаданные',
|
verbose_name='Метаданные',
|
||||||
help_text='JSON с дополнительными данными: источник (парсер, админка), тип (реальное/абстрактное), '
|
help_text='JSON с дополнительными данными: тип (реальное/абстрактное), источник (парсер/админка), '
|
||||||
'достоверность, URL источника и т.д. Гибкое хранилище, позволяет добавлять новые поля '
|
'URL источника и заметки. Предзаполнено структурой для удобства.',
|
||||||
'без миграций БД.',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Связь с TbOffer (часть M2M через эту промежуточную таблицу)
|
# Связь с TbOffer (часть M2M через эту промежуточную таблицу)
|
||||||
@@ -488,6 +500,7 @@ class TbArticle(models.Model):
|
|||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Автоматически генерируем slug на основе заголовка статьи.
|
Автоматически генерируем slug на основе заголовка статьи.
|
||||||
|
Если привязана картинка и метаданных нет — создаем TbImageMetadata с тегами IMG_IS и IMG_FROM.
|
||||||
Вызывается при каждом сохранении записи (создание или обновление).
|
Вызывается при каждом сохранении записи (создание или обновление).
|
||||||
"""
|
"""
|
||||||
# Если slug не установлен (новая запись) — генерируем его из названия
|
# Если slug не установлен (новая запись) — генерируем его из названия
|
||||||
@@ -505,8 +518,39 @@ class TbArticle(models.Model):
|
|||||||
|
|
||||||
self.slug = slug
|
self.slug = slug
|
||||||
|
|
||||||
|
# Сохраняем статью в БД (нужно чтобы потом привязать метаданные картинки)
|
||||||
super().save(*args, **kwargs)
|
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:
|
class Meta:
|
||||||
verbose_name = 'Статья'
|
verbose_name = 'Статья'
|
||||||
verbose_name_plural = 'Статьи'
|
verbose_name_plural = 'Статьи'
|
||||||
|
|||||||
@@ -320,6 +320,17 @@ SLUG_MAX_LENGTH = 60
|
|||||||
KEY_SYNONYM_EN = 'SYN_EN' # Ключ для хранения синонимов на английском языке (для поиска по синонимам) и ДЛЯ ПАРСЕРОВ
|
KEY_SYNONYM_EN = 'SYN_EN' # Ключ для хранения синонимов на английском языке (для поиска по синонимам) и ДЛЯ ПАРСЕРОВ
|
||||||
KEY_SYNONYM_RU = 'SYN_RU' # Ключ для хранения синонимов на русском языке (для представления на фронтенд)
|
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 и т.д.)
|
# ДЛЯ ВАЛИДАЦИИ (поиска похожих исполнителей, стилей и т.д. (используется в TbLabel.matching_type, TbSeller.matching_type и т.д.)
|
||||||
VALIDATE_KEY__MATCH_TYPE = 'MATCH_TYPE'
|
VALIDATE_KEY__MATCH_TYPE = 'MATCH_TYPE'
|
||||||
VALIDATE_KEY__MODEL = 'MODEL'
|
VALIDATE_KEY__MODEL = 'MODEL'
|
||||||
|
|||||||
Reference in New Issue
Block a user