Удалены файлы документации по изображениям
This commit is contained in:
@@ -1,397 +0,0 @@
|
||||
# Руководство по использованию изображений на фронтенде
|
||||
|
||||
## Обзор
|
||||
|
||||
Система автоматически служит изображения разных размеров в зависимости от контекста:
|
||||
|
||||
- **Списки товаров**: миниатюры (150×150) - быстрая загрузка
|
||||
- **Карточки товаров**: средний размер (400×400) - хороший баланс
|
||||
- **Галереи и модальные окна**: большой размер (800×800) - высокое качество
|
||||
- **Полноэкранный просмотр**: оригинал - максимальное качество
|
||||
|
||||
## Использованные размеры в шаблонах
|
||||
|
||||
### 1. all_products_list.html (Объединённый список товаров и комплектов)
|
||||
|
||||
```django
|
||||
<!-- Список с миниатюрами -->
|
||||
<img src="{{ photo.get_thumbnail_url }}" alt="{{ item.name }}" class="img-thumbnail">
|
||||
```
|
||||
|
||||
**Размер на диске**: 438B (93% экономия)
|
||||
**Использование**: Таблица со списком товаров, быстрая загрузка
|
||||
|
||||
---
|
||||
|
||||
### 2. product_list.html (Список только товаров)
|
||||
|
||||
```django
|
||||
<!-- В таблице - миниатюры -->
|
||||
<img src="{{ photo.get_thumbnail_url }}" alt="{{ product.name }}" class="img-thumbnail">
|
||||
```
|
||||
|
||||
**Размер на диске**: 438B (93% экономия)
|
||||
**Использование**: Табличное отображение товаров
|
||||
|
||||
---
|
||||
|
||||
### 3. productkit_list.html (Список комплектов)
|
||||
|
||||
```django
|
||||
<!-- В таблице - миниатюры -->
|
||||
<img src="{{ photo.get_thumbnail_url }}" alt="{{ kit.name }}" class="img-thumbnail">
|
||||
```
|
||||
|
||||
**Размер на диске**: 438B (93% экономия)
|
||||
**Использование**: Табличное отображение комплектов
|
||||
|
||||
---
|
||||
|
||||
### 4. product_detail.html (Детали товара с галереей)
|
||||
|
||||
**Миниатюры в сетке:**
|
||||
```django
|
||||
<!-- Сетка с thumbnail размерами -->
|
||||
<img src="{{ photo.get_thumbnail_url }}"
|
||||
alt="Фото товара"
|
||||
style="max-width: 100%; max-height: 100%; object-fit: contain;">
|
||||
```
|
||||
|
||||
**Файл**: 438B
|
||||
**Использование**: Маленькие превью для клика
|
||||
|
||||
---
|
||||
|
||||
**Модальное окно галереи (carousel):**
|
||||
```django
|
||||
<!-- В модальном окне - большие размеры 800x800 -->
|
||||
<img src="{{ photo.get_large_url }}"
|
||||
class="d-block"
|
||||
alt="Фото товара"
|
||||
style="max-height: 70vh; max-width: 100%; object-fit: contain;">
|
||||
```
|
||||
|
||||
**Файл**: 5.6K
|
||||
**Использование**: Полноэкранный просмотр, хорошее качество
|
||||
|
||||
---
|
||||
|
||||
### 5. productkit_detail.html (Детали комплекта)
|
||||
|
||||
**Боковая панель с фото:**
|
||||
```django
|
||||
<!-- Средний размер для превью -->
|
||||
<img src="{{ photo.get_medium_url }}"
|
||||
class="card-img-top"
|
||||
alt="{{ kit.name }}"
|
||||
style="height: 120px; object-fit: cover; cursor: pointer;">
|
||||
```
|
||||
|
||||
**Файл**: 2.9K
|
||||
**Использование**: Кликабельные превью в боку
|
||||
|
||||
---
|
||||
|
||||
**Модальное окно при клике:**
|
||||
```django
|
||||
<!-- Большой размер в модальном окне -->
|
||||
<img src="{{ photo.get_large_url }}"
|
||||
class="img-fluid"
|
||||
style="max-height: 70vh;">
|
||||
```
|
||||
|
||||
**Файл**: 5.6K
|
||||
**Использование**: Полноэкранный просмотр одного фото
|
||||
|
||||
---
|
||||
|
||||
### 6. category_detail.html (Детали категории)
|
||||
|
||||
```django
|
||||
<!-- Средний размер для отображения -->
|
||||
<img src="{{ photo.get_medium_url }}"
|
||||
class="card-img-top"
|
||||
alt="Фото категории"
|
||||
style="height: 150px; object-fit: cover;">
|
||||
```
|
||||
|
||||
**Файл**: 2.9K
|
||||
**Использование**: Картинка категории в сетке
|
||||
|
||||
---
|
||||
|
||||
## Доступные методы
|
||||
|
||||
Каждая Photo-модель имеет методы для получения URL разных размеров:
|
||||
|
||||
```python
|
||||
photo = ProductPhoto.objects.first()
|
||||
|
||||
# Получить URL миниатюры (150×150)
|
||||
photo.get_thumbnail_url()
|
||||
# → /media/products/thumbnails/image_12345.jpg (438B)
|
||||
|
||||
# Получить URL среднего размера (400×400)
|
||||
photo.get_medium_url()
|
||||
# → /media/products/medium/image_12345.jpg (2.9K)
|
||||
|
||||
# Получить URL большого размера (800×800)
|
||||
photo.get_large_url()
|
||||
# → /media/products/large/image_12345.jpg (5.6K)
|
||||
|
||||
# Получить URL оригинала (без изменений)
|
||||
photo.get_original_url()
|
||||
# → /media/products/originals/image_12345.jpg (6.1K)
|
||||
|
||||
# Получить все URL за раз
|
||||
photo.get_all_urls()
|
||||
# → {
|
||||
# 'thumbnail': '/media/products/thumbnails/...',
|
||||
# 'medium': '/media/products/medium/...',
|
||||
# 'large': '/media/products/large/...',
|
||||
# 'original': '/media/products/originals/...'
|
||||
# }
|
||||
```
|
||||
|
||||
То же самое для ProductKitPhoto и ProductCategoryPhoto.
|
||||
|
||||
---
|
||||
|
||||
## Рекомендации по использованию
|
||||
|
||||
### Списки и таблицы
|
||||
```django
|
||||
<!-- Миниатюры для быстрой загрузки -->
|
||||
{% if item.photos.all %}
|
||||
<img src="{{ item.photos.first.get_thumbnail_url }}"
|
||||
alt="{{ item.name }}"
|
||||
style="max-width: 50px; max-height: 50px;">
|
||||
{% endif %}
|
||||
```
|
||||
|
||||
**Почему**: Список может содержать 50+ товаров, миниатюры загружаются мгновенно
|
||||
|
||||
---
|
||||
|
||||
### Карточки товаров
|
||||
```django
|
||||
<!-- Средний размер для карточек -->
|
||||
<div class="product-card">
|
||||
<img src="{{ product.photos.first.get_medium_url }}"
|
||||
alt="{{ product.name }}">
|
||||
</div>
|
||||
```
|
||||
|
||||
**Почему**: Карточка требует хорошего качества, но не нужна полная 800×800
|
||||
|
||||
---
|
||||
|
||||
### Модальные окна и галереи
|
||||
```django
|
||||
<!-- Большой размер для полного просмотра -->
|
||||
<div class="modal-body">
|
||||
<img src="{{ photo.get_large_url }}"
|
||||
alt="{{ product.name }}"
|
||||
style="max-width: 100%; max-height: 70vh;">
|
||||
</div>
|
||||
```
|
||||
|
||||
**Почему**: Пользователь просматривает один товар, качество важнее
|
||||
|
||||
---
|
||||
|
||||
### Ссылка на оригинал
|
||||
```django
|
||||
<!-- Для скачивания оригинала -->
|
||||
<a href="{{ photo.get_original_url }}"
|
||||
download>
|
||||
Скачать оригинал
|
||||
</a>
|
||||
```
|
||||
|
||||
**Почему**: Для печати или отправки по почте нужно максимальное качество
|
||||
|
||||
---
|
||||
|
||||
## Производительность
|
||||
|
||||
### Пример загрузки страницы:
|
||||
|
||||
**Список товаров (20 товаров)**:
|
||||
- 20 × 438B = 8.76 КБ
|
||||
- Время загрузки: ~100мс на медленном 3G
|
||||
|
||||
**VS** если бы были оригиналы:
|
||||
- 20 × 6.1K = 122 КБ
|
||||
- Время загрузки: ~1.2сек на медленном 3G
|
||||
|
||||
**Экономия: 93% трафика, 12× быстрее** ⚡
|
||||
|
||||
---
|
||||
|
||||
## Адаптивный дизайн
|
||||
|
||||
Система изображений работает на всех устройствах:
|
||||
|
||||
```django
|
||||
<!-- Адаптивное отображение -->
|
||||
<img src="{{ photo.get_thumbnail_url }}"
|
||||
alt="{{ product.name }}"
|
||||
class="img-fluid"
|
||||
style="max-width: 100%;">
|
||||
```
|
||||
|
||||
- На мобильных: 50px → выглядит хорошо
|
||||
- На планшетах: 50px → увеличение не требуется
|
||||
- На десктопе: 50px → оптимально
|
||||
|
||||
Для больших экранов используются большие размеры (800×800) в модальном окне.
|
||||
|
||||
---
|
||||
|
||||
## Примеры в JavaScript
|
||||
|
||||
Если нужно работать с изображениями через JavaScript:
|
||||
|
||||
```javascript
|
||||
// Получить URL через атрибут data
|
||||
const largeImageUrl = document.querySelector('[data-large-url]').dataset.largeUrl;
|
||||
|
||||
// Или из HTML через обычный селектор
|
||||
const img = document.querySelector('.product-image');
|
||||
const src = img.src; // /media/products/medium/image_12345.jpg
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Загрузка изображений
|
||||
|
||||
### Через админку:
|
||||
1. Откройте товар/комплект/категорию в админке
|
||||
2. В секции "Фото товара" загрузите изображение
|
||||
3. **Система автоматически создаст все 4 размера!**
|
||||
4. Фото появится во всех шаблонах с правильным размером
|
||||
|
||||
### Через API (если нужно):
|
||||
```python
|
||||
from products.models import ProductPhoto
|
||||
from products.utils.image_processor import ImageProcessor
|
||||
|
||||
photo = ProductPhoto(product=product)
|
||||
photo.image = request.FILES['image']
|
||||
photo.save() # Все размеры создадутся автоматически
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Кэширование и оптимизация
|
||||
|
||||
### Текущая реализация:
|
||||
- Каждый размер сохраняется на диск (быстро)
|
||||
- URL вычисляется динамически (не нужна дополнительная БД)
|
||||
|
||||
### Будущие оптимизации (опционально):
|
||||
- Redis кэширование URL
|
||||
- WebP формат для современных браузеров
|
||||
- Ленивая загрузка изображений (lazy loading)
|
||||
- CDN интеграция
|
||||
|
||||
---
|
||||
|
||||
## Поиск и устранение проблем
|
||||
|
||||
### Изображение не загружается
|
||||
1. Проверьте консоль браузера (F12 → Network)
|
||||
2. Правильный ли URL? (должен быть `/media/products/...`)
|
||||
3. Загруженное ли изображение в админке?
|
||||
|
||||
### Размер слишком мал/велик
|
||||
1. Используйте правильный метод:
|
||||
- `get_thumbnail_url()` → для списков
|
||||
- `get_medium_url()` → для карточек
|
||||
- `get_large_url()` → для галерей
|
||||
- `get_original_url()` → для оригинала
|
||||
|
||||
### Качество плохое
|
||||
- Используйте `get_large_url()` или `get_original_url()` вместо `get_thumbnail_url()`
|
||||
|
||||
---
|
||||
|
||||
## Примеры полных шаблонов
|
||||
|
||||
### Пример 1: Сетка товаров (Bootstrap)
|
||||
|
||||
```django
|
||||
<div class="row row-cols-1 row-cols-md-3 g-4">
|
||||
{% for product in products %}
|
||||
<div class="col">
|
||||
<div class="card h-100">
|
||||
<!-- Миниатюра - быстро загружается -->
|
||||
{% if product.photos.first %}
|
||||
<img src="{{ product.photos.first.get_thumbnail_url }}"
|
||||
class="card-img-top"
|
||||
alt="{{ product.name }}"
|
||||
loading="lazy">
|
||||
{% endif %}
|
||||
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ product.name }}</h5>
|
||||
<p class="card-text">{{ product.description|truncatewords:20 }}</p>
|
||||
<a href="{% url 'products:product-detail' product.pk %}"
|
||||
class="btn btn-primary">
|
||||
Открыть →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Пример 2: Галерея с модальным окном
|
||||
|
||||
```django
|
||||
<div class="row g-2">
|
||||
{% for photo in product.photos.all %}
|
||||
<div class="col-md-3">
|
||||
<!-- Кликабельная миниатюра -->
|
||||
<div class="card"
|
||||
role="button"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#photoModal{{ photo.pk }}"
|
||||
style="cursor: pointer;">
|
||||
<img src="{{ photo.get_thumbnail_url }}"
|
||||
class="card-img-top"
|
||||
alt="{{ product.name }}"
|
||||
style="height: 150px; object-fit: cover;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно с большим размером -->
|
||||
<div class="modal fade" id="photoModal{{ photo.pk }}">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<img src="{{ photo.get_large_url }}"
|
||||
class="img-fluid"
|
||||
alt="{{ product.name }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Итоги
|
||||
|
||||
✅ **Автоматическое масштабирование** - правильный размер в правильном месте
|
||||
✅ **Экономия трафика** - 90% для миниатюр
|
||||
✅ **Быстрая загрузка** - миниатюры 438B
|
||||
✅ **Высокое качество** - большие размеры 800×800 для просмотра
|
||||
✅ **Простой API** - всего 4 метода в шаблонах
|
||||
✅ **Полная автоматизация** - создание размеров при загрузке
|
||||
|
||||
Система готова к использованию! 🎉
|
||||
@@ -1,234 +0,0 @@
|
||||
# Отчет: Вынос конфигурации размеров и форматов изображений в settings
|
||||
|
||||
## Резюме
|
||||
|
||||
Успешно вынесена конфигурация размеров, форматов и качества изображений из кода в `settings.IMAGE_PROCESSING_CONFIG`. Система теперь поддерживает разные форматы (JPEG для оригинала, WebP для других размеров) с динамическими параметрами качества.
|
||||
|
||||
---
|
||||
|
||||
## Что было реализовано
|
||||
|
||||
### 1. ✓ Добавлена конфигурация в settings.py
|
||||
|
||||
**Файл:** `myproject/myproject/settings.py:130-173`
|
||||
|
||||
```python
|
||||
IMAGE_PROCESSING_CONFIG = {
|
||||
'formats': {
|
||||
'original': {
|
||||
'format': 'JPEG',
|
||||
'quality': 100,
|
||||
'max_width': 2160,
|
||||
'max_height': 2160,
|
||||
'description': 'Original image (4K max, JPEG format)'
|
||||
},
|
||||
'large': {
|
||||
'format': 'WEBP',
|
||||
'quality': 90,
|
||||
'width': 1200,
|
||||
'height': 1200,
|
||||
'description': 'Large image (1200x1200, WebP format)'
|
||||
},
|
||||
'medium': {
|
||||
'format': 'WEBP',
|
||||
'quality': 85,
|
||||
'width': 600,
|
||||
'height': 600,
|
||||
'description': 'Medium image (600x600, WebP format)'
|
||||
},
|
||||
'thumbnail': {
|
||||
'format': 'WEBP',
|
||||
'quality': 80,
|
||||
'width': 200,
|
||||
'height': 200,
|
||||
'description': 'Thumbnail (200x200, WebP format)'
|
||||
},
|
||||
},
|
||||
'folders': {
|
||||
'original': 'originals',
|
||||
'large': 'large',
|
||||
'medium': 'medium',
|
||||
'thumbnail': 'thumbnails',
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. ✓ Полностью переписан ImageProcessor
|
||||
|
||||
**Файл:** `myproject/products/utils/image_processor.py`
|
||||
|
||||
**Основные изменения:**
|
||||
- Все SIZES, SIZE_FOLDERS и JPEG_QUALITY теперь берутся из settings
|
||||
- Добавлены методы для динамического получения конфигурации:
|
||||
- `_get_config()` - получить конфигурацию из settings
|
||||
- `_get_size_dimensions(size_key)` - получить размеры для типа изображения
|
||||
- `_get_format_config(size_key)` - получить конфиг формата
|
||||
- `_get_folder(size_key)` - получить папку для сохранения
|
||||
|
||||
- Полностью переработан метод `_save_image_version()`:
|
||||
- Поддерживает разные форматы (JPEG, WebP, PNG)
|
||||
- Использует качество из конфигурации для каждого типа
|
||||
- Определяет расширение файла в зависимости от формата
|
||||
- Масштабирует оригинал если больше 2160×2160
|
||||
- **Делает изображение квадратным** (добавляет белый фон если нужно)
|
||||
|
||||
- Обновлена функция `delete_all_versions()`:
|
||||
- Учитывает разные расширения файлов при удалении
|
||||
- Работает с новой структурой конфигурации
|
||||
|
||||
### 3. ✓ Обновлен ImageService
|
||||
|
||||
**Файл:** `myproject/products/utils/image_service.py`
|
||||
|
||||
**Основные изменения:**
|
||||
- Добавлены методы для работы с конфигурацией:
|
||||
- `_get_config()` - получить конфигурацию
|
||||
- `_get_size_folders()` - получить папки из конфигурации
|
||||
- `_get_format_config(size_key)` - получить конфиг формата
|
||||
- `_get_file_extension(size_key)` - получить расширение для типа
|
||||
|
||||
- Полностью переработан метод `get_url()`:
|
||||
- Поддерживает разные расширения (.jpg, .webp, .png)
|
||||
- Корректно парсит имена файлов с разными расширениями
|
||||
- Генерирует правильный URL для каждого размера с его расширением
|
||||
|
||||
---
|
||||
|
||||
## Поддерживаемые форматы
|
||||
|
||||
| Формат | Тип | Поддержка | Примечание |
|
||||
|--------|-----|----------|-----------|
|
||||
| **JPEG** | Оригинал | ✓ | Качество: 100 |
|
||||
| **WebP** | Large, Medium, Thumbnail | ✓ | Оптимизация размера |
|
||||
| **PNG** | Все | ✓ | При необходимости |
|
||||
| **GIF** | Входной | ✓ | Конвертируется в RGB |
|
||||
| **TIFF** | Входной | ✓ | Конвертируется в RGB |
|
||||
| **HEIC** | Входной | ✓ | Конвертируется в RGB |
|
||||
|
||||
---
|
||||
|
||||
## Размеры и качество
|
||||
|
||||
| Тип | Размер | Формат | Качество |
|
||||
|-----|--------|--------|----------|
|
||||
| **Original** | 2160×2160 макс | JPEG | 100 |
|
||||
| **Large** | 1200×1200 | WebP | 90 |
|
||||
| **Medium** | 600×600 | WebP | 85 |
|
||||
| **Thumbnail** | 200×200 | WebP | 80 |
|
||||
|
||||
---
|
||||
|
||||
## Логика масштабирования оригинала
|
||||
|
||||
**Для оригинала (original):**
|
||||
1. Если размер больше 2160×2160 → масштабировать с сохранением пропорций
|
||||
2. Если размер меньше или равен 2160×2160 → оставить как есть
|
||||
3. **Всегда делать квадратным** → добавить белый фон если нужно
|
||||
|
||||
**Примеры:**
|
||||
- 3000×2000 → масштабируется до ≈2160×1440, потом до 2160×2160 (с белым фоном)
|
||||
- 1000×1000 → остается 1000×1000, потом до 1000×1000 квадратное (без изменений)
|
||||
- 1500×800 → остается 1500×800, потом до 1500×1500 (с белым фоном сверху/снизу)
|
||||
|
||||
---
|
||||
|
||||
## Примеры создаваемых файлов
|
||||
|
||||
### После загрузки изображения "robot-50cm":
|
||||
|
||||
```
|
||||
products/
|
||||
├── originals/
|
||||
│ └── robot-50cm_1729611234567_original.jpg (JPEG, качество 100)
|
||||
├── large/
|
||||
│ └── robot-50cm_1729611234567_large.webp (WebP, качество 90)
|
||||
├── medium/
|
||||
│ └── robot-50cm_1729611234567_medium.webp (WebP, качество 85)
|
||||
└── thumbnails/
|
||||
└── robot-50cm_1729611234567_thumbnail.webp (WebP, качество 80)
|
||||
```
|
||||
|
||||
### Примеры генерируемых URL:
|
||||
|
||||
```
|
||||
/media/products/originals/robot-50cm_1729611234567_original.jpg
|
||||
/media/products/large/robot-50cm_1729611234567_large.webp
|
||||
/media/products/medium/robot-50cm_1729611234567_medium.webp
|
||||
/media/products/thumbnails/robot-50cm_1729611234567_thumbnail.webp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Гибкость конфигурации
|
||||
|
||||
Теперь можно легко изменять параметры без изменения кода:
|
||||
|
||||
```python
|
||||
# Например, если нужно изменить качество large с 90 на 95:
|
||||
IMAGE_PROCESSING_CONFIG = {
|
||||
'formats': {
|
||||
'large': {
|
||||
'format': 'WEBP',
|
||||
'quality': 95, # ← измененное значение
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Или если нужен PNG для оригинала:
|
||||
'original': {
|
||||
'format': 'PNG', # ← вместо JPEG
|
||||
'quality': 100,
|
||||
...
|
||||
}
|
||||
|
||||
# Или для WebP оригинала с качеством 95:
|
||||
'original': {
|
||||
'format': 'WEBP', # ← вместо JPEG
|
||||
'quality': 95,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Тестирование
|
||||
|
||||
Конфигурация успешно протестирована в Django shell:
|
||||
|
||||
```
|
||||
✓ IMAGE_PROCESSING_CONFIG loaded successfully!
|
||||
✓ All format configurations present
|
||||
✓ ImageService generates correct URLs with proper extensions
|
||||
✓ WebP files use correct extensions (.webp)
|
||||
✓ Original uses JPEG format (.jpg)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Преимущества решения
|
||||
|
||||
1. **Гибкость** - параметры хранятся в settings, легко менять
|
||||
2. **Масштабируемость** - можно добавлять новые размеры без изменения кода
|
||||
3. **Производительность** - WebP вместо JPEG для меньших размеров уменьшает размер файлов на 20-30%
|
||||
4. **Качество** - JPEG качество 100 для оригинала гарантирует максимальное качество
|
||||
5. **Читаемость** - квадратные изображения более универсальны для использования
|
||||
6. **Совместимость** - поддержка всех популярных форматов при загрузке
|
||||
|
||||
---
|
||||
|
||||
## Файлы измененные
|
||||
|
||||
1. **myproject/myproject/settings.py** - добавлена IMAGE_PROCESSING_CONFIG
|
||||
2. **myproject/products/utils/image_processor.py** - полностью переработан для динамической конфигурации
|
||||
3. **myproject/products/utils/image_service.py** - обновлен для работы с разными расширениями
|
||||
|
||||
---
|
||||
|
||||
## Заключение
|
||||
|
||||
Система обработки изображений успешно переведена на конфигурируемую архитектуру. Все параметры (размеры, форматы, качество) теперь находятся в settings.IMAGE_PROCESSING_CONFIG и могут быть легко изменены без дополнительных изменений кода.
|
||||
|
||||
**Статус:** ✓ **ГОТОВО К ИСПОЛЬЗОВАНИЮ**
|
||||
|
||||
Новые загружаемые изображения будут автоматически обрабатываться согласно новой конфигурации.
|
||||
@@ -1,377 +0,0 @@
|
||||
# Стратегия хранения и обработки изображений
|
||||
|
||||
## Обзор системы
|
||||
|
||||
Система автоматически хранит одно большое оригинальное изображение и создает несколько оптимизированных версий для разных сценариев использования в приложении.
|
||||
|
||||
**Преимущества:**
|
||||
- ✅ Оригинальное изображение сохраняется в полном качестве
|
||||
- ✅ Автоматическое создание всех размеров при загрузке
|
||||
- ✅ Оптимизация под разные части приложения (списки, карточки, просмотр)
|
||||
- ✅ Быстрая загрузка из-за меньшего размера файлов
|
||||
- ✅ Экономия трафика и дискового пространства
|
||||
|
||||
---
|
||||
|
||||
## Размеры изображений
|
||||
|
||||
| Размер | Размер (px) | Использование |
|
||||
|--------|------------|---------------|
|
||||
| **thumbnail** | 150×150 | Каталоги, списки товаров, сетки |
|
||||
| **medium** | 400×400 | Карточки товаров, превью в админке |
|
||||
| **large** | 800×800 | Полноразмерный просмотр на фронте |
|
||||
| **original** | Без изменений* | Архив, печать, экспорт |
|
||||
|
||||
\* *Сохраняется в JPEG с качеством 90 для оптимизации*
|
||||
|
||||
---
|
||||
|
||||
## Структура хранения файлов
|
||||
|
||||
```
|
||||
media/
|
||||
├── products/
|
||||
│ ├── originals/ # Оригинальные изображения товаров
|
||||
│ │ └── product_name_12345.jpg
|
||||
│ ├── thumbnails/ # Миниатюры (150x150)
|
||||
│ │ └── product_name_12345.jpg
|
||||
│ ├── medium/ # Средние (400x400)
|
||||
│ │ └── product_name_12345.jpg
|
||||
│ └── large/ # Большие (800x800)
|
||||
│ └── product_name_12345.jpg
|
||||
│
|
||||
├── kits/
|
||||
│ ├── originals/ # Оригинальные изображения комплектов
|
||||
│ ├── thumbnails/
|
||||
│ ├── medium/
|
||||
│ └── large/
|
||||
│
|
||||
└── categories/
|
||||
├── originals/ # Оригинальные изображения категорий
|
||||
├── thumbnails/
|
||||
├── medium/
|
||||
└── large/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Использование в шаблонах (templates)
|
||||
|
||||
### Товары
|
||||
|
||||
```django
|
||||
{% load static %}
|
||||
|
||||
<!-- Список товаров - используем миниатюры -->
|
||||
<div class="product-grid">
|
||||
{% for product in products %}
|
||||
<div class="product-card">
|
||||
{% if product.photos.first %}
|
||||
<img src="{{ product.photos.first.get_thumbnail_url }}"
|
||||
alt="{{ product.name }}"
|
||||
class="product-thumbnail">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Карточка товара - используем средний размер -->
|
||||
<div class="product-detail-card">
|
||||
{% if product.photos.first %}
|
||||
<img src="{{ product.photos.first.get_medium_url }}"
|
||||
alt="{{ product.name }}"
|
||||
class="product-preview">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Полный просмотр - используем большой размер -->
|
||||
<div class="product-gallery">
|
||||
{% for photo in product.photos.all %}
|
||||
<div class="gallery-item">
|
||||
<img src="{{ photo.get_large_url }}"
|
||||
alt="{{ product.name }}"
|
||||
class="gallery-image">
|
||||
<a href="{{ photo.get_original_url }}"
|
||||
target="_blank"
|
||||
class="view-original">Оригинал</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Комплекты (букеты)
|
||||
|
||||
```django
|
||||
<!-- Список комплектов -->
|
||||
<img src="{{ kit.photos.first.get_thumbnail_url }}"
|
||||
alt="{{ kit.name }}">
|
||||
|
||||
<!-- Карточка комплекта -->
|
||||
<img src="{{ kit.photos.first.get_medium_url }}"
|
||||
alt="{{ kit.name }}">
|
||||
|
||||
<!-- Просмотр комплекта -->
|
||||
<img src="{{ kit.photos.first.get_large_url }}"
|
||||
alt="{{ kit.name }}">
|
||||
```
|
||||
|
||||
### Категории
|
||||
|
||||
```django
|
||||
<!-- Превью категории -->
|
||||
<img src="{{ category.photos.first.get_thumbnail_url }}"
|
||||
alt="{{ category.name }}">
|
||||
|
||||
<!-- Детальное изображение категории -->
|
||||
<img src="{{ category.photos.first.get_medium_url }}"
|
||||
alt="{{ category.name }}">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Использование в представлениях (views)
|
||||
|
||||
```python
|
||||
from products.models import Product, ProductPhoto
|
||||
|
||||
def product_detail(request, pk):
|
||||
product = Product.objects.get(pk=pk)
|
||||
|
||||
# Получить первое фото
|
||||
main_photo = product.photos.first()
|
||||
|
||||
if main_photo:
|
||||
context = {
|
||||
'product': product,
|
||||
'thumbnail_url': main_photo.get_thumbnail_url(),
|
||||
'medium_url': main_photo.get_medium_url(),
|
||||
'large_url': main_photo.get_large_url(),
|
||||
'original_url': main_photo.get_original_url(),
|
||||
}
|
||||
|
||||
return render(request, 'product_detail.html', context)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API фото моделей
|
||||
|
||||
### Методы для получения URL
|
||||
|
||||
Каждая фото-модель (`ProductPhoto`, `ProductKitPhoto`, `ProductCategoryPhoto`) имеет методы:
|
||||
|
||||
```python
|
||||
photo = ProductPhoto.objects.first()
|
||||
|
||||
# Получить URL нужного размера
|
||||
photo.get_thumbnail_url() # → /media/products/thumbnails/photo_12345.jpg
|
||||
photo.get_medium_url() # → /media/products/medium/photo_12345.jpg
|
||||
photo.get_large_url() # → /media/products/large/photo_12345.jpg
|
||||
photo.get_original_url() # → /media/products/originals/photo_12345.jpg
|
||||
|
||||
# Прямой доступ к изображению
|
||||
photo.image # → путь к оригиналу в БД
|
||||
photo.image.url # → URL оригинала
|
||||
```
|
||||
|
||||
### Пример полного использования
|
||||
|
||||
```python
|
||||
from products.models import Product
|
||||
from products.utils.image_service import ImageService
|
||||
|
||||
product = Product.objects.get(pk=1)
|
||||
photo = product.photos.first()
|
||||
|
||||
# Способ 1: через методы модели
|
||||
thumbnail = photo.get_thumbnail_url()
|
||||
medium = photo.get_medium_url()
|
||||
large = photo.get_large_url()
|
||||
original = photo.get_original_url()
|
||||
|
||||
# Способ 2: через ImageService (если нужна большая гибкость)
|
||||
from products.utils.image_service import ImageService
|
||||
|
||||
all_urls = ImageService.get_all_urls(photo.image.name)
|
||||
# → {
|
||||
# 'original': '/media/products/originals/...',
|
||||
# 'thumbnail': '/media/products/thumbnails/...',
|
||||
# 'medium': '/media/products/medium/...',
|
||||
# 'large': '/media/products/large/...'
|
||||
# }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Загрузка изображений
|
||||
|
||||
### Через админку Django
|
||||
|
||||
1. Откройте админку: `http://localhost:8000/admin/`
|
||||
2. Перейдите в раздел "Товары", "Комплекты" или "Категории"
|
||||
3. Нажмите на объект или создайте новый
|
||||
4. В разделе "Фото" нажмите "Добавить фото"
|
||||
5. Загрузьте изображение (JPEG или PNG)
|
||||
6. После сохранения система автоматически создаст все размеры
|
||||
|
||||
### Процесс при загрузке
|
||||
|
||||
1. Система получает загруженное изображение
|
||||
2. Проверяет его валидность (должно быть JPEG или PNG)
|
||||
3. Конвертирует в RGB если нужно (для PNG с прозрачностью)
|
||||
4. Создает 4 версии:
|
||||
- **original**: сохраняет в JPEG (quality=90)
|
||||
- **thumbnail**: изменяет размер до 150×150
|
||||
- **medium**: изменяет размер до 400×400
|
||||
- **large**: изменяет размер до 800×800
|
||||
5. Все версии сохраняются в правильные папки
|
||||
6. В БД хранится путь только к оригиналу
|
||||
|
||||
---
|
||||
|
||||
## Management команды
|
||||
|
||||
### Обработка существующих изображений
|
||||
|
||||
Если вы загрузили изображения ДО внедрения этой системы, используйте команду:
|
||||
|
||||
```bash
|
||||
# Обработать все изображения
|
||||
python manage.py process_images
|
||||
|
||||
# Обработать только товары
|
||||
python manage.py process_images --model ProductPhoto
|
||||
|
||||
# Обработать только комплекты
|
||||
python manage.py process_images --model ProductKitPhoto
|
||||
|
||||
# Обработать только категории
|
||||
python manage.py process_images --model ProductCategoryPhoto
|
||||
```
|
||||
|
||||
Команда:
|
||||
- Найдет все существующие изображения
|
||||
- Создаст все недостающие размеры
|
||||
- Покажет прогресс обработки
|
||||
- Выведет количество успешно обработанных и ошибок
|
||||
|
||||
---
|
||||
|
||||
## Автоматическое удаление
|
||||
|
||||
При удалении фото все его версии удаляются автоматически:
|
||||
|
||||
```python
|
||||
photo = ProductPhoto.objects.get(pk=1)
|
||||
photo.delete() # Удалит оригинал + thumbnail + medium + large
|
||||
```
|
||||
|
||||
Это происходит благодаря переопределенному методу `delete()` в моделях.
|
||||
|
||||
---
|
||||
|
||||
## Обновление изображения
|
||||
|
||||
При загрузке нового изображения для существующего фото:
|
||||
|
||||
1. Система обнаруживает, что `image` поле изменилось
|
||||
2. Старые версии удаляются (оригинал + все размеры)
|
||||
3. Создаются новые версии для нового изображения
|
||||
|
||||
```python
|
||||
photo = ProductPhoto.objects.get(pk=1)
|
||||
photo.image = request.FILES['new_image']
|
||||
photo.save() # Старые версии удалены, созданы новые
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Оптимизация и производительность
|
||||
|
||||
### Как работает кэширование путей
|
||||
|
||||
URL изображения рассчитывается динамически на основе пути к оригиналу:
|
||||
|
||||
```
|
||||
Оригинал: products/originals/flower_12345.jpg
|
||||
↓ Динамический расчет пути
|
||||
Миниатюра: products/thumbnails/flower_12345.jpg
|
||||
Средний: products/medium/flower_12345.jpg
|
||||
Большой: products/large/flower_12345.jpg
|
||||
```
|
||||
|
||||
Это не требует хранения 4 путей в БД - экономим место и упрощаем код.
|
||||
|
||||
### Параметры сжатия
|
||||
|
||||
- **Алгоритм**: LANCZOS (лучше всего сохраняет качество)
|
||||
- **Качество JPEG**: 90 (оптимальный баланс между качеством и размером)
|
||||
- **Оптимизация**: включена (`optimize=True`)
|
||||
|
||||
---
|
||||
|
||||
## Типичные размеры файлов
|
||||
|
||||
При загрузке изображения 2000×2000 px (∼2-3 МБ):
|
||||
|
||||
| Версия | Размер | Экономия |
|
||||
|--------|--------|---------|
|
||||
| original | ∼150-200 КБ | - |
|
||||
| thumbnail | ∼5-8 КБ | 95% |
|
||||
| medium | ∼15-25 КБ | 85% |
|
||||
| large | ∼50-80 КБ | 65% |
|
||||
| **Итого** | **∼230-310 КБ** | **~90%** |
|
||||
|
||||
---
|
||||
|
||||
## Возможные доработки в будущем
|
||||
|
||||
1. **Redis кэш URL**: кэшировать сгенерированные URL для еще большей производительности
|
||||
2. **WebP формат**: сохранять в WebP для еще большей оптимизации
|
||||
3. **Responsive images**: служить разные размеры в зависимости от устройства
|
||||
4. **CDN интеграция**: заливать изображения на CDN для быстрой доставки
|
||||
5. **Водяной знак**: добавлять водяной знак при экспорте
|
||||
6. **Batch обработка**: обрабатывать несколько изображений параллельно
|
||||
|
||||
---
|
||||
|
||||
## Решение проблем
|
||||
|
||||
### Изображение не показывается в админке
|
||||
|
||||
Проверьте:
|
||||
1. Изображение загружено (есть путь в БД)
|
||||
2. MEDIA_URL и MEDIA_ROOT настроены правильно в settings.py
|
||||
3. Django runserver запущен (в продакшене нужно настроить serving)
|
||||
|
||||
### Некоторые размеры не созданы
|
||||
|
||||
Запустите management команду:
|
||||
```bash
|
||||
python manage.py process_images
|
||||
```
|
||||
|
||||
### Слишком долгая загрузка изображения
|
||||
|
||||
- Проверьте размер загружаемого файла (обычно <5 МБ)
|
||||
- На сервере может быть медленнее - это нормально
|
||||
- Рассмотрите асинхронную обработку через Celery (будущая доработка)
|
||||
|
||||
---
|
||||
|
||||
## Версия и требования
|
||||
|
||||
- Django 5.2+
|
||||
- Pillow (любая последняя версия)
|
||||
- Python 3.8+
|
||||
|
||||
---
|
||||
|
||||
## История изменений
|
||||
|
||||
### v1.0 (2025-10-22)
|
||||
- Первая версия системы хранения изображений
|
||||
- Поддержка товаров, комплектов и категорий
|
||||
- Автоматическое создание 4 размеров
|
||||
- Management команда для обработки существующих данных
|
||||
- Интеграция с админкой Django
|
||||
@@ -1,547 +0,0 @@
|
||||
# Примеры использования системы хранения изображений
|
||||
|
||||
## Содержание
|
||||
|
||||
1. [Примеры в шаблонах](#примеры-в-шаблонах)
|
||||
2. [Примеры в представлениях](#примеры-в-представлениях)
|
||||
3. [Примеры в моделях](#примеры-в-моделях)
|
||||
4. [Примеры в админке](#примеры-в-админке)
|
||||
5. [Примеры JSON API](#примеры-json-api)
|
||||
6. [Продвинутые примеры](#продвинутые-примеры)
|
||||
|
||||
---
|
||||
|
||||
## Примеры в шаблонах
|
||||
|
||||
### Пример 1: Простой список товаров
|
||||
|
||||
```django
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="products-grid">
|
||||
{% for product in products %}
|
||||
<div class="product-card">
|
||||
{% if product.photos.first %}
|
||||
<img src="{{ product.photos.first.get_thumbnail_url }}"
|
||||
alt="{{ product.name }}"
|
||||
class="product-image"
|
||||
loading="lazy">
|
||||
{% else %}
|
||||
<div class="no-image">Нет фото</div>
|
||||
{% endif %}
|
||||
|
||||
<h3>{{ product.name }}</h3>
|
||||
<p class="price">{{ product.sale_price }} ₽</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Пример 2: Карточка товара с галереей
|
||||
|
||||
```django
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="product-detail">
|
||||
<div class="gallery">
|
||||
<!-- Основное изображение -->
|
||||
<div class="main-image">
|
||||
{% if product.photos.first %}
|
||||
<img id="main-img"
|
||||
src="{{ product.photos.first.get_large_url }}"
|
||||
alt="{{ product.name }}"
|
||||
class="responsive-image">
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Галерея миниатюр -->
|
||||
<div class="thumbnails">
|
||||
{% for photo in product.photos.all %}
|
||||
<img src="{{ photo.get_thumbnail_url }}"
|
||||
alt="{{ product.name }}"
|
||||
class="thumbnail"
|
||||
onclick="changeMainImage('{{ photo.get_large_url }}')">
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="product-info">
|
||||
<h1>{{ product.name }}</h1>
|
||||
<p class="description">{{ product.description }}</p>
|
||||
<p class="price">{{ product.sale_price }} ₽</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function changeMainImage(url) {
|
||||
document.getElementById('main-img').src = url;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Пример 3: Каталог комплектов с категориями
|
||||
|
||||
```django
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="categories">
|
||||
{% for category in categories %}
|
||||
<section class="category-section">
|
||||
<h2>{{ category.name }}</h2>
|
||||
|
||||
<div class="kits-grid">
|
||||
{% for kit in category.kits.all %}
|
||||
<div class="kit-card">
|
||||
{% if kit.photos.first %}
|
||||
<div class="kit-image">
|
||||
<img src="{{ kit.photos.first.get_medium_url }}"
|
||||
alt="{{ kit.name }}"
|
||||
class="kit-photo">
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<h3>{{ kit.name }}</h3>
|
||||
<p class="price">от {{ kit.get_sale_price }} ₽</p>
|
||||
|
||||
<a href="{% url 'kit-detail' kit.id %}" class="btn-view">
|
||||
Подробнее
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### Пример 4: Слайдер (carousel)
|
||||
|
||||
```django
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="slider">
|
||||
<div class="slides">
|
||||
{% for photo in featured_photos %}
|
||||
<div class="slide" style="background-image: url('{{ photo.get_large_url }}')">
|
||||
<div class="slide-content">
|
||||
<h2>{{ photo.product.name }}</h2>
|
||||
<p>{{ photo.product.description|truncatewords:20 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.slide {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примеры в представлениях
|
||||
|
||||
### Пример 1: Список товаров с пагинацией
|
||||
|
||||
```python
|
||||
from django.shortcuts import render
|
||||
from django.views.generic import ListView
|
||||
from products.models import Product
|
||||
|
||||
class ProductListView(ListView):
|
||||
model = Product
|
||||
template_name = 'products/list.html'
|
||||
context_object_name = 'products'
|
||||
paginate_by = 12
|
||||
|
||||
def get_queryset(self):
|
||||
return Product.active.prefetch_related('photos')
|
||||
|
||||
def products_list(request):
|
||||
products = Product.active.prefetch_related('photos').all()
|
||||
|
||||
context = {
|
||||
'products': products,
|
||||
}
|
||||
|
||||
return render(request, 'products/list.html', context)
|
||||
```
|
||||
|
||||
### Пример 2: Детальный вид товара
|
||||
|
||||
```python
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from products.models import Product
|
||||
|
||||
def product_detail(request, pk):
|
||||
product = get_object_or_404(Product, pk=pk)
|
||||
photos = product.photos.all().order_by('order')
|
||||
|
||||
# Получить все URL для шаблона
|
||||
main_photo = photos.first()
|
||||
all_photo_urls = []
|
||||
|
||||
if main_photo:
|
||||
all_photo_urls = [{
|
||||
'thumbnail': photo.get_thumbnail_url(),
|
||||
'medium': photo.get_medium_url(),
|
||||
'large': photo.get_large_url(),
|
||||
'original': photo.get_original_url(),
|
||||
} for photo in photos]
|
||||
|
||||
context = {
|
||||
'product': product,
|
||||
'photos': photos,
|
||||
'photo_urls': all_photo_urls,
|
||||
'main_photo': main_photo,
|
||||
}
|
||||
|
||||
return render(request, 'products/detail.html', context)
|
||||
```
|
||||
|
||||
### Пример 3: API endpoint с изображениями
|
||||
|
||||
```python
|
||||
from django.http import JsonResponse
|
||||
from products.models import Product
|
||||
|
||||
def product_api(request, pk):
|
||||
product = Product.objects.get(pk=pk)
|
||||
photo = product.photos.first()
|
||||
|
||||
data = {
|
||||
'id': product.id,
|
||||
'name': product.name,
|
||||
'price': float(product.sale_price),
|
||||
'images': {
|
||||
'thumbnail': photo.get_thumbnail_url() if photo else None,
|
||||
'medium': photo.get_medium_url() if photo else None,
|
||||
'large': photo.get_large_url() if photo else None,
|
||||
'original': photo.get_original_url() if photo else None,
|
||||
}
|
||||
}
|
||||
|
||||
return JsonResponse(data)
|
||||
```
|
||||
|
||||
### Пример 4: Экспорт в CSV с ссылками на изображения
|
||||
|
||||
```python
|
||||
import csv
|
||||
from django.http import HttpResponse
|
||||
from products.models import Product
|
||||
|
||||
def export_products_csv(request):
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
response['Content-Disposition'] = 'attachment; filename="products.csv"'
|
||||
|
||||
writer = csv.writer(response)
|
||||
writer.writerow(['ID', 'Название', 'Цена', 'Фото (thumbnail)', 'Фото (original)'])
|
||||
|
||||
for product in Product.active.prefetch_related('photos'):
|
||||
photo = product.photos.first()
|
||||
writer.writerow([
|
||||
product.id,
|
||||
product.name,
|
||||
product.sale_price,
|
||||
photo.get_thumbnail_url() if photo else '',
|
||||
photo.get_original_url() if photo else '',
|
||||
])
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примеры в моделях
|
||||
|
||||
### Пример 1: Добавить метод для получения лучшего фото
|
||||
|
||||
```python
|
||||
from products.models import Product
|
||||
|
||||
class ProductWithBestPhoto(Product):
|
||||
"""Добавить метод для получения лучшего фото по рейтингу"""
|
||||
|
||||
def get_best_photo(self):
|
||||
"""Получить фото с наименьшим order (основное)"""
|
||||
return self.photos.first()
|
||||
|
||||
def get_photo_urls(self):
|
||||
"""Получить словарь всех URL основного фото"""
|
||||
photo = self.get_best_photo()
|
||||
if not photo:
|
||||
return {
|
||||
'thumbnail': '',
|
||||
'medium': '',
|
||||
'large': '',
|
||||
'original': '',
|
||||
}
|
||||
|
||||
return {
|
||||
'thumbnail': photo.get_thumbnail_url(),
|
||||
'medium': photo.get_medium_url(),
|
||||
'large': photo.get_large_url(),
|
||||
'original': photo.get_original_url(),
|
||||
}
|
||||
```
|
||||
|
||||
### Пример 2: Сигнал для логирования при загрузке фото
|
||||
|
||||
```python
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from products.models import ProductPhoto
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@receiver(post_save, sender=ProductPhoto)
|
||||
def log_photo_upload(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
logger.info(f"Новое фото для товара '{instance.product.name}': {instance.image.name}")
|
||||
else:
|
||||
logger.info(f"Фото для товара '{instance.product.name}' обновлено: {instance.image.name}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примеры в админке
|
||||
|
||||
### Пример: Кастомная админка с дополнительными фильтрами
|
||||
|
||||
```python
|
||||
from django.contrib import admin
|
||||
from products.models import Product, ProductPhoto
|
||||
|
||||
class ProductPhotoInline(admin.TabularInline):
|
||||
model = ProductPhoto
|
||||
extra = 0
|
||||
fields = ('image', 'order')
|
||||
readonly_fields = ('image_preview',)
|
||||
|
||||
class ProductAdminCustom(admin.ModelAdmin):
|
||||
list_display = ('name', 'photo_count', 'has_photos', 'created_at')
|
||||
inlines = [ProductPhotoInline]
|
||||
|
||||
def photo_count(self, obj):
|
||||
"""Количество фото"""
|
||||
return obj.photos.count()
|
||||
photo_count.short_description = 'Фото'
|
||||
|
||||
def has_photos(self, obj):
|
||||
"""Есть ли фото"""
|
||||
return obj.photos.exists()
|
||||
has_photos.boolean = True
|
||||
has_photos.short_description = 'Есть фото'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примеры JSON API
|
||||
|
||||
### Пример: REST API с сериализаторами (DRF)
|
||||
|
||||
```python
|
||||
from rest_framework import serializers
|
||||
from products.models import Product, ProductPhoto
|
||||
|
||||
class ProductPhotoSerializer(serializers.ModelSerializer):
|
||||
thumbnail_url = serializers.SerializerMethodField()
|
||||
medium_url = serializers.SerializerMethodField()
|
||||
large_url = serializers.SerializerMethodField()
|
||||
original_url = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = ProductPhoto
|
||||
fields = ['id', 'thumbnail_url', 'medium_url', 'large_url', 'original_url']
|
||||
|
||||
def get_thumbnail_url(self, obj):
|
||||
return obj.get_thumbnail_url()
|
||||
|
||||
def get_medium_url(self, obj):
|
||||
return obj.get_medium_url()
|
||||
|
||||
def get_large_url(self, obj):
|
||||
return obj.get_large_url()
|
||||
|
||||
def get_original_url(self, obj):
|
||||
return obj.get_original_url()
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
photos = ProductPhotoSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = ['id', 'name', 'price', 'photos']
|
||||
```
|
||||
|
||||
### Пример JSON ответ:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Роза красная Freedom",
|
||||
"price": "150.00",
|
||||
"photos": [
|
||||
{
|
||||
"id": 1,
|
||||
"thumbnail_url": "/media/products/thumbnails/rose_12345.jpg",
|
||||
"medium_url": "/media/products/medium/rose_12345.jpg",
|
||||
"large_url": "/media/products/large/rose_12345.jpg",
|
||||
"original_url": "/media/products/originals/rose_12345.jpg"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Продвинутые примеры
|
||||
|
||||
### Пример 1: Оптимизированный запрос для перечисления
|
||||
|
||||
```python
|
||||
from django.db.models import Prefetch
|
||||
from products.models import Product, ProductPhoto
|
||||
|
||||
def get_optimized_products(queryset=None):
|
||||
"""
|
||||
Получить товары с оптимизированными запросами к фото
|
||||
"""
|
||||
if queryset is None:
|
||||
queryset = Product.active
|
||||
|
||||
# Prefetch только первое фото для каждого товара
|
||||
photo_prefetch = Prefetch(
|
||||
'photos',
|
||||
ProductPhoto.objects.order_by('order')[:1]
|
||||
)
|
||||
|
||||
return queryset.prefetch_related(photo_prefetch)
|
||||
|
||||
# Использование
|
||||
products = get_optimized_products()
|
||||
for product in products:
|
||||
photo = product.photos.first()
|
||||
print(f"{product.name}: {photo.get_medium_url()}")
|
||||
```
|
||||
|
||||
### Пример 2: Кэширование URL в Redis
|
||||
|
||||
```python
|
||||
from django.core.cache import cache
|
||||
from products.models import ProductPhoto
|
||||
|
||||
def get_photo_urls_cached(photo_id, timeout=3600):
|
||||
"""
|
||||
Получить все URL фото из кэша или создать новые
|
||||
"""
|
||||
cache_key = f'photo_urls_{photo_id}'
|
||||
|
||||
urls = cache.get(cache_key)
|
||||
if urls is None:
|
||||
photo = ProductPhoto.objects.get(id=photo_id)
|
||||
urls = {
|
||||
'thumbnail': photo.get_thumbnail_url(),
|
||||
'medium': photo.get_medium_url(),
|
||||
'large': photo.get_large_url(),
|
||||
'original': photo.get_original_url(),
|
||||
}
|
||||
cache.set(cache_key, urls, timeout)
|
||||
|
||||
return urls
|
||||
|
||||
# Использование
|
||||
urls = get_photo_urls_cached(photo_id=1)
|
||||
```
|
||||
|
||||
### Пример 3: Генерация миниатюр для социальных сетей
|
||||
|
||||
```python
|
||||
from products.models import Product
|
||||
from products.utils.image_service import ImageService
|
||||
|
||||
def get_social_media_image(product):
|
||||
"""
|
||||
Получить оптимальное изображение для социальных сетей
|
||||
"""
|
||||
photo = product.photos.first()
|
||||
if not photo:
|
||||
return None
|
||||
|
||||
social_images = {
|
||||
'og_image': photo.get_large_url(), # Facebook, VK
|
||||
'twitter_image': photo.get_medium_url(),
|
||||
'pinterest': photo.get_original_url(),
|
||||
'instagram_thumbnail': photo.get_thumbnail_url(),
|
||||
}
|
||||
|
||||
return social_images
|
||||
|
||||
# Использование в шаблоне
|
||||
{% with social_images=get_social_media_image %}
|
||||
<meta property="og:image" content="{{ social_images.og_image }}">
|
||||
<meta name="twitter:image" content="{{ social_images.twitter_image }}">
|
||||
{% endwith %}
|
||||
```
|
||||
|
||||
### Пример 4: Batch обработка и переформатирование
|
||||
|
||||
```python
|
||||
from django.core.management.base import BaseCommand
|
||||
from products.models import Product
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Переобработать все изображения'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for product in Product.objects.prefetch_related('photos'):
|
||||
for photo in product.photos.all():
|
||||
# Пересохранить - это вызовет переобработку
|
||||
photo.save()
|
||||
self.stdout.write(f"✓ {product.name}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Чек-лист для разработчика
|
||||
|
||||
Когда добавляете изображения в новую модель:
|
||||
|
||||
- [ ] Наследуется ли модель от `Photo*`?
|
||||
- [ ] Есть ли методы `get_*_url()`?
|
||||
- [ ] Переопределены ли `save()` и `delete()`?
|
||||
- [ ] Добавлена ли в админку?
|
||||
- [ ] Добавлен ли `prefetch_related` в queryset?
|
||||
- [ ] Проверены ли все размеры в шаблонах?
|
||||
|
||||
---
|
||||
|
||||
## Производительность
|
||||
|
||||
Типичные цифры на проекте с 1000 товарами:
|
||||
|
||||
| Операция | Время |
|
||||
|----------|-------|
|
||||
| Загрузка товара + первое фото | 5мс |
|
||||
| Получение URL миниатюры | 0.1мс |
|
||||
| Вся галерея (10 фото) | 50мс |
|
||||
| Экспорт в CSV (1000 товаров) | 2сек |
|
||||
|
||||
---
|
||||
|
||||
Больше примеров и кейсов см. в основной документации: `IMAGE_STORAGE_STRATEGY.md`
|
||||
@@ -1,138 +0,0 @@
|
||||
╔══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ СИСТЕМА ХРАНЕНИЯ ИЗОБРАЖЕНИЙ ║
|
||||
║ ГОТОВО К ИСПОЛЬЗОВАНИЮ ║
|
||||
╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
|
||||
📋 ЧТО БЫЛО РЕАЛИЗОВАНО
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
✅ Автоматическая обработка изображений
|
||||
└─ При загрузке изображения система создает 4 версии:
|
||||
• thumbnail (150×150) - для каталогов, списков
|
||||
• medium (400×400) - для карточек товаров
|
||||
• large (800×800) - для полного просмотра
|
||||
• original (без изм.) - архив в качестве 90
|
||||
|
||||
✅ Чистое хранилище файлов
|
||||
└─ media/products/originals/ (оригинальные)
|
||||
└─ media/products/thumbnails/ (миниатюры)
|
||||
└─ media/products/medium/ (средние)
|
||||
└─ media/products/large/ (большие)
|
||||
|
||||
✅ Простой API в шаблонах
|
||||
└─ {{ photo.get_thumbnail_url }} # миниатюра
|
||||
{{ photo.get_medium_url }} # средний размер
|
||||
{{ photo.get_large_url }} # большой размер
|
||||
{{ photo.get_original_url }} # оригинал
|
||||
|
||||
✅ Автоматическое управление жизненным циклом
|
||||
└─ Удаление старых версий при замене фото
|
||||
└─ Удаление всех версий при удалении фото
|
||||
└─ Обновление при сохранении
|
||||
|
||||
✅ Админка Django с превью всех размеров
|
||||
└─ Наглядное отображение 4 версий каждого фото
|
||||
└─ Быстрая загрузка и управление
|
||||
|
||||
✅ Management команда для batch-обработки
|
||||
└─ python manage.py process_images
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
📁 СОЗДАННЫЕ ФАЙЛЫ
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
1. products/utils/image_processor.py
|
||||
└─ Класс ImageProcessor для обработки и создания размеров
|
||||
└─ Методы для сохранения и удаления версий
|
||||
|
||||
2. products/utils/image_service.py
|
||||
└─ Класс ImageService для получения URL нужного размера
|
||||
└─ Методы: get_url(), get_thumbnail_url(), get_medium_url() и т.д.
|
||||
|
||||
3. products/models.py (ОБНОВЛЕНО)
|
||||
└─ ProductPhoto, ProductKitPhoto, ProductCategoryPhoto
|
||||
└─ Добавлены методы save() и delete() для автоматической обработки
|
||||
└─ Добавлены методы get_*_url() для получения URL
|
||||
|
||||
4. products/admin.py (ОБНОВЛЕНО)
|
||||
└─ Админка с красивым превью всех размеров
|
||||
└─ Grid для наглядного сравнения размеров
|
||||
|
||||
5. products/management/commands/process_images.py
|
||||
└─ Management команда для обработки существующих изображений
|
||||
└─ Поддержка фильтра по моделям
|
||||
|
||||
6. Документация:
|
||||
└─ IMAGE_STORAGE_STRATEGY.md - полная документация системы
|
||||
└─ QUICK_START_IMAGES.md - быстрый старт
|
||||
└─ IMAGE_SYSTEM_EXAMPLES.md - примеры кода
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
🚀 БЫСТРЫЙ СТАРТ
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
1. В шаблонах (templates):
|
||||
|
||||
<!-- Список товаров - миниатюры -->
|
||||
<img src="{{ product.photos.first.get_thumbnail_url }}"
|
||||
alt="{{ product.name }}">
|
||||
|
||||
<!-- Карточка - средний размер -->
|
||||
<img src="{{ product.photos.first.get_medium_url }}"
|
||||
alt="{{ product.name }}">
|
||||
|
||||
<!-- Полный просмотр - большой размер -->
|
||||
<img src="{{ product.photos.first.get_large_url }}"
|
||||
alt="{{ product.name }}">
|
||||
|
||||
2. В представлениях (views):
|
||||
|
||||
from products.models import Product
|
||||
|
||||
product = Product.objects.get(pk=1)
|
||||
photo = product.photos.first()
|
||||
|
||||
context = {
|
||||
'thumbnail': photo.get_thumbnail_url(),
|
||||
'medium': photo.get_medium_url(),
|
||||
'large': photo.get_large_url(),
|
||||
'original': photo.get_original_url(),
|
||||
}
|
||||
|
||||
3. В админке:
|
||||
└─ Просто загрузите изображение - система сделает все автоматически!
|
||||
└─ Сможете видеть все 4 версии при редактировании
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
📊 ПРОИЗВОДИТЕЛЬНОСТЬ И ЭКОНОМИЯ
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
При загрузке изображения 2000×2000 px (~2-3 МБ):
|
||||
|
||||
Версия │ Размер │ Экономия
|
||||
─────────────┼──────────┼─────────────
|
||||
original │ 6.1K │ -
|
||||
medium │ 2.9K │ 52% ↓
|
||||
large │ 5.6K │ 8% ↓
|
||||
thumbnail │ 0.4K │ 93% ↓
|
||||
─────────────┼──────────┼─────────────
|
||||
ИТОГО │ 14.9K │ 90% ↓
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
✅ ТЕСТИРОВАНИЕ
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
✓ Загрузка тестового изображения (1000×1000 px)
|
||||
✓ Создание всех 4 версий
|
||||
✓ Правильное сохранение в папки
|
||||
✓ Получение URL нужного размера
|
||||
✓ Экономия размера файлов (93% для миниатюр)
|
||||
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
🎯 ГОТОВО К ИСПОЛЬЗОВАНИЮ
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Система полностью реализована, протестирована и готова!
|
||||
|
||||
Версия: 1.0
|
||||
Дата: 2025-10-22
|
||||
@@ -1,249 +0,0 @@
|
||||
# Быстрый старт: Система хранения изображений
|
||||
|
||||
## Что было реализовано ✅
|
||||
|
||||
Полнофункциональная система автоматической обработки и хранения изображений для:
|
||||
- Товаров (ProductPhoto)
|
||||
- Комплектов/букетов (ProductKitPhoto)
|
||||
- Категорий (ProductCategoryPhoto)
|
||||
|
||||
## Основные возможности
|
||||
|
||||
1. **Автоматическое создание 4 размеров:**
|
||||
- `thumbnail` (150×150) - для каталогов
|
||||
- `medium` (400×400) - для карточек
|
||||
- `large` (800×800) - для полного просмотра
|
||||
- `original` - архив в качестве 90
|
||||
|
||||
2. **Чистое хранилище:** все версии в разных папках
|
||||
```
|
||||
media/products/originals/...
|
||||
media/products/thumbnails/...
|
||||
media/products/medium/...
|
||||
media/products/large/...
|
||||
```
|
||||
|
||||
3. **Простой API в шаблонах:**
|
||||
```django
|
||||
{{ photo.get_thumbnail_url }} <!-- 150×150 -->
|
||||
{{ photo.get_medium_url }} <!-- 400×400 -->
|
||||
{{ photo.get_large_url }} <!-- 800×800 -->
|
||||
{{ photo.get_original_url }} <!-- полный размер -->
|
||||
```
|
||||
|
||||
4. **Автоматическое управление:**
|
||||
- Удаление старых версий при замене фото
|
||||
- Удаление всех версий при удалении фото
|
||||
|
||||
## Как использовать
|
||||
|
||||
### В шаблонах (templates)
|
||||
|
||||
```django
|
||||
<!-- Список товаров - миниатюры -->
|
||||
<img src="{{ product.photos.first.get_thumbnail_url }}" alt="{{ product.name }}">
|
||||
|
||||
<!-- Карточка товара - средний размер -->
|
||||
<img src="{{ product.photos.first.get_medium_url }}" alt="{{ product.name }}">
|
||||
|
||||
<!-- Полный просмотр - большой размер -->
|
||||
<img src="{{ product.photos.first.get_large_url }}" alt="{{ product.name }}">
|
||||
|
||||
<!-- Скачать оригинал -->
|
||||
<a href="{{ product.photos.first.get_original_url }}" download>
|
||||
Скачать в полном размере
|
||||
</a>
|
||||
```
|
||||
|
||||
### В представлениях (views)
|
||||
|
||||
```python
|
||||
from products.models import Product
|
||||
|
||||
def product_detail(request, pk):
|
||||
product = Product.objects.get(pk=pk)
|
||||
photo = product.photos.first()
|
||||
|
||||
context = {
|
||||
'thumbnail': photo.get_thumbnail_url(),
|
||||
'medium': photo.get_medium_url(),
|
||||
'large': photo.get_large_url(),
|
||||
'original': photo.get_original_url(),
|
||||
}
|
||||
|
||||
return render(request, 'product_detail.html', context)
|
||||
```
|
||||
|
||||
## Загрузка изображений
|
||||
|
||||
1. Зайти в админку: `http://localhost:8000/admin/`
|
||||
2. Логин: `admin`, Пароль: `admin123`
|
||||
3. Открыть Товары, Комплекты или Категории
|
||||
4. Добавить/редактировать объект
|
||||
5. Загрузить изображение в секцию "Фото"
|
||||
6. Сохранить - **система автоматически создаст все размеры!**
|
||||
|
||||
## Management команды
|
||||
|
||||
### Обработать старые изображения
|
||||
|
||||
Если было изображения ДО внедрения этой системы:
|
||||
|
||||
```bash
|
||||
# Все изображения
|
||||
python manage.py process_images
|
||||
|
||||
# Только товары
|
||||
python manage.py process_images --model ProductPhoto
|
||||
|
||||
# Только комплекты
|
||||
python manage.py process_images --model ProductKitPhoto
|
||||
|
||||
# Только категории
|
||||
python manage.py process_images --model ProductCategoryPhoto
|
||||
```
|
||||
|
||||
## Файлы системы
|
||||
|
||||
| Файл | Назначение |
|
||||
|------|-----------|
|
||||
| `products/utils/image_processor.py` | Обработка и создание размеров |
|
||||
| `products/utils/image_service.py` | Получение URL нужного размера |
|
||||
| `products/models.py` | Обновленные Photo модели с методами |
|
||||
| `products/admin.py` | Админка с превью всех размеров |
|
||||
| `products/management/commands/process_images.py` | Command для batch-обработки |
|
||||
|
||||
## Размеры файлов (пример)
|
||||
|
||||
При загрузке фото 2000×2000 px:
|
||||
|
||||
| Версия | Размер | Экономия |
|
||||
|--------|--------|---------|
|
||||
| original | 6.1K | - |
|
||||
| medium | 2.9K | 52% ↓ |
|
||||
| large | 5.6K | 8% ↓ |
|
||||
| thumbnail | 438B | **93% ↓** |
|
||||
|
||||
## API методов моделей
|
||||
|
||||
```python
|
||||
photo = ProductPhoto.objects.first()
|
||||
|
||||
# URL разных размеров
|
||||
photo.get_thumbnail_url() # → /media/products/thumbnails/image_123.jpg
|
||||
photo.get_medium_url() # → /media/products/medium/image_123.jpg
|
||||
photo.get_large_url() # → /media/products/large/image_123.jpg
|
||||
photo.get_original_url() # → /media/products/originals/image_123.jpg
|
||||
|
||||
# Удаление (удалит все версии автоматически)
|
||||
photo.delete()
|
||||
|
||||
# Замена изображения (удалит старые, создаст новые)
|
||||
photo.image = new_file
|
||||
photo.save()
|
||||
```
|
||||
|
||||
## Для разработчиков
|
||||
|
||||
### Структура обработки изображения
|
||||
|
||||
```
|
||||
1. Загрузка файла → ImageProcessor.process_image()
|
||||
2. Проверка валидности (должен быть JPEG/PNG)
|
||||
3. Конвертирование в RGB (для PNG с прозрачностью)
|
||||
4. Создание 4 версий:
|
||||
- original: JPEG quality=90
|
||||
- thumbnail: resize to 150×150
|
||||
- medium: resize to 400×400
|
||||
- large: resize to 800×800
|
||||
5. Сохранение в media/[type]/[size]/filename
|
||||
6. Хранение только пути к оригиналу в БД
|
||||
```
|
||||
|
||||
### Добавление нового размера
|
||||
|
||||
Если нужен новый размер, отредактируйте `products/utils/image_processor.py`:
|
||||
|
||||
```python
|
||||
class ImageProcessor:
|
||||
SIZES = {
|
||||
'thumbnail': (150, 150),
|
||||
'medium': (400, 400),
|
||||
'large': (800, 800),
|
||||
'xl': (1200, 1200), # ← новый размер
|
||||
}
|
||||
```
|
||||
|
||||
И добавьте метод в модели:
|
||||
|
||||
```python
|
||||
def get_xl_url(self):
|
||||
"""Получить URL XL размера (1200x1200)"""
|
||||
from .utils.image_service import ImageService
|
||||
return ImageService.get_url(self.image.name, 'xl')
|
||||
```
|
||||
|
||||
## Производительность
|
||||
|
||||
- **Нет дополнительной БД записи** - все пути рассчитываются на лету
|
||||
- **Минимальный оверхед** - вычисление пути занимает <1мс
|
||||
- **Оптимальное сжатие** - LANCZOS для качества, quality=90 для баланса
|
||||
|
||||
Если нужна оптимизация:
|
||||
- Можно добавить кэш в Redis
|
||||
- Можно генерировать в фоне через Celery
|
||||
- Можно загружать на CDN
|
||||
|
||||
## Полная документация
|
||||
|
||||
См. файл `IMAGE_STORAGE_STRATEGY.md` для полной документации.
|
||||
|
||||
## Контрольный список
|
||||
|
||||
- [x] ImageProcessor создан
|
||||
- [x] ImageService создан
|
||||
- [x] Модели обновлены
|
||||
- [x] Методы получения URL добавлены
|
||||
- [x] Админка обновлена с превью всех размеров
|
||||
- [x] Management команда создана
|
||||
- [x] Миграции применены
|
||||
- [x] Тестирование пройдено ✓
|
||||
|
||||
## Быстрая проверка
|
||||
|
||||
```bash
|
||||
# Запустить сервер
|
||||
python manage.py runserver
|
||||
|
||||
# В другом терминале - протестировать
|
||||
python manage.py shell
|
||||
```
|
||||
|
||||
```python
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from products.models import Product, ProductPhoto
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
# Создать тестовое изображение
|
||||
img = Image.new('RGB', (1000, 1000), color='blue')
|
||||
img_io = BytesIO()
|
||||
img.save(img_io, format='JPEG')
|
||||
img_io.seek(0)
|
||||
|
||||
# Получить первый товар
|
||||
product = Product.objects.first()
|
||||
|
||||
# Создать фото
|
||||
photo = ProductPhoto(product=product)
|
||||
photo.image = SimpleUploadedFile('test.jpg', img_io.getvalue())
|
||||
photo.save()
|
||||
|
||||
# Проверить что все работает
|
||||
print(photo.get_thumbnail_url())
|
||||
print(photo.get_medium_url())
|
||||
print(photo.get_large_url())
|
||||
print(photo.get_original_url())
|
||||
```
|
||||
|
||||
Все должно вывести правильные пути к файлам! 🎉
|
||||
Reference in New Issue
Block a user