Коротко: правильный и «более нормальный» способ — хранить изображения в отдельной модели (GalleryImage) и связывать её с родительской через ForeignKey / ManyToMany. Это даёт максимум гибкости (порядок, подписи, миниатюры, удаление, фильтрация) и совместимо с инструментами Django (валидация, storage, миграции). Если принципиально хочется единого поля в модели — можно сделать поле, которое хранит список путей (JSONField или кастомный TextField), и написать для него form-field + widget, но это более «нестандартно» и потребует ручной работы с загрузкой/удалением файлов.
Ниже — два варианта с примерами кода и рекомендациями.
1) Рекомендованный (нормализованный) вариант — отдельная модель изображений
- Плюсы: простота, гибкость, порядок, подписи, интеграция с ORM, простая миграция/бэкап.
- Минусы: в админке по умолчанию используются инлайны; если не хотите инлайнов — можно сделать собственный виджет/JS для массовой загрузки и endpoint для AJAX-загрузки.
Пример моделей:
```
# models.py
from django.db import models
class Gallery(models.Model):
title = models.CharField(max_length=200)
class GalleryImage(models.Model):
gallery = models.ForeignKey(Gallery, related_name='images', on_delete=models.CASCADE)
image = models.ImageField(upload_to='galleries/%Y/%m/%d')
caption = models.CharField(max_length=200, blank=True)
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['order']
```
Простой админ (с инлайнами):
```
# admin.py
from django.contrib import admin
from .models import Gallery, GalleryImage
class GalleryImageInline(admin.TabularInline):
model = GalleryImage
extra = 1
@admin.register(Gallery)
class GalleryAdmin(admin.ModelAdmin):
inlines = [GalleryImageInline]
```
Если не хотите инлайнов — делаете:
- отдельный view/REST endpoint для загрузки файлов (при загрузке создаёт GalleryImage с fk на родителя и возвращает JSON с id и url),
- в форме/шаблоне админки подключаете JS (Dropzone/Plupload/vanilla) для множественной загрузки и управления порядком,
- на сохранении формы — JS сохраняет/удаляет связанные изображения через AJAX или передаёт список существующих id.
Это — промышленный подход (много сайтов так делают).
2) Альтернативный вариант — поле, хранящее список путей (JSONField) + form-field+widget
- Плюсы: в модели — одно поле, легче отрисовать форму, можно быстро сделать «галерею» без новой таблицы.
- Минусы: приходится вручную сохранять файлы в storage, управлять удалением старых файлов, сложнее делать выборки/фильтрацию по изображениям, теряется нормализация.
Пример минимальной реализации:
```
# models.py (Django 3.1+)
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=200)
images = models.JSONField(default=list, blank=True) # список путей/URL
```
Form field + widget (упрощённо):
```
# forms.py
from django import forms
from django.core.files.storage import default_storage
class MultiImageWidget(forms.ClearableFileInput):
allow_multiple_selected = True
class MultiImageFormField(forms.Field):
widget = MultiImageWidget
def to_python(self, data):
# data — может быть список загруженных файлов и/или строк с уже загруженными путями
if not data:
return []
if not isinstance(data, list):
data = [data]
return data
def clean(self, data):
files = []
existing = []
for item in data:
if hasattr(item, 'read'): # UploadedFile
files.append(item)
elif isinstance(item, str):
existing.append(item)
saved = []
for f in files:
name = default_storage.save('galleries/' + f.name, f)
saved.append(default_storage.url(name)) # или name, в зависимости от ваших нужд
return existing + saved
```
Использование в ModelForm и админке:
```
class ProductForm(forms.ModelForm):
images = MultiImageFormField(required=False)
class Meta:
model = Product
fields = '__all__'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance and self.instance.pk:
self.fields['images'].initial = self.instance.images
def save(self, commit=True):
self.instance.images = self.cleaned_data['images']
return super().save(commit=commit)
```
Widget-шаблон должен показывать превью существующих изображений и иметь <input type="file" multiple>. В админке можно заменить change_form_template или подключить Media (JS/CSS) к ModelAdmin.
Советы по реализации:
- Обязательно подумайте о том, как удалять неиспользуемые файлы (старые файлы в storage) — реализуйте удаление при перезаписи/удалении записи.
- Храните в JSON либо относительные пути (storage.save возвращает имя), либо абсолютные URL — решите один стиль и используйте consistently.
- Валидация: ограничение типа файла, размера, количества изображений.
- Для порядка добавьте поле order в JSON (список объектов), либо храните порядок в массиве путей.
- Для удобства покройте фронтенд превью/drag&drop и AJAX-загрузку (Dropzone.js, FilePond и т. п.). Тогда вы можете полностью скрыть инлайны и работать через API.
Пакеты и инструменты
- django-filer — популярный файл-менеджер, всё ещё используется.
- django-versatileimagefield, django-imagekit, sorl-thumbnail — для обработки/кеширования/thumbnail'ов.
- django-photologue — старый, но иногда полезен.
- Для админ-множественной загрузки полюбому придётся писать немного JS или интегрировать Dropzone/FilePond. Готовых «под ключ» современного пакета для одного поля-галереи я не встречал, поэтому обычно делают либо инлайны, либо кастомный виджет + endpoint.
Резюме / рекомендация
- Если проект серьёзный: используйте отдельную модель GalleryImage + интерфейс загрузки (инлайн или AJAX). Это лучший баланс гибкости и надёжности.
- Если нужен быстрый и компактный вариант: JSONField + кастомный form-field/widget, но будьте готовы к ручному управлению storage/удалением/версированием.
Если хотите, могу:
- прислать готовый пример widget + template + JS (Dropzone/FilePond) и view для AJAX-загрузки, чтобы в админке получить «одно поле» для галереи, или
- помочь написать конкретную модель/форму по вашей задаче (пример структуры данных, требования: порядок, подписи, макс. количество, хранение URL/имени файла, миниатюры и т. п.).