0,0
рейтинг
16 ноября 2012 в 20:34

Разработка → Сложные формы в Django из песочницы tutorial

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):
    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 %}

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

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

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


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


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


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


Вот и всё. Всем спасибо за внимание.
Дмитрий Дмитриенко @chexov
карма
7,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (17)

  • +7
    • 0
      Тут бы лучше вообще сделать пример и выложить код куда-нибудь. А то слишком много сорцов. Да и пример мне показался сложноватым.
    • +1
      Присовокупите к своему сообщению ссылку на автоматический проверяльщик PEP8, будем признательны.
    • +1
      спасибо, будем исправляться.
  • +1
    > Но это в корне не верно, потому что при таком наследовании все функции StudentForm
    > перетрутся ContractForm, потому что они идентичны по названиям и параметрам
    > (т.к наследованны от одного класса forms.Form).
    Что?
  • 0
    А почему для «Пол» вы не сделали CHOICES?
    Разве такой большой и неопределенный выбор?
    • +1
      В современном обществе — да.
    • +1
      исправился.
      • 0
        Ваш «SEX_CHOICES» лучше прописать где-нить один раз и ипортировать оттуда. Чем куча «SEX_CHOICES» в разных файлах(в вашем случае в models.py и в forms.py). Что если кому-то надо срочно добавить что-то или изменить? Придется искать во всех файлах где вы накопипастили этот «SEX_CHOICES» да и не DRY получается.
        • +1
          Я называю подобные вещи GENDER_CHOICES — смысл тот-же, визуально не так смущает.
          • 0
            Я тоже предпочитаю это слово)
          • 0
            возьмем на вооружение :)
  • +1
    По заголовку топика думал речь пойдет о действительно сложных формах, а не о визарде. Давайте представим ситуацию, что вам надо сделать то же самое, но ограничиться одной страницей. Чтобы долго не филосовствовать зачем это может быть нужно, таковы требования заказчика. В результате нам нужно показать на одной странице три формы и ограничиться одним HTTP запросом. Есть несколько вариантов и в текущей версии джанги с ними не все просто.

    1) Django-way — это сделать одну мега-форму, объединяющую все три. Если эти 3 формы больше ни где не используются, то вполне вариант. Если они есть где-то еще, то это может усложнить поддержку системы. Ну и еще копипаст — это плохо

    2) Мы делаем вьюху, которая умеет работать с нужным количеством форм. Тут все достаточно просто, но операция рутиная — generic views для этого нет. Почему-то нету generic views даже для редактирования объекта вместе со связанными объектами.

    3) Делаем прокси-объект, который содержит в себе все нужные формы и имеет интерфейсы, аналогичные формам джанги. Здесь требуется некоторый опыт работы с джанго-формами либо желание разобраться в том как они работают. Самый универсальный вариант, но свободной реализации такого не видел.

    Если извесные разработки по 2 и 3 случаю, и кто-то поделится ссылками, буду благодарен.
    • 0
      попробую разобраться, 2ое думаю самое реальное, что можно найти.
  • 0
    В wtforms это решается парой строк кода. Пусть StudentFrom — класс формы одного студента, тогда ListField(FormField(StudentForm)) — поле, содержащее список форм студентов.
  • 0
    Спасибо за пост,

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.