Сложные формы в Django

в 16:34, , рубрики: django, python, Песочница, метки: ,

image
Добрый день. Постараюсь рассказать о сложных формах в 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 %}

Вот теперь все. Теперь к практике.

Первоначальный вид формы.
Сложные формы в Django

После неверного ввода.
Сложные формы в Django

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

После неверного ввода.
Сложные формы в Django

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

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

Автор: chexov

Источник


  1. Andrey:

    Если я делаю {{ wizard.form }} – все нормально работает, но если я по вашему способу вместо этого подключаю темплейт для каждого поля – после отправки данных отображается тот же шаг, что и был… Подскажите пожалуйста, в чем дело?

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js