Добрый день. Постараюсь рассказать о сложных формах в Django. Все началось, когда в моем дипломе понадобилось сделать форму, которая состояла бы из других форм. Ведь если у вас есть две формы, которые вы используете, и тут понадобилась другая, которая является просто контейнером тех двух, вы же не будете создавать новую, копируя в неё все поля из старых, это очень тупо. Поэтому надо как-то их объединить. В свое время было FormWizard в Django, но он был крайне не удобным так что в новой версии её переделали на WizardView. Django конечно MVC, но я в статье все как можно детально постараюсь продемонстрировать, а потом уже можно все сжать используя ModelForm и циклы в шаблонах.
Поглядим на наши модели, ничего особенного, но чтобы было понятней, продемонстрируем.
class Citizenship(models.Model):
name = models.CharField(max_length = 50,verbose_name= u'наименование')
class CertificateType(models.Model):
name = models.CharField(max_length=50,verbose_name=u'наименование')
class Student(models.Model):
id = models.AutoField(primary_key=True, db_column='ID')
sex = models.CharField(max_length=6,verbose_name=u"пол")
citizenship = models.ForeignKey(Citizenship, verbose_name=u"гражданство")
doc = models.CharField(max_length=240,verbose_name=u"doc")
student_document_type = models.ForeignKey(CertificateType, related_name='student_document',verbose_name=u"документ студента")
parent_document_type = models.ForeignKey(CertificateType, related_name='parent_document',verbose_name=u"документ родителей")
def __unicode__(self):
try:
return unicode(self.fiochange_set.latest('event_date').fio)
except FioChange.DoesNotExist:
return u'No name'
class Contract(models.Model):
student = models.ForeignKey(Student,verbose_name=u'студента')
number = models.CharField(max_length=24,verbose_name=u"Номер договора")
student_home_phone = models.CharField(max_length=180, verbose_name=u"домашний телефон студента")
class FioChange(models.Model):
id = models.AutoField(primary_key=True, db_column='ID')
event_date = models.DateField(verbose_name=u'дата создания фио',null=True,blank=True)
student = models.ForeignKey(Student,verbose_name=u"студент")
fio = models.CharField(max_length=120, verbose_name=u"ФИО")
def __unicode__(self):
return unicode(self.fio)
Теперь ближе к делу, как говорится. Посмотрим на наши формы.
Формы(forms.py)
class NameModelChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return "%s"%obj.name
class StudentForm(forms.Form):
sex = forms.CharField(label=u'Пол',max_length = 10)
citizenship = NameModelChoiceField(label = u'Гражданство',queryset = Citizenship.objects.order_by('-name'),initial = Citizenship.objects.get(id=1))
doc = forms.CharField(label=u'Документ',max_length = 50)
student_document_type = NameModelChoiceField(label=u'Документ студента',queryset=CertificateType.objects.order_by('-name'),initial = CertificateType.objects.get(id = 1))
parent_document_type = NameModelChoiceField(label=u'Документ родителей',queryset=CertificateType.objects.order_by('-name'),initial = CertificateType.objects.get(id = 1))
event_date = forms.DateTimeField(required=False,label=u'Дата добавления: ', initial=datetime.date.today,help_text=u'Введите дату')
fio = forms.CharField(label=u'ФИО студента',max_length=60)
class ContractForm(forms.Form):
number = forms.CharField(label=u'Номер договора',max_length=5)
phone = forms.CharField(label=u'Телефон для контакта',max_length=7)
Две формы: одна чтобы заполнить данные студента, а другая форма — данные договора студента. Связанны они 1:N, т.е 1 студент может иметь N договоров. Поэтому нам надо иметь сразу форму, чтобы добавить студента и заключить с ним контракт(допустим так). Сразу напрашивается сделать так:
class AddStudentForm(StudentForm,ContractForm):
pass
Но это в корне не верно, потому что при таком наследовании все функции StudentForm перетрутся ContractForm, потому что они идентичны по названиям и параметрам(т.к наследованны от одного класса forms.Form).
Для этого и используем WizardView. Я опишу более сложный случай с SessionWizardView. Он позволяет заполнять данные пошагово, сохраняя промежуточные данные формы — это очень круто, при этом он не теряет индивидуальную валидацию форм. Кто смотрел документацию джанго, согласятся, пример какой то вообще хлипкий и не очень, понятно не много. Итак, что же нам надо: нужно отобразить 2 формы, после заполнения всех форм верно создать студента и его договор и, скажем ради прикалюхи, передавать сообщение к последующей форме о том, что предыдущая верно заполнена. По сути, вьюха хранит список форм и при переходе к другой форме вызывает методы валидации, и если форма не прошла валидацию, возвращает пользователя к не верной форме и просит заполнить верно. Опишем нашу вьюху.
View(view.py)
FORMS = [
("student", StudentForm),
("contract", ContractForm)
]
TEMPLATES = {
"student" : "student.html",
"contract" : "contract.html"
}
class AddStudentWizard(SessionWizardView):
def get_template_names(self):
return [TEMPLATES[self.steps.current]]
def get_context_data(self, form, **kwargs):
context = super(AddStudentWizard, self).get_context_data(form=form, **kwargs)
if self.steps.current == 'contract':
context.update({'ok': 'True'})
return context
def done(self, form_list, **kwargs):
student_form = form_list[0]
contract_form = form_list[1]
s = Student.objects.create(
sex = student_form['sex'],
citizenship = student_form['citizenship'],
doc = student_form['doc'],
student_document_type = student_form['student_document_type'],
parent_document_type = student_form['parent_document_type']
)
f = FioChange.objects.create(
student = s,
event_date = student_form['event_date'],
fio = student_form['fio']
)
c = Contract.objects.create(
student = s,
number = contract_form['number'],
student_home_phone = contract_form['phone']
)
return HttpResponseRedirect(reverse('liststudent'))
FORMS = [
("student", StudentForm),
("contract", ContractForm)
]
Описывает просто список форм с названиями, если передать [StudentForm,ContractForm], то форма будет доступна через ключ ‘0’ или ‘1’.
TEMPLATES = {
"student" : "student.html",
"contract" : "contract.html"
}
Описание, как можно через ключ получить нужный шаблон к форме, т.к я параноик и предпочитаю, чтобы все данные, преданные в шаблон через форму, описывались вручную, т.к, потом перейти на что-то иное(кроме Bootstrap) для оформления будет легче.
Пройдемся по функциям.
def get_template_names(self):
return [TEMPLATES[self.steps.current]]
Возвращает нам шаблон при переходе или первом отображении формы. Как видите, self.steps мы получаем варианты шагов, в самом первом отображении формы self.steps.current вернет “student”, а если мы не описывали бы FORMS, вернула бы ‘0’.
def get_context_data(self, form, **kwargs):
context = super(AddStudentWizard, self).get_context_data(form=form, **kwargs)
if self.steps.current == 'contract':
context.update({'ok': 'True'})
return context
Возвращает нам контекстные данные формы для шаблона. Итак, в задании мы должны отображать, что предыдущая форма верно заполнена, давайте дополним данные для шаблона контракта значением ok. Да, ok равняется именно строке ‘True’, потому что я в свое время столкнулся с неоднозначностью True, как booelean при варианте None и т.д, поэтому я теперь всегда пишу однозначные варианты соответствия.
def done(self, form_list, **kwargs)
Функция, которая вызывается, когда все формы заполнены верно, на этом этапе мы должны, что-то сделать с верными данными формы и отправить пользователя дальше.
Так мы тут и поступаем, создаем студента, его фио и контракт. И перенаправляем на страницу с ФИО студентов. Опишем теперь шаблоны для отображения форм. Начнем с базового.
base.html
<!DOCTYPE html>
{% load static %}
<html>
<head>
<script type="text/javascript" src="{% static 'bootstrap/js/jquery.js'%}"></script>
<link href="{% static 'bootstrap/css/bootstrap.css'%}" rel="stylesheet">
<script type="text/javascript" src="{% static 'bootstrap/js/bootstrap.js'%}"></script>
<style type="text/css">
#main-conteiter {
padding-top: 5%;
}
</style>
{% block head %}
<title>{% block title %}Example Wizard{% endblock %}</title>
{% endblock %}
</head>
<body>
<div class="container" id="main-conteiner">
{% block content %}
<!-- body -->
{% endblock %}
</div>
</body>
</html>
Не имеет особого смысла тут что-то конкретно расписывать.
Опишем наш базовый шаблон для отображения сложных форм.
wizard_template.html
{% extends "base.html" %}
{% block head %}
{{ block.super }}
{{ wizard.form.media }}
{% endblock %}
{% block content %}
<p class="text-info">Создание студента, шаг {{ wizard.steps.step1 }} из {{ wizard.steps.count }}</p>
<h3>{% block title_wizard %}{% endblock %}</h3>
<form class="well form-horizontal" action="." method="POST">{% csrf_token %}
{{ wizard.management_form }}
<div class="control-group">
{% block form_wizard %}
{% endblock %}
</div>
<div class="form-actions" style="padding-left: 50%">
{% block button_wizard %}
{% endblock %}
</div>
</form>
{% endblock %}
wizard.management_form нужно, чтобы наша форма заработала, указывать эту вещь всегда при работе с WizardView.
<div class="control-group">
Тут будет описываться наша форма.
<div class="form-actions" style="padding-left: 50%">
Тут кнопки для управления действиями. Да-да, стиль я засунул именно сюда, лень было выносить в файл.
Посмотрим на шаблон с описанием формы для ввода данных студента.
student.html
{% extends "wizard_template.html" %}
{% load i18n %}
{% block title_wizard %}
Добавление студета
{% endblock %}
{% block form_wizard %}
{% include "input_field.html" with f=wizard.form.sex %}
{% include "input_field.html" with f=wizard.form.citizenship %}
{% include "input_field.html" with f=wizard.form.doc %}
{% include "input_field.html" with f=wizard.form.student_document_type %}
{% include "input_field.html" with f=wizard.form.parent_document_type %}
{% include "input_field.html" with f=wizard.form.event_date %}
{% include "input_field.html" with f=wizard.form.fio %}
{% endblock %}
{% block button_wizard %}
<button type="submit" class="btn btn-primary">
<i class="icon-user icon-white"></i> Контракт <i class="icon-arrow-right icon-white"></i>
</button>
{% endblock %}
Тут описываем все поля формы именно вручную. Как видим, наша форма доступна через wizard.form, и так мы можем обойти все поля формы. Для более полного описания полей мы используем другой шаблон — описания поля формы.
input_field.html
<div class="control-group {% if f.errors %}error{% endif %}">
<label class="control-label" for="{{f.id_for_label}}">{{ f.label|capfirst }}</label>
<div class="controls">
{{f}}
<span class="help-inline">
{% for error in f.errors %}
{{ error|escape }}
{% endfor %}
</span>
</div>
</div>
Я использую этот шаблон для описания сообщений об ошибках к полям.
Посмотрим на шаблон описания формы контракта, тут почти то же самое, только добавляется кнопка назад к данным студента и кнопка для сохранения, которая создаст нам студента и его контракт, а потом перекинет на страницу со списком студентов.
contract.html
{% extends "wizard_template.html" %}
{% block title_wizard %}
Контракт студента
{% endblock %}
{% block form_wizard %}
{% if ok == 'True' %}
<div class="alert alert-success">
<button type="button" class="close" data-dismiss="alert">×</button>
<strong>Отлично!</strong>
Форма добавления ФИО студента верно заполнена.
</div>
{% endif %}
{% include "input_field.html" with f=wizard.form.number %}
{% include "input_field.html" with f=wizard.form.phone %}
{% endblock %}
{% block button_wizard %}
<button name="wizard_goto_step" class="btn btn-primary" type="submit" value="{{ wizard.steps.prev }}">
<i class="icon-user icon-white"></i> ФИО студента <i class="icon-arrow-left icon-white"></i>
</button>
<input type="submit" class="btn btn-primary" value="Сохранить"/>
{% endblock %}
Фух, вроде все описали, теперь надо подцепить все это дело к url и запустить проект.
url(r'^addstudent/$',AddStudentWizard.as_view(FORMS),name='addstudent'),
url(r'^liststudent$',StudentsView.as_view(),name='liststudent'),
Ах да, опишем еще view для списка студентов.
class StudentsView(TemplateView):
template_name = "list.html"
def get_context_data(self, **kwargs):
context = super(StudentsView, self).get_context_data(**kwargs)
context.update({
'students' : Student.objects.all()
})
return context
Опишем шаблон для этой view.
{% extends "base.html" %}
{% block content %}
{% for s in students %}
{{ s }}<br>
{% endfor %}
<br>
<a href="{% url addstudent %}" class="btn btn-primary">Добавить студента</a>
{% endblock %}
Вот теперь все. Теперь к практике.
Первоначальный вид формы.
После неверного ввода.
Переход к форме с контрактом при верном заполнении прошлой формы.
После неверного ввода.
Когда все верно заполнили и нажали на “Сохранить “, нас перебрасывает на страницу со студентами.
Вот и всё. Всем спасибо за внимание.
Автор: chexov
Если я делаю {{ wizard.form }} – все нормально работает, но если я по вашему способу вместо этого подключаю темплейт для каждого поля – после отправки данных отображается тот же шаг, что и был… Подскажите пожалуйста, в чем дело?