Загрузка файлов с помощью Django
Вступление
Интернет существенно помогает в передаче огромных объёмов данных между компьютерами, подключенными к сети. Эти данные могут принимать форму изображения, видео или же аудиофайла.
Все уже настолько привыкли делиться файлами, что этот функционал – не новость для опытных пользователей.
В этом гайде мы научимся загружать файлы в веб-приложение на языке Django, используя Python.
Загруженные файлы могут быть дополнительно обработаны. Но, загружая файлы, мы также задаём вопрос о том, где они будут хранятся, и как их потом отобразить. На протяжении этого гайда мы будем отвечать на эти вопросы.
Начало проекта
Мы создадим небольшой проект, куда мы сможем загрузить файлы и хранить их там, используя функционал Django.
Предположим, мы живём во вселенной Гарри Поттера. Магам-зоологам вдруг понадобилось приложение с информацией о каждом существе, которое они изучают. Мы создадим форму, через которую они смогут добавлять фото и информацию о каждом таком зверьке. Затем выведем эту форму, покажем, где храниться информация и где её искать, когда нам это понадобиться.
Начнём с создания виртуальной среды, чтобы не получилось так, чтобы наши зависимости вызывали проблемы с несоответствием версий другим проектам. Это делать необязательно, но всё же рекомендуется для поддержания чистоты среды Python. Давайте создадим каталог, который будет выступать в качестве контейнера для среды.
Откройте командную строку/оболочку и в только что созданном каталоге пропишите следующее:
$ mkdir fileupload
$ cd fileupload
$ python -m venv ./myenv
# OR
$ python3 -m venv ./myenv
Теперь, когда мы создали виртуальную среду, нам нужно активировать её, запустив скрипт активации (activate):
# Windows
$ myenv/Scripts/activate.bat
# Linux
$ source myenv/Scripts/activate
# MacOS
$ source env/bin/activate
После активации среды, если мы установим зависимости, они будут применимы только к этой среде и не будут конфликтовать с другими средами или даже с системной средой. Так мы можем установить Django через pip:
$ pip install "Django==3.0.*"
Теперь давайте создадим проект под названием fantasticbeasts с помощью команды startproject модуля django-admin. Как только мы создадим скелет проекта, мы можем перейти в этот каталог и запустить приложение через startapp:
$ django-admin startproject fantasticbeasts
$ cd fantasticbeasts
$ django-admin startapp beasts
Наконец, давайте зарегистрируем это приложение в файл, добавив его в список INSTALLED_APPS:
INSTALLED_APPS = [
'beasts',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
Класс! Теперь всё готово. Мы можем описать простую модель для Зверя (Beast), создать форму и шаблон для ее отображения конечному пользователю, а также обрабатывать файлы, которые они отправляют вместе с формой.
Создаём модель
Сначала опишем модель Зверь (Beast), которая будет соответствовать базе данных. Затем можно создать форму, чтобы пользователь мог сам добавлять информацию. Мы можем описать модель, которая расширит класс models.Model:
from django.db import models
class Beast(models.Model):
MOM_CLASSIFICATIONS = [
('XXXXX', 'Known Wizard Killer'),
('XXXX', 'Dangerous'),
('XXX', 'Competent wizard should cope'),
('XX', 'Harmless'),
('X', 'Boring'),
]
name = models.CharField(max_length=60)
mom_classification = models.CharField(max_length=5, choices=MOM_CLASSIFICATIONS)
description = models.TextField()
media = models.FileField(null=True, blank=True)
У каждого зверя есть имя (name), описание (description), сопутствующие материалы (media)(наблюдения за зверем), а также классификация ММ (mom_classification)(MМ означает Министерство магии).
Медиа (media) – это пример FileField, который был инициализирован аргументом null, имеющим значение True. Эта инициализация позволяет базе данных узнать, что поле “Носитель” может быть пустым, если у пользователя, вводящего данные, просто не знает носителя. Поскольку мы будем сопоставлять эту модель с формой – и Django позаботится о проверке за нас, нам нужно сообщить Django, что ввод формы для носителя может быть пустым, поэтому он не вызывает никаких исключений во время проверки. Значение null относится к базе данных, в то время как значение blank относится к проверке со стороны пользователя, чтобы вы могли перепроверить.
Примечание: Если вы хотите принудительно добавить носитель пользователем, установите для этих аргументов значение False.
FileField по умолчанию будет обрабатывать только один файл и позволит пользователю загружать один элемент из своей файловой системы. Позже мы также рассмотрим, как загружать несколько файлов.
Создание формы модели
Как только наша модель будет описана, мы привяжем ее к форме. Нам не нужно делать это вручную, так как Django может это сделать за нас:
from django.forms import ModelForm
from .models import Beast
class BeastForm(ModelForm):
class Meta:
model = Beast
fields = '__all__'
Мы создали Форму Зверя (BeastForm) и привязали к ней модель Зверь (Beast). Мы также установили для полей значение все, чтобы все поля нашей модели отображались, когда мы используем ее на HTML-странице. Вы можете индивидуально настроить поля здесь, если хотите, чтобы некоторые из них оставались скрытыми, но мы отобразим их все.
Регистрируем Модели с помощью прав администратора
Django автоматически создает сайт администратора для разработчиков, который будет использоваться на протяжении всего процесса разработки. Здесь мы можем протестировать наши модели и поля, не открывая страницы. Однако для пользователей вы захотите создать их и отключить веб-сайт администратора перед выходом в эфир.
Давайте зарегистрируем нашу модель на сайте администратора:
from django.contrib import admin
from .models import Beast
admin.site.register(Beast)
Регистрация URL-адресов
Когда структура приложения готова, модель определена и зарегистрирована, а также привязана к форме – давайте настроим URL-пути, которые позволят пользователю использовать это приложение. Для этого давайте создадим файл urls.py внутри вашего приложения. Затем мы можем продолжить и “включить” его содержание на уровне проекта urls.py.
Наше приложение будет выглядеть так:
from django.urls import path
from .import views
urlpatterns = [
path("", views.addbeast, name='addbeast')
]
На уровне проекта сделаем так:
urlpatterns = [
path("", include("reviews.urls"))
]
Мы добавляем пустую строку для вашего URL-адреса просто потому, что это маленький проект, и нам не нужно его усложнять. Мы еще не знаем, как приложение будет выглядеть, но зарегистрировали его имя здесь до его создания.
Создаём шаблон
Чтобы хранить наши шаблоны, давайте создадим папку шаблонов в нашем каталоге зверей. Это имя нельзя изменить, потому что Django будет искать HTML-шаблоны только в папках с именем templates.
Внутри вашей новой папки давайте добавим файл entry.html, имеющий <форму> (<form>), которая принимает поля, относящиеся к Зверю:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fantastic Beasts</title>
</head>
<body>
<form action="/" method="POST" enctype="multipart/form-data">
{% csrf_token %}
{% for entry in form %}
<div>
{{ entry.label_tag }}
</div>
<div>
{{entry}}
</div>
{% endfor %}
<button>
Save!
</button>
</form>
</body>
</html>
Атрибут action=”/” указывает на обработчик запроса, который активируется, когда пользователь нажмет кнопку “Сохранить!”. Ввод формы определяет, как кодируются данные, поэтому мы установили для типа enctype тип данных multipart/form data, чтобы разрешить загрузку файлов. Всякий раз, когда вы добавляете ввод типа “файл” (“file”) в форму Django, вам нужно будет установить тип enctype в multipart/form data .
{% csrf_token %} является еще одним обязательным элементом для любой формы, где action = “POST”. Это уникальный токен, который Django любезно создает для каждого клиента, чтобы обеспечить безопасность при приеме запросов. Токен CSRF уникален для каждого запроса POST из этой формы, и они делают атаки CSRF невозможными.
Атаки CSRF происходят, когда злоумышленники подделывают запрос вместо другого пользователя, обычно через другой домен, и если действительный токен отсутствует, запрос на сервер отклоняется.
Переменная form, которую мы повторяем для каждого цикла ({% for entry in form %}), будет передана в этот HTML-шаблон. Эта переменная является примером нашей BeastForm, и она поставляется с некоторыми классными фичами. Мы используем entry.label_tag, который возвращает нам метку для этого поля формы модели (меткой будет имя поля, если не указано иное), и мы помещаем поле формы в div, чтобы наша форма выглядела прилично.
Думаем над шаблоном
Теперь давайте создадим шаблон визуализации и подключим его к нашей серверной части. Мы начнем с импорта классов render и HttpResponseRedirect, которые являются встроенными классами Django, наряду с нашим объектом BeastForm.
Вместо представления на основе классов мы можем выбрать представление на основе функций, которое хорошо подходит для простых прототипов и демонстраций, подобных этому.
Если входящий запрос – это запрос POST, создается новый экземпляр BeastForm с телом запроса POST (полями) и файлами, отправленными через запрос. Django автоматически десериализует данные в объект и вводит request.FILES в качестве нашего поля файла:
from django.shortcuts import render
from .forms import BeastForm
from django.http import HttpResponseRedirect
def entry(request):
if request.method == 'POST':
form = BeastForm(request.POST, request.FILES)
if form.is_valid():
form.save()
return HttpResponseRedirect("/")
else:
form = BeastForm()
return render(request, "entry.html", {
"form": form
})
Чтобы проверить ввод, поскольку он может быть недействительным, мы можем использовать метод is_valid() экземпляра BeastForm, очистив форму, если она недействительна. В противном случае, если форма действительна – мы сохраняем ее в базе данных с помощью метода save() и перенаправляем пользователя на домашнюю страницу (которая также является страницей entry.html), предлагая пользователю ввести информацию о другом звере.
Где файлы?
Примечание. При таком подходе файлы сохраняются в базе данных, и обработка файлов не требуется. Однако так лучше не делать, а лучше использовать хорошую систему обработки файлов, о которой поговорим далее.
А пока давайте сделаем миграции и мигрируем, чтобы внести изменения в схему модели (поскольку мы этого раньше не делали). Как только мы запустим проект, мы сможем увидеть, как все это выглядит на сервере разработки. С терминала, пока виртуальная среда все еще активна, запустите:
$ python manage.py makemigrations
$ python manage.py migrate
$ python manage.py runserver
Теперь, как только мы перейдём по ссылке http://127.0.0.1:8000, используя браузер, вы должны увидеть что-то вроде этого:
Вы можете продолжить и заполнить форму случайным вводом и добавить файл; подойдет любой тип файла, так как мы назвали поле “носитель”, но присвоили ему общее поле файла.
Примечание: Вы можете принудительно использовать определенные типы файлов, такие как изображения, с помощью Django, о которых мы поговорим, как только рассмотрим более надежную систему хранения файлов и обработки нескольких файлов вместо одного.
После отправки формы вы можете просмотреть свои данные в базе данных через страницу администратора!
Храним файлы на жестком диске
На данный момент наш код способен хранить файлы в базе данных. Однако это нежелательная практика. Со временем наша база данных станет “толстой” и медленной, и мы не хотим, чтобы это произошло. Изображения уже некоторое время не хранятся в базах данных в виде больших двоичных объектов, и вы обычно сохраняете изображения на своем собственном сервере, на котором размещено приложение, или на внешнем сервере или сервисе, таком как AWS S3.
Давайте посмотрим, как мы можем хранить загруженные файлы на диске, в маленькой папке в рамках нашего проекта. Чтобы разместить их, давайте добавим папку uploads в разделе beasts и изменим поле “медиа” в BeastForm, чтобы оно было нацелено на папку, а не на базу данных:
media = models.FileField(upload_to="media", null=True, blank=True)
Мы установили для FileField значение “media”, которое еще не существует. Поскольку предположительно могут быть загружены и другие файлы, в папке uploads будет подкаталог с именем “media”, в который пользователи могут загружать изображения зверей.
Чтобы сообщить Django, где находится этот каталог “media”, мы добавляем его в файл settings.py:
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads/')
os.path.join(BASE_DIR, ‘uploads/’) добавляет “/uploads” в BASE_DIR – встроенную переменную, которая содержит путь к папке нашего проекта. MEDIA_ROOT сообщает Django, где будут находиться наши файлы.
Давайте сохраним все внесенные нами изменения, и как только мы применим наши миграции, Django создаст папку с именем “media”, как в [upload_to=”media”], в разделе “uploads”.
После этого все отправленные файлы будут сохранены в этой папке. Так мы исправили раздувание базы данных!
Загрузка нескольких файлов с помощью Django
Для обработки загрузки нескольких файлов не требуется много лишней работы. Все, что нам нужно сделать, это сообщить форме нашей модели, что поле мультимедиа может принимать более одного ввода.
Мы делаем это, добавляя поле widgets в нашу BeastsForm:
from django.forms import ModelForm, ClearableFileInput
from .models import Beast
class BeastForm(ModelForm):
class Meta:
model = Beast
fields = '__all__'
widgets = {
'media': ClearableFileInput(attrs={'multiple': True})
}
Теперь на странице entry.html пользователю разрешается выбрать несколько файлов и request.FILES будет содержать больше одного файла.
Принудительное использование файлов изображений с помощью Django ImageField
Django определяет дополнительный тип поля – ImageField, которое может ограничить пользовательский ввод файлами изображений. Мы собрали различные типы файлов для документации наших зверей, но чаще всего мы запрашиваем у пользователя ввод одного конкретного файла.
Давайте поменяем FileField на ImageField:
media = models.ImageField(upload_to="media", null=True, blank=True,)
FileField находится в Pillow images, которая является широко используемой библиотекой Python для обработки изображений и управления ими, поэтому, если она еще не установлена, вам будет предложено сделать исключение:
Cannot use ImageField because Pillow is not installed.
HINT: Get Pillow at https://pypi.org/project/Pillow/ or run command "python -m pip install Pillow".
Давайте продолжим и последуем совету терминала. Закройте сервер, чтобы запустить:
$ python -m pip install Pillow
Теперь, если мы продолжим, сделаем и применим наши миграции и запустим наш сервер разработки, мы увидим, что при попытке загрузить файл наши возможности ограничены изображениями.
Отображаем загруженные изображения
Скоро мы закончим. Давайте посмотрим, как мы можем извлечь и отобразить ваши сохраненные изображения.
Откройте файл beasts/views.py. Мы изменим наше предложение if, чтобы при успешной отправке формы страница не перезагружалась, а вместо этого перенаправляла нас на другую, которая будет содержать список всех животных и их информацию, а также их связанное изображение:
if form.is_valid():
form.save()
return HttpResponseRedirect("/success")
Теперь давайте продолжим и создадим представление для отображения страницы успеха. Внутри нашего файла beasts/views.py напишем следующее:
def success(request):
beasts = Beast.objects.order_by('name')
return render(request, "success.html", {
"beasts": beasts
})
На нашей странице мы перечислим имена и изображения зверей в нашей базе данных. Для этого мы просто собираем объекты Зверя, упорядочиваем их по имени и визуализируем в шаблон success.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fantastic Beasts</title>
</head>
<body>
{% for beast in beasts %}
<div>
{{ beast.name }}
</div>
{% if beast.media %}
<div>
<img src="{{ beast.media.url }}" width="500" height=auto alt="">
</div>
{% endif %}
{% endfor %}
</body>
</html>
Мы уже упоминали, что задача базы данных не в том, чтобы хранить файлы, ее задача – хранить пути к этим файлам. Любой экземпляр FileField или ImageField будет иметь атрибут URL, указывающий на местоположение файла в файловой системе. В теге <img> мы передаем этот атрибут атрибуту src для отображения изображений наших зверей.
По умолчанию система безопасности Django срабатывает, чтобы помешать нам передавать любые файлы из проекта наружу, это проверка безопасности. Тем не менее, мы хотим предоставить доступ к файлам в файле “media”, поэтому нам придется определить URL-адрес мультимедиа и добавить его в файл urls.py:
В файл settings.py добавим MEDIA_URL:
MEDIA_URL = "/beast-media/"
Здесь /name-between/ может быть любым, что вы хотите, хотя оно должно быть заключено в кавычки и косые черты. Теперь измените файл urls.py, чтобы добавить папку, которая обслуживает статические файлы:
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path("", include("ency.urls"))
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Функция static() сопоставляет MEDIA_URL с фактическим путем к месту нахождения наших файлов, MEDIA_ROOT. Запросы, которые пытаются получить доступ к любому из наших файлов, могут получить доступ через этот MEDIA_URL, который автоматически добавляется к атрибуту [url] экземпляров FileField и ImageField.
Если мы сохраним наши изменения и перейдем на наш сервер разработки, мы увидим, что всё хорошо работает .
Примечание: Этот метод можно использовать только при разработке и только в том случае, если MEDIA_URL является локальным.
Заключение
В этом руководстве мы рассмотрели, как загружать файлы, хранить файлы и, наконец, обслуживать файлы с помощью Django.
Мы создали небольшое приложение, которое, не очень полезно, но зато мы потренировались. Это наша отправная точка в мир загрузки файлов.