mod: модели (15) переделка логики подключения картинок (3) модель TbImageMetadata (бывшая TbImage) стала промежуточной таблицей m2m для связи TbOffer с картинками из filer.Image + удаление ненужных полей из TbImageMetadata + миграции

This commit is contained in:
2026-07-10 16:56:34 +03:00
parent 8ea088e26c
commit bd96468dd5
3 changed files with 110 additions and 50 deletions
+1 -1
View File
@@ -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_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') list_filter = ('l_offer_condition_media', 'l_offer_condition_sleeve', 't_offer_created', 'l_offer_to_format')
search_fields = ('s_offer',) 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') readonly_fields = ('s_offer_skip32', 't_offer_created', 't_offer_updated', 'i_offer_views', 'i_offer_favorites')
@@ -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.</br><b>ВАЖНО</b>: Так как статья призывается к исполнителям, лейблам, продавцам, музыкальным стилям, её можно использовать как логотип-пкитограмму или баннер этих сущностей. В этом случае рекомендуется использовать изображение с прозрачным фоном (движок поддерживает 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='Файл изображения'),
),
]
+44 -49
View File
@@ -259,21 +259,28 @@ class TbImageMetadata(models.Model):
""" """
Метаданные к изображениям из django_filer. Метаданные к изображениям из django_filer.
OneToOne связь с filer.Image. Хранит дополнительные поля: Промежуточная таблица для M2M связи TbOffer ↔ filer.Image.
- i_img_sort: порядок сортировки изображений Хранит дополнительные поля:
- i_img_sort: порядок сортировки изображений (0=обложка, 1=задник, 2=вкладка и т.д.)
- j_img_metadata: JSON с гибкими данными (источник, тип, достоверность и т.д.) - 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( image = FilerImageField(
# Связь OneToOne с filer.Image (через наследование) # Связь в M2M с filer.Image (часть промежуточной таблицы)
# CASCADE: если удалится картинка → удалить метаданные (метаданные без картинки бессмысленны)
null=False, null=False,
blank=False, blank=False,
on_delete=models.DO_NOTHING, on_delete=models.CASCADE,
related_name='metadata', # Встречный доступ: filer_image.metadata related_name='m_offer', # Встречный доступ: filer_image.m_offer.all()
verbose_name='Файл изображения', verbose_name='Файл изображения',
help_text='Файл изображения из django_filer.', help_text='Файл изображения из django_filer.',
) )
@@ -299,36 +306,16 @@ class TbImageMetadata(models.Model):
'без миграций БД.', 'без миграций БД.',
) )
# Эти поля ПОКА остаются (удалим после миграции данных) # Связь с TbOffer (часть M2M через эту промежуточную таблицу)
# но НЕ используются в новой архитектуре m_offer = models.ForeignKey(
l_img_source = models.CharField( 'TbOffer',
max_length=10, on_delete=models.SET_NULL, # SET_NULL: если удалится оффер → метаданные остаются (картинка же еще есть!)
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(
null=True, null=True,
blank=True, blank=True,
verbose_name='[DEPRECATED] Достоверность', related_name='m_image', # Встречный доступ: offer.m_image.all()
help_text='[DEPRECATED] Использовать j_img_metadata вместо этого поля.', 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: class Meta:
verbose_name = 'Метаданные изображения' verbose_name = 'Метаданные изображения'
@@ -406,11 +393,10 @@ class TbArticle(models.Model):
' будет отображаться без заголовка.' ' будет отображаться без заголовка.'
) )
k_article_to_image = FilerImageField( k_article_to_image = FilerImageField(
# Прямая ссылка на filer.Image (вместо TbImage) # Метаданные изображения (сортировка, источник, тип и т.д.) находятся в TbImageMetadata.
# Метаданные изображения (сортировка, источник, тип и т.д.) находятся в TbImageMetadata # Если нужно получить метаданные изображения, можно использовать `article.k_article_to_image.metadata`.
to='filer.Image',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name='article_images', related_name='image_to_article',
blank=True, blank=True,
null=True, null=True,
db_index=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): class Condition(models.TextChoices):
S = 's', 'Still Sealed (новое, запечатано)' S = 's', 'Still Sealed (новое, запечатано)'
@@ -1128,18 +1121,19 @@ class TbOffer(models.Model):
help_text='Обязательно - каждый оффер должен иметь источник. Через источник получаем данные ' help_text='Обязательно - каждый оффер должен иметь источник. Через источник получаем данные '
'продавца: offer.k_offer_to_source.k_source_to_seller', 'продавца: offer.k_offer_to_source.k_source_to_seller',
) )
k_offer_to_image = models.ManyToManyField( # Изображения (M2M к filer.Image через TbImageMetadata промежуточную таблицу)
# Изображения товара из django_filer (одна картинка может быть у многих офферов, m_offer_to_image = models.ManyToManyField(
# а оффер иметь много картинок) 'filer.Image',
# M2M связь для удобства в админке (filter_horizontal) through='TbImageMetadata',
# Порядок картинок определяется полем i_img_sort в TbImageMetadata related_name='m_image_to_offer',
to='filer.Image',
blank=True, blank=True,
related_name='offer_images', verbose_name='Изображения товара',
db_index=True, help_text='Картинки этого предложения (обложка, задник, фото и т.д). '
verbose_name='Изображения', 'Управляются в админке TbImageMetadata с сортировкой (i_img_sort) и метаданными (j_img_metadata). '
help_text='Картинки этого товара/предложения. Порядок определяется полем i_img_sort в TbImageMetadata. ' 'Доступ в коде: '
'Доступ к метаданным: image.metadata.i_img_sort и image.metadata.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( s_offer_catalog_num = models.TextField(
@@ -1265,6 +1259,7 @@ class TbOffer(models.Model):
# M2M не поддерживают участие в constraints. Уникальность на уровне БД не требуется. # M2M не поддерживают участие в constraints. Уникальность на уровне БД не требуется.
# ============================================================================
class TbSource(models.Model): class TbSource(models.Model):
""" """
Источник данных, из которого был импортирован оффер. Источник данных, из которого был импортирован оффер.