This page was translated from English by the community.Learn more and join the MDN Web Docs community.
Руководство часть 9: Работа с формами
На этом уроке мы покажем вам процесс работы с HTML-формами в Django. В частности, продемонстрируем самый простой способ построения формы для создания, обновления и удаления экземпляров модели. При этом мы расширим сайтместной библиотеки, чтобы библиотекари могли обновлять книги, создавать, обновлять и удалять авторов, используя наши собственные формы (а не возможности приложения администратора).
| Необходимые условия: | Завершите все предыдущие учебные темы, в том числеDjango руководство часть 8: Аутентификация пользователя и права доступа. |
|---|---|
| Цель: | Научиться понимать, как создавать формы, чтобы получать информацию от пользователей и обновлять базу данных. Узнать, как обобщённые классы отображения форм могут значительно упростить процесс создания форм при работе с одной моделью. |
In this article
Обзор
HTML форма - это группа из одного или нескольких полей/виджетов на веб-странице, которая используется для сбора информации от пользователей для последующей отправки на сервер. Формы являются гибким механизмом сбора пользовательских данных, поскольку имеют целый набор виджетов для ввода различных типов данных, как то: текстовые поля, флажки, переключатели, установщики дат и т. д. Формы являются относительно безопасным способом взаимодействия пользовательского клиента и сервера, поскольку они позволяют отправлять данные в POST-запросах, применяя защиту отМежсайтовой подделки запроса (Cross Site Request Forgery - CSRF)
Пока что мы не создавали каких-либо форм в этом учебнике, но мы встречались с ними в административной панели Django — например, снимок экрана ниже показывает форму для редактирования одной из наших моделей книг (Book), состоящую из нескольких списков выбора и текстовых редакторов.

Работа с формами может быть достаточно сложной! Разработчикам надо описать форму на HTML, проверить её валидность, а также, на стороне сервера, проверять введённые пользователем данные (а возможно и на стороне клиента), далее, в случае возникновения ошибок необходимо опять показать пользователю форму и, при этом, указать на то, что пошло не так, в случае же успеха проделать с данными необходимые операции и каким-то образом проинформировать об этом пользователя. Django, при работе с формами, берёт большую часть, описанной выше работы, на себя. Он предоставляет фреймворк, который позволяет вам определять форму и её поля программно, а затем использовать эти объекты и для генерации непосредственно кода HTML-формы, и для контроля за процессом валидации и других пользовательский взаимодействий с формой.
В данной части руководства мы покажем вам несколько способов создания и работы с формами и, в частности, как применение обобщённых классов работы с формой могут значительно уменьшить необходимый объем работы. Кроме того, мы расширим возможности нашего сайтаLocalLibrary, путём добавления функциональности для библиотекарей, которая будет позволять им обновлять информацию - добавим страницы для создания, редактирования, удаления книг и авторов (воспроизведём и расширим стандартные возможности административной части сайта).
Формы HTML
Начнём мы с краткого обзораФорм HTML. Рассмотрим простую форму HTML, имеющую поле для ввода имени некоторой "команды" ("team"), и, связанную с данным полем, текстовой меткой:

Форма описывается на языке HTML как набор элементов, расположенных внутри парных тэгов<form>...</form>. Любая форма содержит как минимум одно поле-тэгinput типаtype="submit".
<form action="/team_name_url/" method="post"> <label for="team_name">Enter name: </label> <input type="text" name="name_field" value="Default name for team." /> <input type="submit" value="OK" /></form>Здесь у нас только одно поле для ввода имени команды, но формаможет иметь любое количество элементов ввода и, связанных с ними, текстовых меток. Атрибут элементаtype определяет какого типа виджет будет показан в данной строке. Атрибутыname иid используются для однозначной идентификации данного поля в JavaScript/CSS/HTML, в то время какvalue содержит значение для поля (когда оно показывается в первый раз). Текстовая метка добавляется при помощи тэгаlabel (смотрите "Enter name", в предыдущем фрагменте) и имеет атрибутfor со значением идентификатораid, того поля, с которым данная текстовая метка связана.
Элементinput сtype="submit" будет показана как кнопка (по умолчанию), нажав на которую, пользователь отправляет введённые им данные на сервер (в данном случае только значение поля с идентификаторомteam_name). Атрибуты формы определяют каким методом будут отправлены данные на сервер (атрибутmethod) и куда (атрибутaction):
action: Это ресурс/URL-адрес куда будут отправлены данные для обработки. Если значение не установлено (то есть, значением поля является пустая строка), тогда данные будут отправлены в отображение (функцию, или класс), которое сформировало текущую страницу.method: HTTP-метод, используемый для отправки данных:post, илиget.- Метод
POSTдолжен всегда использоваться если отправка данных приведёт к внесению изменений в базе данных на сервере. Применение данного метода должно повысить уровень защиты от CSRF. - Метод
GETдолжен применяться только для форм, действия с которыми не приводят к изменению базы данных (например для поисковых запросов). Кроме того, данный метод рекомендуется применять для создания внешних ссылок на ресурсы сайта.
- Метод
Ролью сервера в первую очередь является отрисовка начального состояния формы — либо содержащей пустые поля, либо с установленными начальными значениями. После того как пользователь нажмёт на кнопку, сервер получит все данные формы, а затем должен провести их валидацию. В том случае, если форма содержит неверные данные, сервер должен снова отрисовать форму, показав при этом поля с правильными данными, а также сообщения, описывающие "что именно пошло не так". В тот момент, когда сервер получит запрос с "правильными" данными он должен выполнить все необходимые действия (например, сохранение данных, возврат результата поиска, загрузка файла и тому подобное), а затем, в случае необходимости, проинформировать пользователя.
Как вы видите, создание HTML-формы, валидация и возврат данных, переотрисовка введённых значений, при необходимости, а также выполнение желаемых действий с "правильными данными", в целом, может потребовать довольно больших усилий для того, чтобы все "заработало". Django делает этот процесс намного проще, беря на себя некоторые "тяжёлые" и повторяющиеся участки кода!
Процесс управления формой в Django
Управление формами в Django использует те же самые техники, которые мы изучали в предыдущих частях руководства (при показе информации из наших моделей): отображение получает запрос, выполняет необходимые действия, включающие в себя чтение данных из моделей, генерацию и возврат страницы HTML (из шаблона, в который передаётсяконтекст, содержащий данные, которые и будут показаны). Что делает данный процесс более сложным, так это то, что серверной части надо дополнительно обработать данные, предоставленные пользователем и, в случае возникновения ошибок, снова перерисовать страницу.
Диаграмма, представленная ниже, демонстрирует процесс работы с формой в Django, начиная с запроса страницы, содержащей форму (выделено зелёным цветом).

