diff --git a/agent-reports/20260707_2109_migration_tbimage_to_imagemetadata.md b/agent-reports/20260707_2109_migration_tbimage_to_imagemetadata.md deleted file mode 100644 index 87c262e..0000000 --- a/agent-reports/20260707_2109_migration_tbimage_to_imagemetadata.md +++ /dev/null @@ -1,496 +0,0 @@ -# План миграции: 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: Переименование и упрощение TbImageMetadata - -Архитектура: **МИНИМАЛИЗМ + JSON для гибкости** - -- [ ] Переименовать класс TbImage → TbImageMetadata -- [ ] Добавить/проверить `related_name='metadata'` на FilerImageField - ```python - image = FilerImageField( - ..., - related_name='metadata' # ← для доступа: filer_image.metadata - ) - ``` -- [ ] **ОСТАВИТЬ в TbImageMetadata:** - - `image` (OneToOneField → filer.Image) - - `i_img_sort` (IntegerField, db_index=True) ← ТОЛЬКО это индексируется! - - `j_img_metadata` (JSONField, null=True, blank=True) ← всё остальное сюда - -- [ ] **УДАЛИТЬ ВСЕ остальные поля:** - - ❌ `l_img_source` (будет в JSON если нужно) - - ❌ `l_img_reality` (дефолт по контексту: TbItem→abstract, TbOffer админка→real, иное→abstract) - - ❌ `s_img_src_url` (в JSON если нужно) - - ❌ `f_img_confidence_score` (в JSON если нужно, парсеры его заполнят) - - ❌ `t_img_created` (используем filer.File.uploaded_at) - - ❌ `t_img_updated` (используем filer.File.modified_at) - -- [ ] Обновить `Meta.verbose_name` на "Метаданные изображения" -- [ ] Обновить `Meta.ordering` — просто `('i_img_sort',)` - -**ПОЧЕМУ?** -- Парсеров еще нет, не знаем какие поля реально нужны -- Справочные поля (источник, тип, достоверность) не нужны для индексации/фильтрации -- JSON дает гибкость: позже когда парсеры появятся, просто добавим новые ключи или выносим в таблицу если нужен индекс -- YAGNI: не создаем сложность пока она не нужна - -### Задача 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 TbImageMetadata -```python -# В apps.py -@receiver(post_save, sender=filer.models.Image) -def auto_create_image_metadata(sender, instance, created, **kwargs): - """При создании filer.Image автоматически создавать TbImageMetadata""" - if created: - TbImageMetadata.objects.get_or_create( - image=instance, - defaults={ - 'i_img_sort': 0, - # j_img_metadata оставляем пустым, заполняется по мере необходимости - } - ) -``` - -### Задача 3.2: Обратный сигнал — синхронизация дат (опционально) -```python -# В apps.py — ЕСЛИ нужно обновлять modified_at в filer при изменении i_img_sort -@receiver(post_save, sender=TbImageMetadata) -def sync_image_modification_date(sender, instance, **kwargs): - """При обновлении i_img_sort обновляем modified_at в filer.File""" - # Обновляем filer.File.modified_at через SQL update (без повторного save()!) - from filer.models import File - File.objects.filter(id=instance.image.id).update(modified_at=now()) -``` - -**ПРИМЕЧАНИЕ:** Сигнал опционален. Если не нужно обновлять дату каждый раз при изменении сортировки — можно пропустить. - -### Задача 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: Создание TbImageMetadataInline -- [ ] StackedInline для TbImageMetadata -- [ ] Поля: `i_img_sort`, `j_img_metadata` -- [ ] `j_img_metadata` как JSON editor (админка Django поддерживает JSONField) -- [ ] Всё просто и минималистично - -### Задача 4.3: Обновление админок моделей -- [ ] TbArticleAdmin — добавить TbImageMetadataAdminMixin -- [ ] **TbItemAdmin — добавить мixin для обложки** - - Поле `k_item_to_image` с инлайном TbImageMetadata - - `i_img_sort` редактируемо - - `j_img_metadata` может содержать замечания ("абстрактная", "с Discogs" и т.д.) если нужно -- [ ] **TbOfferAdmin — добавить мixin для картинок товара** - - M2M к filer.Image с инлайном TbImageMetadata - - `i_img_sort` редактируемо (для сортировки фото товара) - - `j_img_metadata` редактируемо (админ может добавить замечания) -- [ ] [Все остальные администраторы с изображениями] - ---- - -## Фаза 5: Парсеры и интеграция - -### Задача 5.1: Подготовка к парсерам (FUTURE-PROOF) -- [ ] Найти все парсеры (их еще нет, но логика готова) -- [ ] Парсеры смогут заполнять `j_img_metadata` с любыми данными: - ```json - { - "source": "discogs", - "reality": "abstract", - "confidence": 0.95, - "original_url": "https://...", - "notes": "..." - } - ``` -- [ ] `i_img_sort` может устанавливаться парсером если нужна специальная сортировка -- [ ] **Дефолтная логика (без парсера):** - - TbItem → картинка? → j_img_metadata = `{"reality": "abstract"}` - - TbOffer + админка → картинка? → j_img_metadata пустой или `{}` - - На фронте: если не указано → apply defaults - -### Задача 5.2: Миграция существующих данных (ТЫ ДЕЛАЕШЬ) -- [ ] Перенести все TbImage в TbImageMetadata (переименование таблицы) -- [ ] Заполнить `i_img_sort` из старого поля (если было) -- [ ] `j_img_metadata` заполнить на основе старых полей если они важны - ```json - { - "old_l_img_source": "...", - "old_l_img_reality": "...", - "old_confidence": "..." - } - ``` -- [ ] Или просто пусто, парсеры потом заполнят правильно - ---- - -## Фаза 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 часов - ---- - -## НОВАЯ АРХИТЕКТУРА: МИНИМАЛИЗМ + JSON - -**TbImageMetadata содержит:** -- `image` (OneToOne → filer.Image) -- `i_img_sort` (IntegerField, db_index=True) ← **ТОЛЬКО поле с индексом!** -- `j_img_metadata` (JSONField, null=True) ← **Всё остальное сюда** - -**Почему?** -- ✅ Парсеров еще нет → не знаем какие поля реально нужны -- ✅ Справочные данные не используются для фильтрации/индексации -- ✅ JSON гибко хранит всё: источник, тип, достоверность, URL и т.д. -- ✅ Если потом парсеры потребуют индексы → просто выносим поле из JSON -- ✅ Дефолтная логика: TbItem → abstract, TbOffer админка → любое значение - -**Пример j_img_metadata:** -```json -{ - "source": "parser", - "reality": "abstract", - "confidence": 0.95, - "original_url": "https://discogs.com/...", - "notes": "Обложка из каталога" -} -``` - ---- - -## ИТОГОВЫЙ ПОРЯДОК РЕАЛИЗАЦИИ (ОБНОВЛЕНО) - -🔑 **Этап 1 БЕЗ МИГРАЦИЙ** — сначала весь код готовый, потом БД меняем -🔑 **Этап 2 ТЫ ДЕЛАЕШЬ** — я не трогаю, ты контролируешь миграции -🔑 **Архитектура МИНИМАЛИСТИЧНА** — только i_img_sort индексируется, всё остальное в JSON -🔑 **Гибкость МАКСИМАЛЬНАЯ** — JSON позволяет добавлять любые данные без миграций БД -🔑 **Парсеры готовы** — при создании просто заполняют j_img_metadata как нужно -🔑 **Дефолтная логика** — на фронте если метаданных нет → применяем defaults по контексту - ---- - -**Готов начинать с ЭТАПА 1?** Или еще вопросы/уточнения?