From ee45d3a2faeb83424fad14e9cdb1fe1223eea8f2 Mon Sep 17 00:00:00 2001 From: erjemin Date: Fri, 10 Jul 2026 23:52:54 +0300 Subject: [PATCH] =?UTF-8?q?mod:=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4=D0=B0?= =?UTF-8?q?=D1=82=D0=BE=D1=80=20=D1=84=D0=BE=D1=80=D0=BC,=20=D0=BF=D0=B0?= =?UTF-8?q?=D1=80=D1=81=D0=B5=D1=80=D0=B0=20=D0=B8=20=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B5=D0=B9=20(19)=20=D0=B0=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D0=BC=D0=B0=D1=82=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=B8=20=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BD=D0=B0=D0=B2=D0=BB=D0=B8=D0=B2=D0=B0?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=B4=D0=B5=D1=84=D0=BE=D0=BB=D1=82=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=BC=D0=B5=D1=82=D0=B0=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B?= =?UTF-8?q?=D0=B5=20=D0=BA=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B0=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=BC=20=D0=BA=D0=B0=D1=80=D1=82=D0=B8=D0=BD=D0=BA=D0=B0?= =?UTF-8?q?=D0=BC.=20=D0=9F=D0=B5=D1=80=D0=B5=D0=BE=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20save=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20TbA?= =?UTF-8?q?rticle=20=D0=B8=20=D0=BF=D1=80=D0=BE=D0=B1=D1=80=D0=BE=D1=81=20?= =?UTF-8?q?=D0=BD=D0=B5=D0=B3=D0=BE=20request=20=D0=B8=D0=B7=20ArticleAdmi?= =?UTF-8?q?n=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=BE=D0=BF=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20save=5Fmodel...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lpon_site/frontend/admin.py | 29 ++++++++++++--- lpon_site/frontend/models.py | 62 ++++++++++++++++++++++++++++----- lpon_site/lpon_site/settings.py | 11 ++++++ 3 files changed, 88 insertions(+), 14 deletions(-) diff --git a/lpon_site/frontend/admin.py b/lpon_site/frontend/admin.py index 2aa6bd2..bd7fa75 100644 --- a/lpon_site/frontend/admin.py +++ b/lpon_site/frontend/admin.py @@ -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): """Админ для предложений""" diff --git a/lpon_site/frontend/models.py b/lpon_site/frontend/models.py index 52363ac..0dd5e70 100644 --- a/lpon_site/frontend/models.py +++ b/lpon_site/frontend/models.py @@ -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 = 'Статьи' diff --git a/lpon_site/lpon_site/settings.py b/lpon_site/lpon_site/settings.py index f408527..4d67578 100644 --- a/lpon_site/lpon_site/settings.py +++ b/lpon_site/lpon_site/settings.py @@ -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'