From bd96468dd5271f3055121cdc74aefc4166f4b6f3 Mon Sep 17 00:00:00 2001 From: erjemin Date: Fri, 10 Jul 2026 16:56:34 +0300 Subject: [PATCH] =?UTF-8?q?mod:=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20(?= =?UTF-8?q?15)=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B8=20=D0=BF=D0=BE=D0=B4=D0=BA?= =?UTF-8?q?=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BA=D0=B0=D1=80?= =?UTF-8?q?=D1=82=D0=B8=D0=BD=D0=BE=D0=BA=20(3)=20=D0=BC=D0=BE=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D1=8C=20TbImageMetadata=20(=D0=B1=D1=8B=D0=B2=D1=88?= =?UTF-8?q?=D0=B0=D1=8F=20TbImage)=20=D1=81=D1=82=D0=B0=D0=BB=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D0=BC=D0=B5=D0=B6=D1=83=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D0=B5?= =?UTF-8?q?=D0=B9=20m2m=20=D0=B4=D0=BB=D1=8F=20=D1=81=D0=B2=D1=8F=D0=B7?= =?UTF-8?q?=D0=B8=20TbOffer=20=D1=81=20=D0=BA=D0=B0=D1=80=D1=82=D0=B8?= =?UTF-8?q?=D0=BD=D0=BA=D0=B0=D0=BC=D0=B8=20=D0=B8=D0=B7=20filer.Image=20+?= =?UTF-8?q?=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=83=D0=B6=D0=BD=D1=8B=D1=85=20=D0=BF=D0=BE=D0=BB?= =?UTF-8?q?=D0=B5=D0=B9=20=D0=B8=D0=B7=20TbImageMetadata=20+=20=D0=BC?= =?UTF-8?q?=D0=B8=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lpon_site/frontend/admin.py | 2 +- ...etadata_f_img_confidence_score_and_more.py | 65 +++++++++++++ lpon_site/frontend/models.py | 93 +++++++++---------- 3 files changed, 110 insertions(+), 50 deletions(-) create mode 100644 lpon_site/frontend/migrations/0003_remove_tbimagemetadata_f_img_confidence_score_and_more.py diff --git a/lpon_site/frontend/admin.py b/lpon_site/frontend/admin.py index f380b59..9c3bbb3 100644 --- a/lpon_site/frontend/admin.py +++ b/lpon_site/frontend/admin.py @@ -923,7 +923,7 @@ class OfferAdmin(admin.ModelAdmin): list_display = ('id', 's_offer', 'k_offer_to_item', 'f_offer_price', 'i_offer_quantity', 'i_offer_views') list_filter = ('l_offer_condition_media', 'l_offer_condition_sleeve', 't_offer_created', 'l_offer_to_format') search_fields = ('s_offer',) - filter_horizontal = ('k_offer_to_image',) + # Изображения теперь управляются через OfferImage inline (будет добавлено позже) readonly_fields = ('s_offer_skip32', 't_offer_created', 't_offer_updated', 'i_offer_views', 'i_offer_favorites') diff --git a/lpon_site/frontend/migrations/0003_remove_tbimagemetadata_f_img_confidence_score_and_more.py b/lpon_site/frontend/migrations/0003_remove_tbimagemetadata_f_img_confidence_score_and_more.py new file mode 100644 index 0000000..cd17ea7 --- /dev/null +++ b/lpon_site/frontend/migrations/0003_remove_tbimagemetadata_f_img_confidence_score_and_more.py @@ -0,0 +1,65 @@ +# Generated by Django 6.0.5 on 2026-07-10 13:40 + +import django.db.models.deletion +import filer.fields.image +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('frontend', '0002_alter_tbarticle_k_article_to_image_and_more'), + migrations.swappable_dependency(settings.FILER_IMAGE_MODEL), + ] + + operations = [ + migrations.RemoveField( + model_name='tbimagemetadata', + name='f_img_confidence_score', + ), + migrations.RemoveField( + model_name='tbimagemetadata', + name='l_img_reality', + ), + migrations.RemoveField( + model_name='tbimagemetadata', + name='l_img_source', + ), + migrations.RemoveField( + model_name='tbimagemetadata', + name='s_img_src_url', + ), + migrations.RemoveField( + model_name='tbimagemetadata', + name='t_img_created', + ), + migrations.RemoveField( + model_name='tbimagemetadata', + name='t_img_updated', + ), + migrations.RemoveField( + model_name='tboffer', + name='k_offer_to_image', + ), + migrations.AddField( + model_name='tbimagemetadata', + name='m_offer', + field=models.ForeignKey(blank=True, help_text='Оффер, к которому привязана эта картинка (может быть пусто, если оффер удален)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='m_image', to='frontend.tboffer', verbose_name='Оффер'), + ), + migrations.AddField( + model_name='tboffer', + name='m_offer_to_image', + field=models.ManyToManyField(blank=True, help_text='Картинки этого предложения (обложка, задник, фото и т.д). Управляются в админке TbImageMetadata с сортировкой (i_img_sort) и метаданными (j_img_metadata). Доступ в коде: offer.m_image.all() (все TbImageMetadata, отсортированные) или offer.m_offer_to_image.all() (все картинки). Каждая запись TbImageMetadata содержит: image, m_offer, i_img_sort, j_img_metadata.', related_name='m_image_to_offer', through='frontend.TbImageMetadata', to=settings.FILER_IMAGE_MODEL, verbose_name='Изображения товара'), + ), + migrations.AlterField( + model_name='tbarticle', + name='k_article_to_image', + field=filer.fields.image.FilerImageField(blank=True, help_text='Обложка или иллюстрация статьи из файлового хранилища (django_filer). Метаданные: image.metadata.i_img_sort и image.metadata.j_img_metadata.
ВАЖНО: Так как статья призывается к исполнителям, лейблам, продавцам, музыкальным стилям, её можно использовать как логотип-пкитограмму или баннер этих сущностей. В этом случае рекомендуется использовать изображение с прозрачным фоном (движок поддерживает SVG, WebP и PNG).', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='image_to_article', to=settings.FILER_IMAGE_MODEL, verbose_name='Изображение для статьи'), + ), + migrations.AlterField( + model_name='tbimagemetadata', + name='image', + field=filer.fields.image.FilerImageField(help_text='Файл изображения из django_filer.', on_delete=django.db.models.deletion.CASCADE, related_name='m_offer', to=settings.FILER_IMAGE_MODEL, verbose_name='Файл изображения'), + ), + ] diff --git a/lpon_site/frontend/models.py b/lpon_site/frontend/models.py index e7a6233..52363ac 100644 --- a/lpon_site/frontend/models.py +++ b/lpon_site/frontend/models.py @@ -259,21 +259,28 @@ class TbImageMetadata(models.Model): """ Метаданные к изображениям из django_filer. - OneToOne связь с filer.Image. Хранит дополнительные поля: - - i_img_sort: порядок сортировки изображений + Промежуточная таблица для M2M связи TbOffer ↔ filer.Image. + Хранит дополнительные поля: + - i_img_sort: порядок сортировки изображений (0=обложка, 1=задник, 2=вкладка и т.д.) - j_img_metadata: JSON с гибкими данными (источник, тип, достоверность и т.д.) + - m_offer: FK на TbOffer (часть M2M через эту промежуточную таблицу) + - image: FK на filer.Image (часть M2M через эту промежуточную таблицу) Данные можно заполнять вручную через админку или программно из парсеров. - При создании новой filer.Image автоматически создаётся TbImageMetadata - с дефолтными значениями (через signal). + + Доступ в коде: + - Все картинки офера (отсортированные): offer.m_image.all().order_by('i_img_sort') + - Все картинки для изображения: image.m_offer.all() + - Прямой доступ к данным: tbimagemetadata.image, tbimagemetadata.m_offer, tbimagemetadata.i_img_sort, tbimagemetadata.j_img_metadata """ image = FilerImageField( - # Связь OneToOne с filer.Image (через наследование) + # Связь в M2M с filer.Image (часть промежуточной таблицы) + # CASCADE: если удалится картинка → удалить метаданные (метаданные без картинки бессмысленны) null=False, blank=False, - on_delete=models.DO_NOTHING, - related_name='metadata', # Встречный доступ: filer_image.metadata + on_delete=models.CASCADE, + related_name='m_offer', # Встречный доступ: filer_image.m_offer.all() verbose_name='Файл изображения', help_text='Файл изображения из django_filer.', ) @@ -299,36 +306,16 @@ class TbImageMetadata(models.Model): 'без миграций БД.', ) - # Эти поля ПОКА остаются (удалим после миграции данных) - # но НЕ используются в новой архитектуре - l_img_source = models.CharField( - max_length=10, - blank=True, - null=True, - verbose_name='[DEPRECATED] Источник', - help_text='[DEPRECATED] Использовать j_img_metadata вместо этого поля.', - ) - l_img_reality = models.CharField( - max_length=10, - blank=True, - null=True, - verbose_name='[DEPRECATED] Тип снимка', - help_text='[DEPRECATED] Использовать j_img_metadata вместо этого поля.', - ) - s_img_src_url = models.URLField( - blank=True, - null=True, - verbose_name='[DEPRECATED] URL', - help_text='[DEPRECATED] Использовать j_img_metadata вместо этого поля.', - ) - f_img_confidence_score = models.FloatField( + # Связь с TbOffer (часть M2M через эту промежуточную таблицу) + m_offer = models.ForeignKey( + 'TbOffer', + on_delete=models.SET_NULL, # SET_NULL: если удалится оффер → метаданные остаются (картинка же еще есть!) null=True, blank=True, - verbose_name='[DEPRECATED] Достоверность', - help_text='[DEPRECATED] Использовать j_img_metadata вместо этого поля.', + related_name='m_image', # Встречный доступ: offer.m_image.all() + verbose_name='Оффер', + help_text='Оффер, к которому привязана эта картинка (может быть пусто, если оффер удален)', ) - t_img_created = models.DateTimeField(auto_now_add=True, verbose_name='[DEPRECATED] Дата добавления') - t_img_updated = models.DateTimeField(auto_now=True, verbose_name='[DEPRECATED] Дата обновления') class Meta: verbose_name = 'Метаданные изображения' @@ -406,11 +393,10 @@ class TbArticle(models.Model): ' будет отображаться без заголовка.' ) k_article_to_image = FilerImageField( - # Прямая ссылка на filer.Image (вместо TbImage) - # Метаданные изображения (сортировка, источник, тип и т.д.) находятся в TbImageMetadata - to='filer.Image', + # Метаданные изображения (сортировка, источник, тип и т.д.) находятся в TbImageMetadata. + # Если нужно получить метаданные изображения, можно использовать `article.k_article_to_image.metadata`. on_delete=models.SET_NULL, - related_name='article_images', + related_name='image_to_article', blank=True, null=True, db_index=True, @@ -1040,6 +1026,13 @@ class TbOffer(models.Model): """ Конкретное предложение от продавца. Один и тот же релиз может быть несколько раз в системе от разных продавцов. + + СВЯЗЬ С КАРТИНКАМИ: + Картинки к офферу управляются через TbImageMetadata.offer (M2M): + - offer.image_metadata.all() — все картинки этого офера (упорядочены по i_img_sort) + - TbImageMetadata(offer=self, image=<картинка>, i_img_sort=0, j_img_metadata={...}) + + В админке это выглядит как M2M с дополнительными полями (сортировка, метаданные). """ class Condition(models.TextChoices): S = 's', 'Still Sealed (новое, запечатано)' @@ -1128,18 +1121,19 @@ class TbOffer(models.Model): help_text='Обязательно - каждый оффер должен иметь источник. Через источник получаем данные ' 'продавца: offer.k_offer_to_source.k_source_to_seller', ) - k_offer_to_image = models.ManyToManyField( - # Изображения товара из django_filer (одна картинка может быть у многих офферов, - # а оффер иметь много картинок) - # M2M связь для удобства в админке (filter_horizontal) - # Порядок картинок определяется полем i_img_sort в TbImageMetadata - to='filer.Image', + # Изображения (M2M к filer.Image через TbImageMetadata промежуточную таблицу) + m_offer_to_image = models.ManyToManyField( + 'filer.Image', + through='TbImageMetadata', + related_name='m_image_to_offer', blank=True, - related_name='offer_images', - db_index=True, - verbose_name='Изображения', - help_text='Картинки этого товара/предложения. Порядок определяется полем i_img_sort в TbImageMetadata. ' - 'Доступ к метаданным: image.metadata.i_img_sort и image.metadata.j_img_metadata', + verbose_name='Изображения товара', + help_text='Картинки этого предложения (обложка, задник, фото и т.д). ' + 'Управляются в админке TbImageMetadata с сортировкой (i_img_sort) и метаданными (j_img_metadata). ' + 'Доступ в коде: ' + 'offer.m_image.all() (все TbImageMetadata, отсортированные) или ' + 'offer.m_offer_to_image.all() (все картинки). ' + 'Каждая запись TbImageMetadata содержит: image, m_offer, i_img_sort, j_img_metadata.', ) # Характеристики s_offer_catalog_num = models.TextField( @@ -1265,6 +1259,7 @@ class TbOffer(models.Model): # M2M не поддерживают участие в constraints. Уникальность на уровне БД не требуется. +# ============================================================================ class TbSource(models.Model): """ Источник данных, из которого был импортирован оффер.