В соответствии с данной диаграммой, главными моментами, которые берут на себя формы Django являются:
Показ формы по умолчанию при первом запросе со стороны пользователя.
- Форма может содержать пустые поля (например, если вы создаёте новую запись в базе данных), или они (поля) могут иметь начальные значения (например, если вы изменяете запись, или хотите заполнить её каким-либо начальным значением).
- Форма в данный момент являетсянесвязанной, потому что она не ассоциируется с какими-либо введёнными пользователем данными (хотя и может иметь начальные значения).
Получение данных из формы (из HTML-формы) со стороны клиента и связывание их с формой (классом формы) на стороне сервера.
- Связывание данных с формой означает, что данные, введённые пользователем, а также возможные ошибки, при переотрисовке в дальнейшем, будут относиться именно к данной форме, а не к какой-либо ещё.
Очистка и валидация данных.
- Очистка данных - это их проверка на наличие возможных значений, или вставок в поля ввода (то есть очистка - это удаление неправильных символов, которые потенциально могут использоваться для отправки вредоносного содержимого на сервер), с последующей конвертацией очищенных данных в подходящие типы данных Python.
- Валидация проверяет, значения полей (например, правильность введённых дат, их диапазон и так далее)
Если какие-либо данные являются неверными, то выполнение перерисовки формы, но на этот раз, с уже введёнными пользователем данными и сообщениями об ошибках, описывающих возникшие проблемы.
Если все данные верны, то исполнение необходимых действий (например, сохранение данных, отправка писем, возврат результата поиска, загрузка файла и так далее)
Когда все действия были успешно завершены, то перенаправление пользователя на другую страницу.
Django предоставляет несколько инструментов и приёмов, которые помогают вам во время выполнения задач, описанных выше. Наиболее фундаментальным из них является классForm, который упрощает генерацию HTML-формы и очистку/валидацию её данных. В следующем разделе мы опишем процесс работы с формами при помощи практического примера по созданию страницы, которая позволит библиотекарям обновлять информацию о книгах.
Примечание:Понимание того, как используется классForm поможет вам когда мы будем рассматривать классы фреймворка Django, для работы с формами более "высокого уровня".
HTML-форма обновления книги. Класс Form и функция отображения
Данная глава будет посвящена процессу создания страницы, которая позволит библиотекарям обновлять информацию о книгах (в частности, вводить дату возврата книги). Для того, чтобы сделать это мы создадим форму, которая позволит пользователям вводить значение дат. Мы проинициализируем поле датой, равной 3 неделям, начиная с текущего дня, и, для того, чтобы библиотекарь не имел возможность ввести "неправильную" дату, мы добавим валидацию введённых значений, которая будет проверять, чтобы введённая дата не относилась к прошлому, или к слишком далёкому будущему. Когда будет получена "правильная" дата мы запишем её значение в полеBookInstance.due_back.
Данный пример будет использовать отображение на основе функции, а также продемонстрирует работу с классомForm. Следующие разделы покажут изменения, которые вам надо сделать, чтобы продемонстрировать работу форм в проектеLocalLibrary.
Класс Form
КлассForm является сердцем системы Django при работе с формами. Он определяет поля формы, их расположение, показ виджетов, текстовых меток, начальных значений, валидацию значений и сообщения об ошибках для "неправильных" полей (если таковые имеются). Данный класс, кроме того, предоставляет методы для отрисовки самого себя в шаблоне при помощи предопределённых форматов (таблицы, списки и так далее), или для получения значения любого элемента (позволяя выполнять более точную отрисовку).
Объявление класса формы Form
Синтаксис объявления для класса формыForm очень похож на объявление класса моделиModel, он даже использует те же типы полей (и некоторые похожие параметры). Это существенный момент, поскольку в обоих случаях нам надо убедиться, что каждое поле управляет правильным типом данных, соответствует нужному диапазону (или другому критерию) и имеет необходимое описание для показа/документации.
Для того, чтобы создать класс с возможностями базового классаForm мы должны импортировать библиотекуforms, наследовать наш класс от классаForm, а затем объявить поля формы. Таким образом, самый простой класс формы в нашем случае будет иметь вид, показанный ниже:
from django import formsclass RenewBookForm(forms.Form): renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).")Поля формы
В нашем случае мы имеем одно поле типаDateField, которое служит для ввода обновлённой даты возврата книги, которое будет отрендерено в HTML с пустым значением и текстовой меткой "Renewal date:", а также текстовым описанием: "Enter a date between now and 4 weeks (default 3 weeks)." Так как никаких дополнительных опций мы не определяем, то поле будет "получать" даты в следующем форматеinput_formats: YYYY-MM-DD (2016-11-06), MM/DD/YYYY (02/26/2016), MM/DD/YY (10/25/16), а для отрисовки по умолчанию, будет использоватьвиджет:DateInput.
Существует множество других типов полей для класса формы, которые по своей функциональности подобны соответствующим им эквивалентам типов полей для классов моделей:BooleanField,CharField,ChoiceField,TypedChoiceField,DateField,DateTimeField,DecimalField,DurationField,EmailField,FileField,FilePathField,FloatField,ImageField,IntegerField,GenericIPAddressField,MultipleChoiceField,TypedMultipleChoiceField,NullBooleanField,RegexField,SlugField,TimeField,URLField,UUIDField,ComboField,MultiValueField,SplitDateTimeField,ModelMultipleChoiceField,ModelChoiceField.
Общие аргументы для большинства полей перечислены ниже:
- required: Если
True, то данное поле не может быть пустым, или иметь значениеNone. Данное значение установлено по умолчанию. - label: Текстовая метка, используемая для рендеринга поля в HTML-код. Еслиlabel не определена, то Django попытается создать её значение при помощи имени поля, переводя первый символ в верхний регистр, а также заменяя символы подчёркивания пробелами (например, для переменной с именем renewaldate, будет создан следующий текст метки: _Renewal date).
- label_suffix: По умолчанию показывает двоеточие после текста метки (например, Renewal date**:**). Данный параметр позволяет вам указать любой суффикс по вашему желанию.
- initial: Начальное значение для поля при показе формы.
- widget: Применяемый виджет для поля.
- help_text (как показано в примере выше): Дополнительный текст, который может быть показан на форме, для описания того, как использовать поле.
- error_messages: Список сообщений об ошибках для данного поля. Вы можете переопределить его своими сообщениями, при необходимости.
- validators: Список функций, которые будут вызваны для валидации, введённого в поле значения.
- localize: Позволяет осуществить локализацию данных поля формы (например, формат ввода числовых значений, или дат).
- disabled: Если установлено в
True, то поле показывается, но его значение изменить нельзя. По умолчанию равноFalse.
Валидация
Django предоставляет несколько мест где вы можете осуществить валидацию ваших данных. Простейшим способом проверки значения одиночного поля является переопределение методаclean_<fieldname>() (здесь,<fieldname> это имя поля, которое вы хотите проверить). Например, мы хотим проверить, что введённое значениеrenewal_date находится между текущей датой и 4 неделями в будущем. Для этого мы создаём методclean_renewal_date(), как показано ниже:
from django import formsfrom django.core.exceptions import ValidationErrorfrom django.utils.translation import ugettext_lazy as _import datetime #for checking renewal date range.class RenewBookForm(forms.Form): renewal_date = forms.DateField(help_text="Enter a date between now and 4 weeks (default 3).") def clean_renewal_date(self): data = self.cleaned_data['renewal_date'] #Проверка того, что дата не выходит за "нижнюю" границу (не в прошлом). if data < datetime.date.today(): raise ValidationError(_('Invalid date - renewal in past')) #Проверка того, то дата не выходит за "верхнюю" границу (+4 недели). if data > datetime.date.today() + datetime.timedelta(weeks=4): raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead')) # Помните, что всегда надо возвращать "очищенные" данные. return dataНеобходимо отметить два важных момента. Первый это то, что мы получаем наши данные при помощи словаряself.cleaned_data['renewal_date'], а затем в конце возвращаем полученное значение, для проведения необходимых проверок. Данный шаг позволяет нам, при помощи валидаторов, получить "очищенные", проверенные, а затем, приведённые к стандартным типам, данные (в нашем случае к типу Pythondatetime.datetime).
Второй момент касается того случая, когда наше значение "выпадает за рамки" и мы "выкидываем" исключениеValidationError, в котором указываем текст, который мы хотим показать на форме, для случая когда были введены неправильные данные. Пример, показанный выше, оборачивает данный текст при помощифункции перевода Djangougettext_lazy() (импортируемую через_()), которая может вам пригодиться, если вы планируете перевести ваш сайт в будущем.
Примечание:Существует множество других методов и примеров валидации различных форм, которые можно найти вФормы и валидация поля (Django docs). Например, в случае, если у вас имеется много полей, которые зависят один от другого, вы можете переопределить функциюForm.clean() и, при необходимости, "выкинуть"ValidationError.
В целом, это все, что нам понадобится для создания формы в данном примере!
Копирование класса формы
Создайте и откройте файлlocallibrary/catalog/forms.py, а затем скопируйте в него весь код, указанный в предыдущем фрагменте.
Конфигурация URL-адресов
Перед созданием отображения давайте добавим соответствующую конфигурацию URL-адреса для страницы обновления книг. Скопируйте следующий фрагмент в нижнюю часть файлаlocallibrary/catalog/urls.py.
urlpatterns += [ url(r'^book/(?P<pk>[-\w]+)/renew/$', views.renew_book_librarian, name='renew-book-librarian'),]Данная конфигурация перенаправит запросы с адресов формата/catalog/book/<bookinstance id>/renew/ в функции с именемrenew_book_librarian() вviews.py, туда же передаст идентификатор id записиBookInstance в качестве параметра с именемpk. Шаблон соответствует только еслиpk это правильно отформатированныйuiid.
Примечание:Вместо имени "pk" мы можем использовать любое другое, по нашему желанию, потому что мы имеем полный контроль над функцией отображения (которого у нас нет в случае использования встроенного обобщённого класса отображения, который ожидает параметр с определённым именем). Тем не менее имяpk является понятным сокращением от "primary key", поэтому мы его тут и используем!
Отображение
Как было отмечено в разделеПроцесс управление формой в Django, отображение должно отрендерить форму по умолчанию, когда она вызывается в первый раз и, затем, перерендерить её, в том случае, если возникли какие-либо ошибки при работе с её полями. В случае же успеха, после обработки "правильных" данных отображение перенаправляет пользователя на новую (другую) страницу. Для того чтобы выполнить все эти действия, отображение должно знать вызвано ли оно в первый раз для отрисовки формы по умолчанию, а если это не так, то провести валидацию полученных данных.
Для форм, которые используютPOST-запрос при отправке информации на сервер, наиболее общей схемой проверки данного факта является следующая строка кодаif request.method == 'POST':.GET-запросу, а также первому запросу формы, в таком случае соответствует блокelse. Если вы хотите отправлять свои данные в видеGET-запроса, то в таком случае приёмом проверки того факта, что данный запрос первый (или последующий), является получение значения какого-либо поля формы (например, если значение скрытого поля формы пустое, то данный вызов является первым).
Процесс обновления книги приводит к изменению информации в базе данных, таким образом, в соответствии с нашими соглашениями, в таком случае мы должны применять запрос типаPOST. Фрагмент кода, представленный ниже, показывает (наиболее общую) схему работы для таких запросов.
from django.shortcuts import get_object_or_404from django.http import HttpResponseRedirectfrom django.urls import reverseimport datetimefrom .forms import RenewBookFormdef renew_book_librarian(request, pk): book_inst = get_object_or_404(BookInstance, pk=pk) # Если данный запрос типа POST, тогда if request.method == 'POST': # Создаём экземпляр формы и заполняем данными из запроса (связывание, binding): form = RenewBookForm(request.POST) # Проверка валидности данных формы: if form.is_valid(): # Обработка данных из form.cleaned_data #(здесь мы просто присваиваем их полю due_back) book_inst.due_back = form.cleaned_data['renewal_date'] book_inst.save() # Переход по адресу 'all-borrowed': return HttpResponseRedirect(reverse('all-borrowed') ) # Если это GET (или какой-либо ещё), создать форму по умолчанию. else: proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3) form = RenewBookForm(initial={'renewal_date': proposed_renewal_date,}) return render(request, 'catalog/book_renew_librarian.html', {'form': form, 'bookinst':book_inst})В первую очередь мы импортируем наш класс формы (RenewBookForm), а также другие необходимые объекты и методы:
get_object_or_404(): Возвращает определённый объект из модели в зависимости от значения его первичного ключа, или выбрасывает исключениеHttp404, если данной записи не существует.HttpResponseRedirect: Данный класс перенаправляет на другой адрес (HTTP код статуса 302).reverse(): Данная функция генерирует URL-адрес при помощи соответствующего имени URL конфигурации/преобразования и дополнительных аргументов. Это эквивалент Python тэгуurl, которые мы использовали в наших шаблонах.datetime: Библиотека Python для работы с датами и временим.
В отображении аргументpk мы используем в функцииget_object_or_404() для получения текущего объекта типаBookInstance (если его не существует, то функция, а следом и наше отображение прервут своё выполнение, а на странице пользователя отобразится сообщение об ошибке: "объект не найден"). Если запрос вызова отображенияне являетсяPOST-запросом, то мы переходим к условному блокуelse, в котором мы создаём форму по умолчанию и передаём ей начальное значенияinitial для поляrenewal_date (выделено жирным ниже, - 3 недели, начиная с текущей даты).
book_inst = get_object_or_404(BookInstance, pk=pk) # Если это GET (или другой метод), тогда создаём форму по умолчанию else: proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3) form = RenewBookForm(initial={'renewal_date': proposed_renewal_date,}) return render(request, 'catalog/book_renew_librarian.html', {'form': form, 'bookinst':book_inst})После создания формы мы вызываем функциюrender(), чтобы создать HTML страницу; передаём ей в качестве параметров шаблон и контекст, который содержит объект формы. Кроме того, контекст содержит объект типаBookInstance, который мы будем использовать в шаблоне, для получения информации об обновляемой книге.
Если все таки у насPOST-запрос, тогда мы создаём объект с именемform и заполняем его данными, полученными из запроса. Данный процесс называется связыванием (или, биндингом, от англ. "binding") и позволяет нам провести валидацию данных. Далее осуществляется валидация формы, при этом проверяются все поля формы — для этого используются как код обобщённого класса, так и пользовательских функций, в частности нашей функции проверки введённых датclean_renewal_date().
book_inst = get_object_or_404(BookInstance, pk=pk) # Если данный запрос типа POST, тогда if request.method == 'POST': # Создаём экземпляр формы и заполняем данными из запроса (связывание, binding): form = RenewBookForm(request.POST) # Проверка валидности формы: if form.is_valid(): # process the data in form.cleaned_data as required (here we just write it to the model due_back field) book_inst.due_back = form.cleaned_data['renewal_date'] book_inst.save() # redirect to a new URL: return HttpResponseRedirect(reverse('all-borrowed') ) return render(request, 'catalog/book_renew_librarian.html', {'form': form, 'bookinst':book_inst})Если формы не прошла валидацию, то мы снова вызываем функциюrender(), но на этот раз форма будет содержать сообщения об ошибках.
Если форма прошла валидацию, тогда мы можем начать использовать данные, получая их из атрибута формыform.cleaned_data (то есть,data = form.cleaned_data['renewal_date']). Здесь мы просто сохраняем данные в полеdue_back, соответствующего объектаBookInstance.
Предупреждение:Хотя вы также можете получить доступ к данным формы непосредственно через запрос (напримерrequest.POST['renewal_date'], илиrequest.GET['renewal_date'] (в случае GET-запроса), это НЕ рекомендуется. Очищенные данные проверены на вредоносность и преобразованы в типы, совместимые с Python.
Последним шагом в части обработки формы представления является перенаправление на другую страницу, обычно страницу «Успех». В нашем случае мы используем объект классаHttpResponseRedirect и функциюreverse() для перехода к отображению с именем'all-borrowed' (это было домашним заданием вРуководство часть 8: Аутентификация и разграничение доступа). Если вы не создали данную страницу, то просто укажите переход на домашнюю страницу сайта по адресу '/').
Все это необходимо для управления формой как таковой, но нам нужно как-то ограничить доступ к отображению (открыть доступ только библиотекарям). Мы могли бы создать новое разрешение (permission) в классеBookInstance ("can_renew"), но мы пойдём простым путём и воспользуемся функцией-декоратором@permission_required вместе с нашим существующим разрешениемcan_mark_returned.
Окончательный вид отображения показан ниже. Пожалуйста, скопируйте данный текст в нижнюю часть файлаlocallibrary/catalog/views.py.
from django.contrib.auth.decorators import permission_requiredfrom django.shortcuts import get_object_or_404from django.http import HttpResponseRedirectfrom django.urls import reverseimport datetimefrom .forms import RenewBookForm@permission_required('catalog.can_mark_returned')def renew_book_librarian(request, pk): """ View function for renewing a specific BookInstance by librarian """ book_inst = get_object_or_404(BookInstance, pk=pk) # If this is a POST request then process the Form data if request.method == 'POST': # Create a form instance and populate it with data from the request (binding): form = RenewBookForm(request.POST) # Check if the form is valid: if form.is_valid(): # process the data in form.cleaned_data as required (here we just write it to the model due_back field) book_inst.due_back = form.cleaned_data['renewal_date'] book_inst.save() # redirect to a new URL: return HttpResponseRedirect(reverse('all-borrowed') ) # If this is a GET (or any other method) create the default form. else: proposed_renewal_date = datetime.date.today() + datetime.timedelta(weeks=3) form = RenewBookForm(initial={'renewal_date': proposed_renewal_date,}) return render(request, 'catalog/book_renew_librarian.html', {'form': form, 'bookinst':book_inst})Шаблон
Создайте шаблон, на который ссылается наше отображение (/catalog/templates/catalog/book_renew_librarian.html) и скопируйте в него код, указанный ниже:
{% extends "base_generic.html" %}{% block content %} <h1>Renew: {{bookinst.book.title}}</h1> <p>Borrower: {{bookinst.borrower}}</p> <p{% if bookinst.is_overdue %}{% endif %}>Due date: {{bookinst.due_back}}</p> <form action="" method="post"> {% csrf_token %} <table> {{ form }} </table> <input type="submit" value="Submit" /> </form>{% endblock %}Большая его часть вам знакома из предыдущих частей руководства. Мы расширяем базовый шаблон, а затем замещаем блок содержимогоcontent. У нас имеется возможность ссылаться на переменную{{bookinst}} (и её поля) поскольку мы передали её в объект контекста при вызове функцииrender(). Здесь мы используем данный объект для вывода заголовка книги, дат её получения и возврата.
Код формы относительно прост. В первую очередь мы объявляем тэгform, затем определяем куда будут отправлены данные (action) и каким способом (method, в данном случае "HTTP POST") — если обратитесь к обзору разделаФормы HTML в верхней части данной страницы, то найдёте там замещение, что пустое значение атрибутаaction, означает, что данные из формы будут переданы обратно по текущему URL-адресу данной страницы (чего мы и хотим!). Внутри тэга формы мы объявляем кнопкуsubmit при помощи которой мы можем отправить наши данные. Блок{% csrf_token %}, добавленный первой строкой внутри блока формы, является частью фреймворка Django и служит для борьбы с CSRF.
Примечание:Добавляйте{% csrf_token %} в каждый шаблон Django, в котором вы создаёте форму для отправки данных методомPOST. Это поможет уменьшить вероятность взлома вашего сайта злоумышленниками.
Все что осталось, это указать переменную{{form}}, которую мы передали в шаблон в словаре контекста. Возможно это вас не удивит, но таким образом мы предоставим возможность форме отрендерить свои поля с их метками, виджетами и дополнительными текстами, и в результате мы получим следующее:
<tr> <th><label for="id_renewal_date">Renewal date:</label></th> <td> <input name="renewal_date" type="text" value="2016-11-08" required /> <br /> <span >Enter date between now and 4 weeks (default 3 weeks).</span > </td></tr>Примечание:Возможно это не очевидно, поскольку наша форма содержит только одно поле, но по умолчанию каждое поле формы помещается в её собственную строку таблицы (поэтому переменная{{form}} находится внутри тэгаtable. Тот же результат можно получить, если воспользоваться следующим вызовом{{ form.as_table }}.
Если вы ввели неправильную дату, то на странице вы должны получить список сообщений об ошибках (показано жирным ниже).
<tr> <th><label for="id_renewal_date">Renewal date:</label></th> <td> <ul> <li>Invalid date - renewal in past</li> </ul> <input name="renewal_date" type="text" value="2015-11-08" required /> <br /> <span >Enter date between now and 4 weeks (default 3 weeks).</span > </td></tr>Другие варианты применения переменной шаблона form
В простом случае применения{{form}} как показано выше, каждое поле рендерится в виде отдельной строки таблицы. Кроме того, вы можете отрендерить каждое поле как список элементов ({{form.as_ul}} ), или как параграф ({{form.as_p}}).
Что ещё больше вдохновляет, так это то, что вы можете полностью контролировать процесс рендеринга любой части формы, используя для этого дот-нотацию (точку). Например, мы можем получить доступ к следующим полям поля формыrenewal_date:
{{form.renewal_date}}:само поле.{{form.renewal_date.errors}}: Список ошибок.{{form.renewal_date.id_for_label}}: Идентификатор текстовой метки.{{form.renewal_date.help_text}}: Дополнительный текст.- и так далее!
Примеры того как вручную отрендерить формы в шаблонах, а также пробежать циклом по шаблонным полям, смотритеРаботы с формами > Ручная работа с формами (Django docs).
Тестирование страницы
Если вы выполнили задание вDjango руководство часть 8: Аутентификация и разрешение доступа, то у вас должна быть страница со списком всех книг в наличии библиотеки и данный список (страница) должен быть доступен только её сотрудникам. На данной странице в каждом пункте (для каждой книги) мы можем добавить ссылку на нашу новую страницу обновления книги.
{% if perms.catalog.can_mark_returned %}- <a href="{% url 'renew-book-librarian' bookinst.id %}">Renew</a>{% endif %}Примечание:Помните что, для того чтобы перейти на страницу обновления книги, ваш тестовый логин должен иметь разрешение доступа типа "catalog.can_mark_returned"(возможно надо воспользоваться вашим аккаунтом для суперпользователя).
Вы можете попробовать вручную создать URL-адрес для тестирования, например —http://127.0.0.1:8000/catalog/book/<bookinstance_id>/renew/ (правильный идентификатор записи id для bookinstance можно получить, если перейти на страницу детальной информации книги и скопировать полеid).
Как теперь все это выглядит?
Если все получилось как надо, то форма по умолчанию должна выглядеть следующим образом:

А такой наша форма будет в случае ввода неправильной даты:

Список всех книг с ссылками на страницу обновления данных:

Класс ModelForm
Создание класса формыForm при помощи примера, описанного выше, является довольно гибким способом, позволяющим вам создавать формы любой структуры которую вы пожелаете, в связке с любой моделью, или моделями.
Тем не менее, если вам просто нужна форма для отображения полей одиночной модели, тогда эта самая модель уже содержит большую часть информации, которая вам нужна для построения формы: сами поля, текстовые метки, дополнительный текст и так далее. И чтобы не воспроизводить информацию из модели для вашей формы, проще воспользоваться классомModelForm, который помогает создавать формы непосредственно из модели. КлассModelForm может применяться в ваших отображениях точно таким же образом как и "классический" класс формыForm.
Базовая реализацияModelForm содержит тоже поле как и ваш предыдущий класс формыRenewBookForm, что и показано ниже. Все что вам необходимо сделать, - внутри вашего нового класса добавить классMeta и связать его с модельюmodel (BookInstance), а затем перечислить поля модели в полеfields которые должны быть включены в форму (вы можете включить все поля при помощиfields = '__all__', или можно воспользоваться полемexclude (вместоfields), чтобы определить поля модели, которыене нужно включать).
from django.forms import ModelFormfrom .models import BookInstanceclass RenewBookModelForm(ModelForm): class Meta: model = BookInstance fields = ['due_back',]Примечание:Это не выглядит сильно проще, чем просто использовать классForm (и это действительно так, поскольку мы используем только одно поле). Тем не менее, если вы хотите иметь много полей, то такой способ построения формы может значительно уменьшить количество кода и ускорить разработку!
Оставшаяся часть информации касается объявления полей модели (то есть, текстовых меток, виджетов, текстов, сообщений об ошибках). Если они недостаточно "правильные", то тогда мы можем переопределить их в нашем классеMeta при помощи словаря, содержащего поле, которое надо изменить и его новое значение. Например, в нашей форме мы могли бы поменять текст метки для поля "Renewal date" (вместо того, чтобы оставить текст по умолчанию:Due date), а кроме того мы хотим написать другой вспомогательный текст. КлассMeta, представленный ниже, показывает вам, как переопределить данные поля. Кроме того, при необходимости, вы можете установить значения для виджетовwidgets и сообщений об ошибкахerror_messages.
class Meta: model = BookInstance fields = ['due_back',] labels = { 'due_back': _('Renewal date'), } help_texts = { 'due_back': _('Enter a date between now and 4 weeks (default 3).'), }Чтобы добавить валидацию, вы можете использовать тот же способ как и для классаForm — вы определяете функцию с именемclean_field_name() из которой выбрасываете исключениеValidationError, если это необходимо. Единственным отличием от нашей оригинальной формы будет являться то, что поле модели имеет имяdue_back, а не "renewal_date".
from django.forms import ModelFormfrom .models import BookInstanceclass RenewBookModelForm(ModelForm): def clean_due_back(self): data = self.cleaned_data['due_back'] #Проверка того, что дата не в прошлом if data < datetime.date.today(): raise ValidationError(_('Invalid date - renewal in past')) #Check date is in range librarian allowed to change (+4 weeks) if data > datetime.date.today() + datetime.timedelta(weeks=4): raise ValidationError(_('Invalid date - renewal more than 4 weeks ahead')) # Не забывайте всегда возвращать очищенные данные return data class Meta: model = BookInstance fields = ['due_back',] labels = { 'due_back': _('Renewal date'), } help_texts = { 'due_back': _('Enter a date between now and 4 weeks (default 3).'), }Теперь классRenewBookModelForm является функциональным эквивалентом нашему предыдущему классуRenewBookForm. Вы можете импортировать и использовать его в тех же местах, где иRenewBookForm.
Обобщённые классы отображения для редактирования
Алгоритм управления формой, который мы использовали в нашей функции отображения, является примером достаточно общего подхода к работе с формой. Django старается абстрагировать и упростить большую часть данной работы, путём широкого примененияобобщённых классов отображений, которые служат для создания, редактирования и удаления отображений на основе моделей. Они не только управляют поведением отображения, но, кроме того, они из вашей модели автоматически создают класс формы (ModelForm).
Примечание:В дополнение к отображениям для редактирования, описываемых здесь, существует также классFormView, который по своему предназначению находится где-то между "простой" функцией отображения и другими обобщёнными отображениями, то есть в каком-то смысле, в диапазоне: "гибкость" против "усилия при программировании". ПрименяяFormView, вы все ещё нуждаетесь в создании классаForm, но вам не нужно реализовывать всю "стандартную" функциональность работы с формой. Вместо этого, вы должны просто реализовать функцию, которая будет вызвана в тот момент, когда станет понятно, что получаемые из формы данные, "правильные" (валидны).
В данном разделе мы собираемся использовать обобщённые классы для редактирования, для того, чтобы создать страницы, который добавляют функциональность создания, редактирования и удаления записей типаAuthor из нашей библиотеки — предоставляя базовую функциональность некоторых частей административной части сайта (это может быть полезно для случаев, когда вам нужно создать административную часть сайта, которая, в отличие от стандартной, была бы более гибкой).
Отображения
Откройте файл отображений (locallibrary/catalog/views.py) и добавьте следующий код в его нижнюю часть:
from django.views.generic.edit import CreateView, UpdateView, DeleteViewfrom django.urls import reverse_lazyfrom .models import Authorclass AuthorCreate(CreateView): model = Author fields = '__all__' initial={'date_of_death':'12/10/2016',}class AuthorUpdate(UpdateView): model = Author fields = ['first_name','last_name','date_of_birth','date_of_death']class AuthorDelete(DeleteView): model = Author success_url = reverse_lazy('authors')Как вы видите, для создания отображений вам надо наследоваться от следующих классовCreateView,UpdateView иDeleteView (соответственно), а затем связать их с соответствующей моделью.
Для случаев "создать" и "обновить" вам также понадобится определить поля для показа на форме (применяя тот же синтаксис, что и дляModelForm). В этом случае мы демонстрируем синтаксис и для показа "всех" полей, и перечисление их по отдельности. Также вы можете указать начальные значения для каждого поля, применяя словарь паримяполя*/*значение_ (в целях демонстрации, в нашем примере мы явно указываем дату смерти — если хотите, то вы можете удалить это поле). По умолчанию отображения перенаправляют пользователя на страницу "успеха", показывая только что созданные/отредактированные данные (записи в модели). В нашем случае это, созданная в предыдущей части руководства, подробная информация об авторе. Вы можете указать альтернативное перенаправление при помощи параметраsuccess_url (как в примере с классомAuthorDelete).
КлассуAuthorDelete не нужно показывать каких либо полей, таким образом их не нужно и декларировать. Тем не менее, вам нужно указатьsuccess_url, потому что, в данном случае, для Django не очевидно что делать после успешного выполнения операции удаления записи. Мы используем функциюreverse_lazy() для перехода на страницу списка авторов после удаления одного из них —reverse_lazy() это более "ленивая" версияreverse().
Шаблоны
Отображения "создать" и "обновить" используют шаблоны с именемmodel_name_form.html, по умолчанию: (вы можете поменять суффикс на что-нибудь другое, при помощи поляtemplate_name_suffix в вашем отображении, например,template_name_suffix = '_other_suffix')
Создайте файл шаблонаlocallibrary/catalog/templates/catalog/author_form.html и скопируйте в него следующий текст.
{% extends "base_generic.html" %}{% block content %} <form action="" method="post"> {% csrf_token %} <table> {{ form.as_table }} </table> <input type="submit" value="Submit" /> </form>{% endblock %}Это напоминает наши предыдущие формы и рендер полей при помощи таблицы. Заметьте, что мы снова используем{% csrf_token %}.
Отображения "удалить" ожидает "найти" шаблон с именем форматаmodel_name_confirm_delete.html (и снова, вы можете изменить суффикс при помощи поля отображенияtemplate_name_suffix). Создайте файл шаблонаlocallibrary/catalog/templates/catalog/author_confirm_delete.html и скопируйте в него текст, указанный ниже.
{% extends "base_generic.html" %}{% block content %} <h1>Delete Author</h1> <p>Are you sure you want to delete the author: {{ author }}?</p> <form action="" method="POST"> {% csrf_token %} <input type="submit" value="Yes, delete." /> </form>{% endblock %}Настройки URL-адресов
Откройте файл конфигураций URL-адресов (locallibrary/catalog/urls.py) и добавьте в его нижнюю часть следующие настройки:
urlpatterns += [ url(r'^author/create/$', views.AuthorCreate.as_view(), name='author_create'), url(r'^author/(?P<pk>\d+)/update/$', views.AuthorUpdate.as_view(), name='author_update'), url(r'^author/(?P<pk>\d+)/delete/$', views.AuthorDelete.as_view(), name='author_delete'),]Здесь нет ничего нового! Как вы видите отображения являются классами и следовательно должны вызываться через метод.as_view(). Паттерны URL-адресов для каждого случая должны быть вам понятны. Мы обязаны использоватьpk как имя для "захваченного" значения первичного ключа, так как параметр именно с таким именем ожидается классами отображения.
Страницы создания, обновления и удаления автора теперь готовы к тестированию (мы не будем создавать на них ссылки в отдельном меню, но вы, если хотите, можете их сделать).
Примечание:Наблюдательные пользователи могли заметить, что мы ничего не делаем, чтобы предотвратить несанкционированный доступ к страницам! Мы оставили это в качестве упражнения для вас (подсказка: вы можете использоватьPermissionRequiredMixin и, либо создать новое разрешение, или воспользоваться нашим прежнимcan_mark_returned).
Тестирование страницы
Залогиньтесь на сайте с аккаунтом, который позволит вам получить доступ к страницам редактирования данных (и записей) автора.
Затем перейдите на страницу создания новой записи автора:http://127.0.0.1:8000/catalog/author/create/, которая должна быть похожей на следующий скриншот.

Введите в поля значения и нажмите на кнопкуSubmit, чтобы сохранить новую запись об авторе. После этого, вы должны были перейти на страницу редактирования только что созданного автора, имеющий адрес, похожий на следующийhttp://127.0.0.1:8000/catalog/author/10.
У вас есть возможность редактирования записей при помощи добавления/update/ в конец адреса подробной информации (то есть,http://127.0.0.1:8000/catalog/author/10/update/) — мы не показываем скриншот, потому что он выглядит в точности также как страница "создать"!
И последнее, мы можем удалить страницу, добавляя строку/delete/ в конец адреса подробной информации автора (то есть,http://127.0.0.1:8000/catalog/author/10/delete/). Django должен показать страницу, которая похожа на представленную ниже. НажмитеYes, delete., чтобы удалить запись и перейти на страницу со списком авторов.

Проверьте себя
Создайте несколько форм создания, редактирования и удаления записей в моделиBook. При желании, вы можете использовать тоже структуры как и в случае с модельюAuthors. Если ваш шаблонbook_form.html является просто копией шаблонаauthor_form.html, тогда новая страница "create book" будет выглядеть как на следующем скриншоте:

Итоги
Создание и управление формами может быть достаточно сложным! Django делает этот процесс намного проще, предоставляя прикладные механизмы объявления, рендеринга и проверки форм. Более того, Django предоставляет обобщённые классы редактирования форм, которые могут выполнятьпрактически любую работу по созданию, редактированию и удалению записей, связанных с одиночной моделью.
Существует много чего ещё, что можно делать с формами (ознакомьтесь со списком ниже), но теперь вы должны понимать как добавлять базовые формы и создавать код управления формой на вашем сайте.
Смотрите также
- Работа с формами (Django docs)
- Создание вашего первого приложения, часть 4 > Создание простой формы (Django docs)
- Forms API (Django docs)
- Поля класса Form (Django docs)
- Класс Form и валидация поля (Django docs)
- Управление классом Form из классов отображений (Django docs)
- Создание форм из моделей (Django docs)
- Обобщённые отображения для редактирования (Django docs)