mod: модели (15) переделка логики подключения картинок (3) модель TbImageMetadata (бывшая TbImage) стала промежуточной таблицей m2m для связи TbOffer с картинками из filer.Image + удаление ненужных полей из TbImageMetadata + миграции
This commit is contained in:
@@ -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')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+65
@@ -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='Файл изображения'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
Источник данных, из которого был импортирован оффер.
|
Источник данных, из которого был импортирован оффер.
|
||||||
|
|||||||
Reference in New Issue
Block a user