
Добрый день. Постараюсь рассказать о сложных формах в 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): SEX_CHOICES = ( ('m',u"мужской"), ('w',u"женский"), ) sex = models.CharField(max_length=1,verbose_name=u"пол",choices=SEX_CHOICES) 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): 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_CHOICES = ( ('m',u"мужской"), ('w',u"женский"), ) sex = forms.ChoiceField(label=u'Пол', choices = SEX_CHOICES) 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].cleaned_data contract_form = form_list[1].cleaned_data 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 %}
Вот теперь все. Теперь к практике.
Первоначальный вид формы.
После неверного ввода.

Переход к форме с контрактом при верном заполнении прошлой формы.

После неверного ввода.

Когда все верно заполнили и нажали на “Сохранить “, нас перебрасывает на страницу со студентами.

Вот и всё. Всем спасибо за внимание.
