Pull to refresh

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

Reading time 9 min
Views 25K
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 %}

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

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

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


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


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


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


Вот и всё. Всем спасибо за внимание.
Tags:
Hubs:
+19
Comments 17
Comments Comments 17

Articles