From ed40ed1b746d7bab76a663d28bd201395b8b9fb1 Mon Sep 17 00:00:00 2001 From: erjemin Date: Fri, 10 Jul 2026 11:12:47 +0300 Subject: [PATCH] =?UTF-8?q?mod:=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D0=B8=20(?= =?UTF-8?q?13)=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(1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lpon_site/frontend/admin.py | 60 ++++---- ...r_tbarticle_k_article_to_image_and_more.py | 85 +++++++++++ lpon_site/frontend/models.py | 142 ++++++++++-------- 3 files changed, 194 insertions(+), 93 deletions(-) create mode 100644 lpon_site/frontend/migrations/0002_alter_tbarticle_k_article_to_image_and_more.py diff --git a/lpon_site/frontend/admin.py b/lpon_site/frontend/admin.py index 57107ca..486b651 100644 --- a/lpon_site/frontend/admin.py +++ b/lpon_site/frontend/admin.py @@ -9,7 +9,7 @@ from django.contrib import admin from django.utils.html import format_html, mark_safe from easy_thumbnails.files import get_thumbnailer from .models import ( - TbImage, TbArticle, TbArtist, TbItem, TbLabel, TbSeller, + TbImageMetadata, TbArticle, TbArtist, TbItem, TbLabel, TbSeller, TbOffer, TbSource, TbOfferHistory, TbMusicStyle ) from .utils_validators import validate_entity_for_admin_form, generate_admin_save_message @@ -150,14 +150,14 @@ class CodeMirrorFormMixin(forms.ModelForm): # ============================================================================ -# АДМИНИСТРИРОВАНИЕ TbImage +# АДМИНИСТРИРОВАНИЕ TbImageMetadata # -# Кастомня форма для админки TbImage -class TbImageAdminForm(CodeMirrorFormMixin): +# Кастомная форма для админки TbImageMetadata +class TbImageMetadataAdminForm(CodeMirrorFormMixin): """ - Кастомная форма для TbImage в админке. + Кастомная форма для TbImageMetadata в админке. Добавляет виртуальные поля для редактирования метаданных filer_image - (default_alt_text и default_caption), которые не хранятся в TbImage, но есть в filer_image. + (default_alt_text и default_caption), которые не хранятся в TbImageMetadata, но есть в filer_image. Наследует от CodeMirrorFormMixin для поддержки редактора CodeMirror. """ @@ -193,8 +193,8 @@ class TbImageAdminForm(CodeMirrorFormMixin): ) class Meta: - model = TbImage - fields = ('image', 'l_img_source', 'l_img_reality', 's_img_src_url', 'i_img_sort', 'f_img_confidence_score') + model = TbImageMetadata + fields = ('image', 'i_img_sort', 'j_img_metadata') def __init__(self, *args, **kwargs): """ @@ -227,37 +227,39 @@ class TbImageAdminForm(CodeMirrorFormMixin): css_class='codemirror-width-m codemirror-no-lines') # Активируем CodeMirror для реальных полей - self.setup_codemirror_field('s_img_src_url', language='url', - css_class='codemirror-width-xl codemirror-no-lines') self.setup_codemirror_field('i_img_sort', language='text', css_class='codemirror-width-s codemirror-no-lines') - self.setup_codemirror_field('f_img_confidence_score', language='text', - css_class='codemirror-width-s codemirror-no-lines') + self.setup_codemirror_field('j_img_metadata', language='json', + css_class='codemirror-width-xl') -# Админка для TbImage с кастомной формой -class ImageAdmin(admin.ModelAdmin): +# Админка для TbImageMetadata с кастомной формой +class ImageMetadataAdmin(admin.ModelAdmin): """ - Админ для изображений TbImage с поддержкой редактирования метаданных filer_image. + Админ для метаданных изображений TbImageMetadata с поддержкой редактирования метаданных filer_image. Позволяет пользователю заполнить default_alt_text и default_caption для картинки в filer - прямо в админке TbImage, без необходимости отдельного редактирования filer. + прямо в админке TbImageMetadata, без необходимости отдельного редактирования filer. - Использует TbImageAdminForm с наследованием от CodeMirrorFormMixin для поддержки CodeMirror. + Использует TbImageMetadataAdminForm с наследованием от CodeMirrorFormMixin для поддержки CodeMirror. """ - form = TbImageAdminForm # Используем кастомную форму с виртуальными полями (Media подключится через миксин) + form = TbImageMetadataAdminForm # Используем кастомную форму с виртуальными полями (Media подключится через миксин) - list_display = ('id', 'image_thumbnail', 'image', '_display_alt_text', 'i_img_sort', 't_img_created') + list_display = ('id', 'image_thumbnail', 'image', '_display_alt_text', 'i_img_sort') list_display_links = ('id', 'image_thumbnail', 'image') - list_filter = ('l_img_source', 'l_img_reality', 't_img_created') + list_filter = ('i_img_sort',) ordering = ('image', 'i_img_sort') - readonly_fields = ('t_img_created', 't_img_updated', '_display_alt_text', '_display_title_text') + readonly_fields = ('_display_alt_text', '_display_title_text') fieldsets = ( ('Изображение', { - 'fields': ('image', 'l_img_source', 'l_img_reality', 's_img_src_url', 'i_img_sort', - 'f_img_confidence_score'), - 'description': 'Основные данные об изображении и источнике', + 'fields': ('image', 'i_img_sort'), + 'description': 'Файл изображения и порядок его отображения (i_img_sort)', + }), + ('Метаданные (JSON)', { + 'fields': ('j_img_metadata',), + 'description': 'Гибкие дополнительные данные в JSON формате (источник, тип, достоверность и т.д.). ' + 'Пример: {"source": "discogs", "reality": "abstract", "confidence": 0.95}', }), ('Метаданные filer (SEO для картинок)', { 'fields': ('_display_alt_text', '_display_title_text', 'alt_text', 'title_text', @@ -266,10 +268,6 @@ class ImageAdmin(admin.ModelAdmin): ' текущие значения останутся без изменений (и не будут заполнены при создании).', # 'classes': ('collapse',), }), - ('Служебная информация', { - 'fields': ('t_img_created', 't_img_updated'), - 'classes': ('collapse',), - }), ) def image_thumbnail(self, obj): @@ -340,7 +338,7 @@ class ImageAdmin(admin.ModelAdmin): def save_model( self, request: HttpRequest, - obj: TbImage, + obj: TbImageMetadata, form: forms.ModelForm, change: bool, ) -> None: @@ -351,7 +349,7 @@ class ImageAdmin(admin.ModelAdmin): их значения сохраняются в соответствующие поля filer_image. Если поля не заполнены, текущие значения в filer остаются без изменений. """ - # Сначала сохраняем саму запись TbImage + # Сначала сохраняем саму запись TbImageMetadata super().save_model(request, obj, form, change) # Работаем с meta-данными filer только если картинка привязана @@ -949,7 +947,7 @@ class OfferHistoryAdmin(admin.ModelAdmin): # ============================================================================ # Регистрация моделей в дефолтном admin.site # ============================================================================ -admin.site.register(TbImage, ImageAdmin) +admin.site.register(TbImageMetadata, ImageMetadataAdmin) admin.site.register(TbArticle, ArticleAdmin) admin.site.register(TbMusicStyle, MusicStyleAdmin) admin.site.register(TbArtist, ArtistAdmin) diff --git a/lpon_site/frontend/migrations/0002_alter_tbarticle_k_article_to_image_and_more.py b/lpon_site/frontend/migrations/0002_alter_tbarticle_k_article_to_image_and_more.py new file mode 100644 index 0000000..7df04f8 --- /dev/null +++ b/lpon_site/frontend/migrations/0002_alter_tbarticle_k_article_to_image_and_more.py @@ -0,0 +1,85 @@ +# Generated by Django 6.0.5 on 2026-07-10 07:47 + +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', '0001_initial'), + migrations.swappable_dependency(settings.FILER_IMAGE_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='tbarticle', + name='k_article_to_image', + field=models.ForeignKey(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='article_images', to=settings.FILER_IMAGE_MODEL, verbose_name='Изображение для статьи'), + ), + migrations.AlterField( + model_name='tbartist', + name='j_artist_metadata', + field=models.JSONField(blank=True, default=dict, help_text='В основном список синонимов/вариантов названия из Discogs, MusicBrainz и т.д. для матчинга. Пример: {"SYN_EN": ["The Beatles", "Beatles", "Beatles, The"]}.', null=True, verbose_name='Метаданные JSON'), + ), + migrations.AlterField( + model_name='tbartist', + name='k_artist_to_article', + field=models.OneToOneField(blank=True, default=None, help_text='Связанная статья об исполнителе (Типографированные заголовок, тизер и текст статьи. Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)
ОБЯЗАТЕЛЬНО УКАЗЫВАТЬ т.к. через статью получаем слаг для URL артиста.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_to_artist', to='frontend.tbarticle', verbose_name='Связанная статья'), + ), + migrations.AlterField( + model_name='tbitem', + name='k_item_to_article', + field=models.OneToOneField(blank=True, default=None, help_text='Связанная статья об альбоме/релизе/товаре (Типографированные заголовок, тизер и текст статьи. Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)
ОБЯЗАТЕЛЬНО УКАЗЫВАТЬ т.к. через статью получаем слаг для URL альбома/релиза/товара.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_to_item', to='frontend.tbarticle', verbose_name='Связанная статья'), + ), + migrations.AlterField( + model_name='tblabel', + name='j_label_metadata', + field=models.JSONField(blank=True, default=dict, help_text='JSON: страна лейбла, официальный сайт и т.д. Включая список синонимов/вариантов названия для матчинга. Пример: {"SYN_EN": ["Island", "Island Records", "Vertigo France"]}.', null=True, verbose_name='Метаданные'), + ), + migrations.AlterField( + model_name='tblabel', + name='k_label_to_article', + field=models.OneToOneField(blank=True, default=None, help_text='Связанная статья об лейбле (Типографированные заголовок, тизер и текст статьи. Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)
ОБЯЗАТЕЛЬНО УКАЗЫВАТЬ т.к. через статью получаем слаг для URL лейбла.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_to_label', to='frontend.tbarticle', verbose_name='Связанная статья'), + ), + migrations.AlterField( + model_name='tbmusicstyle', + name='j_style_metadata', + field=models.JSONField(blank=True, default=dict, help_text='В основном список синонимов/вариантов названия из Discogs, MusicBrainz и т.д. для матчинга. Пример: {"SYN_EN": ["rock", "Rock Music", "Rock & Roll", "Hard Rock"]}.', verbose_name='Метаданные'), + ), + migrations.AlterField( + model_name='tboffer', + name='k_offer_to_image', + field=models.ManyToManyField(blank=True, db_index=True, help_text='Картинки этого товара/предложения. Порядок определяется полем i_img_sort в TbImageMetadata. Доступ к метаданным: image.metadata.i_img_sort и image.metadata.j_img_metadata', related_name='offer_images', to=settings.FILER_IMAGE_MODEL, verbose_name='Изображения'), + ), + migrations.AlterField( + model_name='tbseller', + name='k_seller_to_article', + field=models.OneToOneField(blank=True, default=None, help_text='Связанная статья о продавце (HTML-готовые заголовок, тизер и текст статьи). Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)
ОБЯЗАТЕЛЬНО УКАЗЫВАТЬ т.к. через статью получаем слаг для URL продавца.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_to_seller', to='frontend.tbarticle', verbose_name='Связанная статья'), + ), + migrations.CreateModel( + name='TbImageMetadata', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('i_img_sort', models.IntegerField(db_index=True, default=0, help_text='Порядок отображения изображений. Чем меньше число, тем выше в списке. Можно использовать для указания обложки (0), задника (1) и т.д.', verbose_name='Сортировка')), + ('j_img_metadata', models.JSONField(blank=True, default=dict, help_text='JSON с дополнительными данными: источник (парсер, админка), тип (реальное/абстрактное), достоверность, URL источника и т.д. Гибкое хранилище, позволяет добавлять новые поля без миграций БД.', null=True, verbose_name='Метаданные')), + ('l_img_source', models.CharField(blank=True, help_text='[DEPRECATED] Использовать j_img_metadata вместо этого поля.', max_length=10, null=True, verbose_name='[DEPRECATED] Источник')), + ('l_img_reality', models.CharField(blank=True, help_text='[DEPRECATED] Использовать j_img_metadata вместо этого поля.', max_length=10, null=True, verbose_name='[DEPRECATED] Тип снимка')), + ('s_img_src_url', models.URLField(blank=True, help_text='[DEPRECATED] Использовать j_img_metadata вместо этого поля.', null=True, verbose_name='[DEPRECATED] URL')), + ('f_img_confidence_score', models.FloatField(blank=True, help_text='[DEPRECATED] Использовать j_img_metadata вместо этого поля.', null=True, verbose_name='[DEPRECATED] Достоверность')), + ('t_img_created', models.DateTimeField(auto_now_add=True, verbose_name='[DEPRECATED] Дата добавления')), + ('t_img_updated', models.DateTimeField(auto_now=True, verbose_name='[DEPRECATED] Дата обновления')), + ('image', filer.fields.image.FilerImageField(help_text='Файл изображения из django_filer.', on_delete=django.db.models.deletion.DO_NOTHING, related_name='metadata', to=settings.FILER_IMAGE_MODEL, verbose_name='Файл изображения')), + ], + options={ + 'verbose_name': 'Метаданные изображения', + 'verbose_name_plural': 'Метаданные изображений', + 'ordering': ('i_img_sort',), + }, + ), + migrations.DeleteModel( + name='TbImage', + ), + ] diff --git a/lpon_site/frontend/models.py b/lpon_site/frontend/models.py index 1f3f586..89cb49a 100644 --- a/lpon_site/frontend/models.py +++ b/lpon_site/frontend/models.py @@ -253,76 +253,87 @@ logger = logging.getLogger(__name__) # ============================================================================ -# ИЗОБРАЖЕНИЯ +# МЕТАДАННЫЕ ИЗОБРАЖЕНИЙ # ============================================================================ -class TbImage(models.Model): +class TbImageMetadata(models.Model): """ - Изображение, связанное с релизом, оффером, исполнителем и т.д. - """ - # Источник изображения - class ImageSource(models.TextChoices): - PARSER_UPLOAD = 'parser', 'Загружено парсером (из Discogs, Meshok или другого сайта)' - MANUAL_UPLOAD = 'manual', 'Ручная загрузка пользователем' - VENDOR = 'vendor', 'От продавца' - OTHER = 'other', 'Другое' + Метаданные к изображениям из django_filer. - # Тип изображения (реальное или абстрактное) - class ImageReality(models.TextChoices): - REAL_PHOTO = 'real', 'Реальная фотография товара' - ABSTRACT = 'abstract', 'Абстрактное (из внешнего источника)' + OneToOne связь с filer.Image. Хранит дополнительные поля: + - i_img_sort: порядок сортировки изображений + - j_img_metadata: JSON с гибкими данными (источник, тип, достоверность и т.д.) + + Данные можно заполнять вручную через админку или программно из парсеров. + При создании новой filer.Image автоматически создаётся TbImageMetadata + с дефолтными значениями (через signal). + """ image = FilerImageField( - # Файл через django_filer + # Связь OneToOne с filer.Image (через наследование) null=False, blank=False, on_delete=models.DO_NOTHING, + related_name='metadata', # Встречный доступ: filer_image.metadata verbose_name='Файл изображения', - help_text='Файл изображения, загруженный через django_filer.', + help_text='Файл изображения из django_filer.', ) + + i_img_sort = models.IntegerField( + # Порядок (сортировка) вывода изображений + default=0, + db_index=True, + verbose_name='Сортировка', + help_text='Порядок отображения изображений. Чем меньше число, тем выше в списке. ' + 'Можно использовать для указания обложки (0), задника (1) и т.д.', + ) + + j_img_metadata = models.JSONField( + # Гибкие дополнительные данные о изображении + # Пример: {"source": "discogs", "reality": "abstract", "confidence": 0.95, ...} + default=dict, + blank=True, + null=True, + verbose_name='Метаданные', + help_text='JSON с дополнительными данными: источник (парсер, админка), тип (реальное/абстрактное), ' + 'достоверность, URL источника и т.д. Гибкое хранилище, позволяет добавлять новые поля ' + 'без миграций БД.', + ) + + # Эти поля ПОКА остаются (удалим после миграции данных) + # но НЕ используются в новой архитектуре l_img_source = models.CharField( max_length=10, - choices=ImageSource.choices, - default=ImageSource.MANUAL_UPLOAD, - verbose_name='Источник', - help_text='Как был получен этот снимок: загружен вручную, получен парсером из внешнего источника (например,' - ' Discogs), предоставлен продавцом и т.д.', + blank=True, + null=True, + verbose_name='[DEPRECATED] Источник', + help_text='[DEPRECATED] Использовать j_img_metadata вместо этого поля.', ) l_img_reality = models.CharField( max_length=10, - choices=ImageReality.choices, - default=ImageReality.ABSTRACT, - verbose_name='Тип снимка', - help_text='Реальная фотография товара или картинка из внешнего источника?', + 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='URL', - help_text='URL источника, если изображение взято (в том числе и парсером) из внешнего источника (например, Discogs)', - ) - i_img_sort = models.IntegerField( - # Порядок (сортировка) вывода - default=0, - db_index=True, - verbose_name='Сортировка', - help_text='Порядок отображения изображений. Чем меньше число, тем выше в списке. Можно использовать' - ' для указания обложки (0), задника (1) и т.д.', + verbose_name='[DEPRECATED] URL', + help_text='[DEPRECATED] Использовать j_img_metadata вместо этого поля.', ) f_img_confidence_score = models.FloatField( - # Доверие данным (для парсеров и API) null=True, blank=True, - default=10.0, - verbose_name='Достоверность', - help_text='Уверенность (для автоматических данных) 0.0 - 10.0, насколько уверены, что это правильное изображение', + verbose_name='[DEPRECATED] Достоверность', + help_text='[DEPRECATED] Использовать j_img_metadata вместо этого поля.', ) - t_img_created = models.DateTimeField(auto_now_add=True, verbose_name='Дата добавления',) - t_img_updated = models.DateTimeField(auto_now=True, verbose_name='Дата обновления',) + 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 = 'Изображение' - verbose_name_plural = 'Изображения' - ordering = ('-t_img_created', 'i_img_sort',) + verbose_name = 'Метаданные изображения' + verbose_name_plural = 'Метаданные изображений' + ordering = ('i_img_sort',) # ============================================================================ @@ -395,13 +406,20 @@ class TbArticle(models.Model): ' будет отображаться без заголовка.' ) k_article_to_image = models.ForeignKey( - TbImage, + # Прямая ссылка на filer.Image (вместо TbImage) + # Метаданные изображения (сортировка, источник, тип и т.д.) находятся в TbImageMetadata + to='filer.Image', on_delete=models.SET_NULL, - related_name='image_to_article', + related_name='article_images', blank=True, null=True, - db_index=True, # Принудительно создаем индекс, т.к. SQLite их сам не создаст. + db_index=True, verbose_name='Изображение для статьи', + help_text='Обложка или иллюстрация статьи из файлового хранилища (django_filer).' + ' Метаданные: image.metadata.i_img_sort и image.metadata.j_img_metadata.
' + 'ВАЖНО: Так как статья призывается к исполнителям, лейблам, продавцам, музыкальным стилям,' + ' её можно использовать как логотип-пкитограмму или баннер этих сущностей. В этом случае ' + ' рекомендуется использовать изображение с прозрачным фоном (движок поддерживает SVG, WebP и PNG).' ) s_article_teaser_html = models.TextField( blank=True, @@ -426,7 +444,7 @@ class TbArticle(models.Model): verbose_name='Число просмотров', ) i_article_favorites = models.IntegerField( - # Счетчик добавлений в избранное (включая избранное артиста, итема/релиза/товара, лейбла и продавца) + # Счетчик добавлений в избранное (включая избранный артист, item/релиз/товар/лейбл/продавец) default=0, db_index=True, # для сортировки "самые добавляемые в избранное" verbose_name='Число в избранном', @@ -632,7 +650,7 @@ class TbArtist(models.Model): blank=True, # <-- Интерфейсное удобство. Связь будет сделана автоматически, и статья создана автоматически. verbose_name='Связанная статья', help_text='Связанная статья об исполнителе (Типографированные заголовок, тизер и текст статьи. Так же' - ' через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)
' + ' через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)
' 'ОБЯЗАТЕЛЬНО УКАЗЫВАТЬ т.к. через статью получаем слаг для URL артиста.' ) j_artist_metadata = models.JSONField( @@ -731,18 +749,16 @@ class TbItem(models.Model): TbArticle, on_delete=models.SET_NULL, related_name='article_to_item', - db_index=True, # Принудительно создаем индекс, т.к. SQLite их сам не создаст. + db_index=True, default=None, null=True, - blank=True, # <-- Интерфейсное удобство. Связь будет сделана автоматически, и статья создана автоматически. + blank=True, verbose_name='Связанная статья', help_text='Связанная статья об альбоме/релизе/товаре (Типографированные заголовок, тизер и текст статьи.' - ' Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)
' + ' Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)
' 'ОБЯЗАТЕЛЬНО УКАЗЫВАТЬ т.к. через статью получаем слаг для URL альбома/релиза/товара.' ) s_item_date = models.CharField( - # Год выпуска (важно для коллекционеров). Так же в будущем можно использовать для "ЮБИЛЕЙНОГО ПРЕДЛОЖЕНИЯ" - # и фан-календаря max_length=10, blank=True, null=True, @@ -853,7 +869,7 @@ class TbLabel(models.Model): blank=True, # <-- Интерфейсное удобство. Связь будет сделана автоматически, и статья создана автоматически. verbose_name='Связанная статья', help_text='Связанная статья об лейбле (Типографированные заголовок, тизер и текст статьи.' - ' Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)
' + ' Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)
' 'ОБЯЗАТЕЛЬНО УКАЗЫВАТЬ т.к. через статью получаем слаг для URL лейбла.' ) j_label_metadata = models.JSONField( @@ -968,7 +984,7 @@ class TbSeller(models.Model): blank=True, # <-- Интерфейсное удобство. Связь будет сделана автоматически, и статья создана автоматически. verbose_name='Связанная статья', help_text='Связанная статья о продавце (HTML-готовые заголовок, тизер и текст статьи).' - ' Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)
' + ' Так же через статью может быть получена картинка, seo атрибуты, слаг (обязательно) и т.п.)
' 'ОБЯЗАТЕЛЬНО УКАЗЫВАТЬ т.к. через статью получаем слаг для URL продавца.' ) l_seller_type = models.CharField( @@ -1113,15 +1129,17 @@ class TbOffer(models.Model): 'продавца: offer.k_offer_to_source.k_source_to_seller', ) k_offer_to_image = models.ManyToManyField( - # Изображения товара (одна картинка может быть у многих офферов, а оффер иметь много картинок) + # Изображения товара из django_filer (одна картинка может быть у многих офферов, + # а оффер иметь много картинок) # M2M связь для удобства в админке (filter_horizontal) - # Порядок картинок определяется полем i_img_sort в TbImage - TbImage, + # Порядок картинок определяется полем i_img_sort в TbImageMetadata + to='filer.Image', blank=True, - related_name='image_to_offer', - db_index=True, # Принудительно создаем индекс, т.к. SQLite их сам не создаст. + related_name='offer_images', + db_index=True, verbose_name='Изображения', - help_text='Картинки этого товара. Порядок определяется полем i_img_sort в ка��тинке.', + help_text='Картинки этого товара/предложения. Порядок определяется полем i_img_sort в TbImageMetadata. ' + 'Доступ к метаданным: image.metadata.i_img_sort и image.metadata.j_img_metadata', ) # Характеристики s_offer_catalog_num = models.TextField(