From f281997afc9260433e8014c098274502116f1329 Mon Sep 17 00:00:00 2001 From: erjemin Date: Tue, 7 Jul 2026 22:24:06 +0300 Subject: [PATCH] =?UTF-8?q?minor:=20=D0=BF=D0=BB=D0=B0=D0=BD=20=D0=BF?= =?UTF-8?q?=D0=B5=D1=80=D0=B5=D0=B4=D0=B5=D0=BB=D0=BA=D0=B8=20=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=BA=D0=B0=D1=80=D1=82?= =?UTF-8?q?=D0=B8=D0=BD=D0=BA=D0=B0=D0=BC=D0=B8!=20=D0=A7=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=B7=20=D1=81=D0=B8=D0=B3=D0=BD=D0=B0=D0=BB=D1=8B!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2109_migration_tbimage_to_imagemetadata.md | 445 ++++++++++++++++++ 1 file changed, 445 insertions(+) create mode 100644 agent-reports/20260707_2109_migration_tbimage_to_imagemetadata.md diff --git a/agent-reports/20260707_2109_migration_tbimage_to_imagemetadata.md b/agent-reports/20260707_2109_migration_tbimage_to_imagemetadata.md new file mode 100644 index 0000000..e3f2885 --- /dev/null +++ b/agent-reports/20260707_2109_migration_tbimage_to_imagemetadata.md @@ -0,0 +1,445 @@ +# План миграции: TbImage → ImageMetadata (Filer Extension) + +**Дата:** 2026-07-07 +**Время:** 21:09 +**Автор:** Copilot CLI +**Статус:** В планировании + +--- + +## Обзор + +Переконфигурирование архитектуры работы с изображениями: +- **Текущее состояние:** Модели ссылаются на TbImage, которая ссылается на FilerImageField +- **Целевое состояние:** Модели ссылаются прямо на filer.Image, дополнительные метаданные хранятся в переименованной TbImage (→ ImageMetadata) + +--- + +## Фаза 1: Подготовка и анализ + +### Задача 1.1: Аудит использования TbImage +- [ ] Найти все FK на TbImage в моделях +- [ ] Найти все M2M связи на TbImage (см. `TbOffer.k_offer_to_image`) +- [ ] Найти все места в админке, которые работают с TbImage +- [ ] Найти все места в парсерах/сигналах, которые создают/обновляют TbImage +- [ ] Документировать каждое найденное место + +### Задача 1.2: Анализ filer.Image структуры +- [ ] Проверить какие даты есть в filer.Image модели (`modified_at`, `uploaded_at` или их эквиваленты) +- [ ] Проверить есть ли `related_name` у FilerImageField в TbImage или он будет auto-generated +- [ ] Убедиться что filer работает с сигналами при изменении файла + +**Результат:** Составить полный список всех мест, требующих изменения + +--- + +## Фаза 2: Модификация моделей + +### Задача 2.1: Переименование и переструктурирование TbImage +- [ ] Переименовать класс TbImage → ImageMetadata +- [ ] Добавить/проверить `related_name='metadata'` на FilerImageField + ```python + image = FilerImageField( + ..., + related_name='metadata' # ← для доступа: filer_image.metadata + ) + ``` +- [ ] **УДАЛИТЬ** поля из ImageMetadata: + - `t_img_created` (используем filer.Image.uploaded_at или эквивалент) + - `t_img_updated` (синхронизируем с filer.Image.modified_at через сигнал) +- [ ] Обновить `Meta.verbose_name` на "Метаданные изображения" +- [ ] Обновить `Meta.ordering` (убрать временные поля если они там были) + +### Задача 2.2: Обновление FK на изображения во всех моделях +- [ ] TbArticle: `k_article_to_image` — ForeignKey(TbImage) → ForeignKey(filer.Image) +- [ ] **TbItem: добавить `k_item_to_image` — ForeignKey(filer.Image, blank=True, null=True)** + - Обложка альбома/релиза (всегда `l_img_reality = 'abstract'`) + - Дефолтное значение при создании ImageMetadata: `l_img_reality = 'abstract'` +- [ ] TbOffer: M2M `k_offer_to_image` — на filer.Image вместо TbImage + - Может переиспользовать картинку из TbItem или иметь свою + - В админке: `l_img_reality` может быть `'real'` (реальная фото товара) или `'abstract'` (из каталога) + - При создании через админку: дефолт `l_img_reality = 'real'` (человек загружает реальные фото) + - При создании через парсер: `l_img_reality = 'abstract'` (парсер берет с Discogs и т.д.) +- [ ] [Проверить другие модели] — все остальные модели с FK на картинки +- [ ] Для каждого изменения: + - Обновить саму модель + - Создать Django миграцию + - Обновить админку (инлайны) + +### Задача 2.3: Создание миграции данных +- [ ] Миграция для переименования таблицы (TbImage → ImageMetadata или оставить таблицу, но переименовать модель?) +- [ ] Миграция для удаления полей `t_img_created`, `t_img_updated` из ImageMetadata +- [ ] Обработка сирот (ImageMetadata без соответствующего filer.Image) — удалить или связать + +**Важно:** Порядок миграций критичен! Сначала добавляем новые FK/M2M, потом удаляем старые. + +--- + +## Фаза 3: Сигналы и автоматизация + +### Задача 3.1: Signal на создание filer.Image → auto-create ImageMetadata +```python +# В apps.py +@receiver(post_save, sender=filer.models.Image) +def auto_create_image_metadata(sender, instance, created, **kwargs): + """При создании filer.Image автоматически создавать ImageMetadata с дефолтами""" + if created: + ImageMetadata.objects.get_or_create( + image=instance, + defaults={ + 'i_img_sort': 0, + 'l_img_source': 'manual', # или 'parser' если известно + 'l_img_reality': 'abstract', + 'f_img_confidence_score': 10.0, + } + ) +``` + +### Задача 3.2: Обратные сигналы — синхронизация дат +```python +# В apps.py +@receiver(post_save, sender=ImageMetadata) +def sync_image_modification_date(sender, instance, **kwargs): + """При обновлении ImageMetadata обновлять modified_at в filer.Image""" + # Обновляем filer.Image.modified_at = now() + # Нужно понять как именно это сделать в filer (direct update без повторного save()) +``` + +**Важно:** Нужно избежать циклических сохранений (Image → Metadata → Image → ...) + +### Задача 3.3: Сигнал на удаление +- [ ] При удалении ImageMetadata → не удаляем filer.Image (метаданные удаляются, файл остаётся) +- [ ] При удалении filer.Image → каскадно удаляем ImageMetadata (on_delete=models.CASCADE) + +--- + +## Фаза 4: Админка и UI + +### Задача 4.1: Создание ImageMetadataAdminMixin +```python +class ImageMetadataAdminMixin: + """Миксин для админок, содержащих связь с filer.Image""" + # Автоматически добавлять инлайн ImageMetadata при наличии FK на Image +``` + +### Задача 4.2: Создание ImageMetadataInline +- [ ] StackedInline или TabularInline? +- [ ] Какие поля показывать: `i_img_sort`, `l_img_source`, `l_img_reality`, `f_img_confidence_score` +- [ ] Read-only поля? Или всё редактируемо? + +### Задача 4.3: Обновление админок моделей +- [ ] TbArticleAdmin — добавить ImageMetadataInline +- [ ] **TbItemAdmin — добавить ImageMetadataInline для обложки (cover image)** + - Поле `k_item_to_image` — ForeignKey с инлайном ImageMetadata + - ImageMetadata: `l_img_reality` должно быть read-only = `'abstract'` +- [ ] **TbOfferAdmin — добавить связь к изображениям с множественным выбором** + - M2M к filer.Image с инлайном ImageMetadata + - ImageMetadata: `l_img_reality` редактируемо (админ может менять на `'real'`) +- [ ] [Все остальные администраторы с изображениями] + +--- + +## Фаза 5: Парсеры и интеграция + +### Задача 5.1: Обновление парсеров +- [ ] Найти все парсеры, которые создают TbImage +- [ ] Обновить их для создания filer.Image вместо TbImage +- [ ] Парсеры должны заполнять ImageMetadata с правильными значениями: + - `l_img_source = 'parser'` + - `l_img_reality` — зависит от контекста: + - Для **TbItem**: `l_img_reality = 'abstract'` (обложка из внешних источников, Discogs, etc) + - Для **TbOffer**: `l_img_reality = 'abstract'` (парсер берет из каталога) + - `f_img_confidence_score = [значение из парсера]` + - `i_img_sort = 0` (дефолт) + +### Задача 5.1.1: Логика l_img_reality в разных контекстах +- [ ] **TbItem → Image (обложка альбома):** всегда `'abstract'` + - Signal: при создании ImageMetadata для TbItem → принудительно устанавливать `l_img_reality = 'abstract'` + - При редактировании в админке: поле `l_img_reality` должно быть read-only для TbItem +- [ ] **TbOffer → Image (фото товара в админке):** + - Дефолт при создании: `l_img_reality = 'real'` (человек загружает реальные фото) + - Signal: при создании ImageMetadata через админку → `l_img_reality = 'real'` +- [ ] **TbOffer → Image (парсер):** + - Дефолт при создании через парсер: `l_img_reality = 'abstract'` + - Signal: при создании ImageMetadata через парсер → `l_img_reality = 'abstract'` + - **Нужна логика определения источника:** как система узнает создана ли ImageMetadata парсером или админкой? + +### Задача 5.2: Обновление других интеграций +- [ ] API endpoints (если есть) +- [ ] Скрипты обслуживания +- [ ] Любая логика, которая работает с изображениями + +--- + +## Фаза 6: Тестирование и валидация + +### Задача 6.1: Unit тесты +- [ ] Тест создания filer.Image → автоматическое создание ImageMetadata +- [ ] Тест обновления ImageMetadata → синхронизация дат в filer.Image +- [ ] Тест удаления ImageMetadata → filer.Image остаётся +- [ ] Тест удаления filer.Image → каскадное удаление ImageMetadata + +### Задача 6.2: Интеграционные тесты +- [ ] Admin interface — создание/редактирование с инлайном +- [ ] Парсеры — создание корректных записей +- [ ] Шаблоны — корректный доступ к метаданным через `image.metadata.i_img_sort` + +### Задача 6.3: Миграция данных (если база содержит данные) +- [ ] Запустить миграции +- [ ] Проверить целостность данных +- [ ] Проверить что все ImageMetadata связаны с filer.Image +- [ ] Проверить обратные связи работают + +--- + +## Фаза 7: Финализация + +### Задача 7.1: Удаление старого кода +- [ ] Удалить все ссылки на старый TbImage (если полностью переименовали на ImageMetadata) +- [ ] Очистить импорты +- [ ] Обновить комментарии и документацию + +### Задача 7.2: Документирование +- [ ] Обновить ER-диаграмму в моделях +- [ ] Описать новую архитектуру в README или docs +- [ ] Примеры использования для разработчиков + +### Задача 7.3: Развёртывание +- [ ] Code review +- [ ] Тестирование на staging +- [ ] Развёртывание на production с миграциями +- [ ] Мониторинг логов на ошибки + +--- + +## Потенциальные ловушки + +⚠️ **Определение источника создания ImageMetadata (админка vs парсер)** +Решение: добавить context или flag в сигнал, или использовать user_id в сигнале (если user_id = None → парсер, иначе админка) + +⚠️ **TbItem.l_img_reality всегда должна быть 'abstract'** +Решение: signal принудительно устанавливает и делает read-only в админке + +⚠️ **TbOffer может иметь как свои картинки, так и наследовать из TbItem** +Решение: M2M на filer.Image, админ может выбирать любые картинки и устанавливать l_img_reality на месте + +--- + +## Оценка сложности + +| Фаза | Сложность | Время (ч) | Приоритет | +|------|-----------|-----------|-----------| +| 1 | Низкая | 0.5 | Высокий | +| 2 | Средняя | 2-3 | Высокий | +| 3 | Средняя | 1-2 | Высокий | +| 4 | Низкая | 1 | Высокий | +| 5 | Средняя | 1-2 | Высокий | +| 6 | Средняя | 1-2 | Средний | +| 7 | Низкая | 1 | Средний | + +**Итого:** ~8-13 часов + +--- + +## Уточнения из ответов + +✅ **1. Модель называется:** `TbImageMetadata` (переименуем TbImage) +✅ **2. Про performa & filer:** +- filer имеет две таблицы: File + Image (Image extends File через OneToOne) +- modified_at находится в таблице File (базовая для всех файлов) +- TbImageMetadata расширяет Image через OneToOne = это ОК, перформанс не критичен +- **Риск цикличности НИЗКИЙ:** обновляем ТОЛЬКО дату, не весь объект + +✅ **3. M2M на TbOffer:** +- `k_offer_to_image` это M2M (Django автоматически создает промежуточную таблицу) +- ОК, оставляем M2M + +✅ **4. Парсеры:** +- Создаются ПОЗЖЕ (в отдельном спринте) +- Сейчас делаем инфраструктуру для них +- Парсеры будут работать с filer напрямую, заполнять ImageMetadata вручную (или через свои сигналы позже) + +✅ **5. Цикличность:** +- Филер обновляет modified_at при изменении файла — ОК, автоматическое +- Мы обновляем modified_at при изменении метаданных — **ТОЛЬКО дату**, не вызовет cascade +- **Цикличности НЕ будет** если правильно реализовать сигнал (через `update()`, не `save()`) + +✅ **6. Определение источника сигнала (админка vs парсер):** +- **Решение:** Передавать через `using` параметр или context manager в signal'е +- **Для парсеров:** Парсеры могут создавать ImageMetadata вручную (без сигналов) или отключать сигналы +- **Для админки:** Сигналы работают стандартно + +✅ **7. Миграцию делаешь сам** — ясно! + +--- + +## ФИНАЛЬНЫЙ ПЛАН: ПОРЯДОК РЕАЛИЗАЦИИ + +### ЭТАП 1: ПОДГОТОВКА И МОДЕЛИ (БЕЗ МИГРАЦИЙ) + +**Шаг 1.1:** Обновить модель TbImage → TbImageMetadata +- Переименовать класс с `TbImage` на `TbImageMetadata` +- Добавить `related_name='metadata'` на FilerImageField ← **ВСТРЕЧНЫЙ ИНДЕКС! Упростит миграции** +- **ПОКА ОСТАВИТЬ** t_img_created, t_img_updated (удалим после миграции данных) +- **НЕ создавать** миграцию на этом этапе + +**Шаг 1.2:** Обновить все FK на картинки в моделях +- TbArticle: `k_article_to_image` ForeignKey(TbImage) → ForeignKey(filer.Image) +- **TbItem: добавить новое поле** `k_item_to_image = ForeignKey(filer.Image, blank=True, null=True)` +- TbOffer: `k_offer_to_image` M2M(TbImage) → M2M(filer.Image) +- Все остальные модели с картинками +- **НЕ создавать** миграции на этом этапе! Только обновляем код + +**Шаг 1.3:** Обновить админку (без инлайнов пока) +- Обновить все FieldChoices в админке на новые FK +- **НЕ добавлять** ImageMetadataInline пока! +- **НЕ добавлять** миксин пока! +- Просто чтобы админка не ломалась с новыми FK + +**РЕЗУЛЬТАТ:** Код скомпилирован, но БД не трогали. Всё готово к миграциям. + +--- + +### ЭТАП 2: МИГРАЦИИ (ТЫ ДЕЛАЕШЬ САМИ) + +**Шаг 2.1:** Создать миграции Django +- Миграция на переименование класса TbImage → TbImageMetadata (если нужно переименовать таблицу, то custom миграция) +- Миграция на удаление старых FK из TbArticle, TbOffer и т.д. +- Миграция на добавление новых FK/M2M на filer.Image +- Миграция на добавление новых полей (TbItem.k_item_to_image и т.д.) + +**Шаг 2.2:** Миграция данных +- Перенести существующие картинки из TbImage в filer.Image (если нужно) +- Привязать существующие записи к новым FK +- **Или:** Просто очистить TbImage и начать с нуля (если картинок мало) + +**Шаг 2.3:** Удалить t_img_created и t_img_updated ПОСЛЕ успешной миграции +- Отдельная миграция на удаление этих двух полей +- Применяется ПОСЛЕ того как все данные перенесены и связи обновлены +- **ПОРЯДОК ВАЖЕН:** сначала новые FK/M2M, потом удаляем старые поля + +**РЕЗУЛЬТАТ:** БД в новом состоянии, приложение рабочее (но без автоматизации через сигналы). + +--- + +### ЭТАП 3: СИГНАЛЫ И АДМИНКА (АВТОМАТИЗАЦИЯ) + +**Шаг 3.1:** Создать Signal для auto-create TbImageMetadata +``` +Когда: post_save filer.Image, created=True +Делает: автоматически создавать TbImageMetadata с дефолтами +Дефолты: i_img_sort=0, l_img_source='manual', l_img_reality зависит от контекста +``` + +**Шаг 3.2:** Создать Signal для синхронизации дат +``` +Когда: post_save TbImageMetadata +Делает: обновляет filer.File.modified_at через SQL update (БЕЗ save()!) +Важно: modified_at из File, а не Image! +``` + +**Шаг 3.3:** Создать миксин для админки +``` +ImageMetadataAdminMixin: автоматически добавляет инлайн для TbImageMetadata +Применяется к: TbArticleAdmin, TbOfferAdmin, и всем остальным +``` + +**Шаг 3.4:** Обновить админку с инлайнами +- TbArticleAdmin добавить миксин +- TbItemAdmin добавить миксин + читать-только l_img_reality для обложек +- TbOfferAdmin добавить миксин + M2M выбор картинок + l_img_reality редактируемо +- Остальные модели с картинками + +**РЕЗУЛЬТАТ:** Всё работает автоматически. Юзер создает/редактирует картинки — метаданные появляются и синхронизируются. + +--- + +### ЭТАП 4: ПОДГОТОВКА К ПАРСЕРАМ (FUTURE-PROOF) + +**Шаг 4.1:** Документировать как парсерам использовать новую архитектуру +- Парсеры создают filer.Image напрямую +- TbImageMetadata автоматически создается через сигнал +- Парсеры могут переопределять значения через direct update + +**Шаг 4.2:** Создать helper для парсеров (optional) +``` +Функция: create_image_from_url(url, l_img_source, l_img_reality, ...) +Делает: загружает в filer.Image + заполняет TbImageMetadata правильно +Это обычно нужно потом, но архитектура к этому готова +``` + +**Шаг 4.3:** Отключение сигналов для парсеров (если нужно) +``` +Если парсер хочет создавать без сигналов: +- Парсер может установить какой-то флаг (например, signals.disconnect()) +- Или парсер создает все сам без сигналов +- ИЛИ передавать context через signal'ы (позже если понадобится) +``` + +**РЕЗУЛЬТАТ:** Архитектура готова для будущих парсеров. Они смогут работать либо через сигналы, либо вручную. + +--- + +## УТОЧНЕНИЕ: КАРТИНКИ ТОЛЬКО В TBIMAGE + +**Важный факт:** Все картинки сейчас в filer привязаны ТОЛЬКО к TbImage, больше никуда не привязаны. + +**Это упрощает миграции!** +- Нет данных в других моделях которые нужно конвертировать +- `related_name='metadata'` на FilerImageField даст встречный доступ: `filer_image.metadata` +- Миграция будет проще: просто переименовать таблицу, добавить новые FK структуры, готово + +**Шаги миграции будут:** +1. Переименовать TbImage → TbImageMetadata (в таблице) +2. Добавить новые поля для TbItem.k_item_to_image +3. Очистить/переинициализировать существующие данные если нужно +4. ПОСЛЕ всего этого — удалить t_img_created, t_img_updated + +--- + +## ОТВЕТ НА ВОПРОС: КОГДА УДАЛЯТЬ t_img_created И t_img_updated? + +**Правильный порядок:** + +1. **Этап 1 (код):** Оставить поля в коде модели (пока!) +2. **Этап 2 (миграции) — ПЕРВЫЕ миграции:** + - Переименование TbImage → TbImageMetadata + - Добавление новых FK структур + - Миграция данных +3. **Этап 2 (миграции) — ПОСЛЕДНЯЯ миграция:** + - Удалить t_img_created и t_img_updated из БД +4. **ПОТОМ, Этап 1 (код):** Удалить эти поля из кода модели + +**ПОЧЕМУ такой порядок?** +- Сначала применяем миграции НОВЫХ структур — убеждаемся что всё работает +- ПОТОМ удаляем старые поля — когда новая структура стабильна +- Если что-то пошло не так — есть откат через старые поля + +**ПРАКТИЧЕСКИ:** +- Этап 1: TbImageMetadata всё ещё имеет `t_img_created`, `t_img_updated` в коде +- Ты создаёшь первую миграцию (переименование, новые FK) +- Ты создаёшь вторую миграцию (удаление дат из БД) +- Применяешь обе +- ПОТОМ я удаляю эти поля из самого кода модели (простое удаление из файла) + +1. **ЭТАП 1 (2-3 часа)** — Обновить код моделей и админки, БД не трогаем +2. **ЭТАП 2 (30 мин - 2 часа)** — Написать миграции и применить их (ты делаешь!) +3. **ЭТАП 3 (2-3 часа)** — Написать сигналы, админка миксины, протестировать +4. **ЭТАП 4 (30 мин)** — Документировать для будущих парсеров + +**ОБЩЕЕ ВРЕМЯ:** ~7-9 часов + +--- + +## КЛЮЧЕВЫЕ МОМЕНТЫ + +🔑 **Этап 1 БЕЗ МИГРАЦИЙ** — сначала весь код готовый, потом БД меняем +🔑 **Этап 2 ТЫ ДЕЛАЕШЬ** — я не трогаю, ты контролируешь +🔑 **Сигналы ТОЛЬКО ПОСЛЕ миграций** — иначе конфликты +🔑 **Цикличность ИСКЛЮЧЕНА** — обновляем только дату через SQL update() +🔑 **Парсеры готовы** — архитектура к ним готова, но коды парсеров делаешь позже +🔑 **Админка будет удобная** — один inline с метаданными внутри FK выбора + +--- + +**Готов начинать с ЭТАПА 1?**