28 KiB
План миграции: 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'на FilerImageFieldimage = 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
# В 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: Обратный сигнал — синхронизация дат (опционально)
# В 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
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с любыми данными:{ "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
- TbItem → картинка? → j_img_metadata =
Задача 5.2: Миграция существующих данных (ТЫ ДЕЛАЕШЬ)
- Перенести все TbImage в TbImageMetadata (переименование таблицы)
- Заполнить
i_img_sortиз старого поля (если было) j_img_metadataзаполнить на основе старых полей если они важны{ "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_imageForeignKey(TbImage) → ForeignKey(filer.Image) - TbItem: добавить новое поле
k_item_to_image = ForeignKey(filer.Image, blank=True, null=True) - TbOffer:
k_offer_to_imageM2M(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 структуры, готово
Шаги миграции будут:
- Переименовать TbImage → TbImageMetadata (в таблице)
- Добавить новые поля для TbItem.k_item_to_image
- Очистить/переинициализировать существующие данные если нужно
- ПОСЛЕ всего этого — удалить t_img_created, t_img_updated
ОТВЕТ НА ВОПРОС: КОГДА УДАЛЯТЬ t_img_created И t_img_updated?
Правильный порядок:
- Этап 1 (код): Оставить поля в коде модели (пока!)
- Этап 2 (миграции) — ПЕРВЫЕ миграции:
- Переименование TbImage → TbImageMetadata
- Добавление новых FK структур
- Миграция данных
- Этап 2 (миграции) — ПОСЛЕДНЯЯ миграция:
- Удалить t_img_created и t_img_updated из БД
- ПОТОМ, Этап 1 (код): Удалить эти поля из кода модели
ПОЧЕМУ такой порядок?
- Сначала применяем миграции НОВЫХ структур — убеждаемся что всё работает
- ПОТОМ удаляем старые поля — когда новая структура стабильна
- Если что-то пошло не так — есть откат через старые поля
ПРАКТИЧЕСКИ:
- Этап 1: TbImageMetadata всё ещё имеет
t_img_created,t_img_updatedв коде - Ты создаёшь первую миграцию (переименование, новые FK)
- Ты создаёшь вторую миграцию (удаление дат из БД)
- Применяешь обе
- ПОТОМ я удаляю эти поля из самого кода модели (простое удаление из файла)
- ЭТАП 1 (2-3 часа) — Обновить код моделей и админки, БД не трогаем
- ЭТАП 2 (30 мин - 2 часа) — Написать миграции и применить их (ты делаешь!)
- ЭТАП 3 (2-3 часа) — Написать сигналы, админка миксины, протестировать
- ЭТАП 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:
{
"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? Или еще вопросы/уточнения?