Домой Edit me on GitHub

2020-12-05

Каналы передачи данных | Сетевое программирование | Базы данных | Основы Веб-программирования

Формы

WTForms

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from jinja2 import Template
from wtforms import BooleanField, Form, StringField, validators


class RegistrationForm(Form):
    username = StringField('Username', [validators.Length(min=4, max=25)])
    email = StringField('Email Address', [validators.Length(min=6, max=35)])
    rules = BooleanField('I accept the site rules',
                         [validators.InputRequired()])

if __name__ == '__main__':
    form = RegistrationForm(username="root")
    template = Template("""
<form method="POST" action="">
    {% for field in form.data %}
        {{ form[field].label }} |
        {{ form[field] }}
        <br />
    {% endfor %}
    <input type="submit" value="Ok">
</form>
    """)
    print(template.render(form=form))
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<form method="POST" action="">
    
        <label for="username">Username</label> |
        <input id="username" name="username" type="text" value="root">
        <br />
    
        <label for="rules">I accept the site rules</label> |
        <input id="rules" name="rules" type="checkbox" value="y">
        <br />
    
        <label for="email">Email Address</label> |
        <input id="email" name="email" type="text" value="">
        <br />
    
    <input type="submit" value="Ok">
</form>
|
|
|

Deform

Deform — это Python библиотека для генерации форм. Deform использует Colander как генератор схемы, Peppercorn для десериализации данных из формы и шаблонизатор Chameleon.

Основные задачи, которые выполняет Deform:

  • Генерирует форму
  • Имеет набор виджетов для форм
  • Умеет генерировать AJAX формы
  • Использует схемы Colander
  • Использует шаблоны Chameleon (Но можно использовать и другие, например Jinja2 или Mako)

Примеры форм http://deformdemo.repoze.org/

Colander

См.также

Colander - десериализует данные полученные как XML, JSON, HTTP POST запрос и проверяет правильность их заполнения по заранее заданной схеме.

  • Определяет структуру (схему) формы
  • Проверяет содержимое формы

Простая форма

Для создания простой формы нам понадобится:

  • Схема Colander
  • Объект Form из Deform
  • WSGI-приложение, которое получает POST параметры из запроса
  • Шаблон страницы с формой

Схема Colander

0.simple_form.py - Colander схема
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import colander
import deform
from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader('templates'))


class Contact(colander.MappingSchema):
    email = colander.SchemaNode(colander.String(), validator=colander.Email())
    name = colander.SchemaNode(colander.String())
    message = colander.SchemaNode(colander.String(),
                                  widget=deform.widget.TextAreaWidget())


def simple_form(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    from webob import Request
    request = Request(environ)

    form = deform.Form(Contact(), buttons=('submit',))
    template = env.get_template('simple.html')
    if request.POST:
        submitted = request.POST.items()
        try:
            form.validate(submitted)
        except deform.ValidationFailure as e:
            return template.render(form=e.render())
    data = {'email': 'jon.staley@fundingoptions.com',
            'name': 'Jon',
            'message': 'Hello World'}
    return template.render(form=form.render(data))


if __name__ == '__main__':
    from paste.httpserver import serve

    serve(simple_form, host='0.0.0.0', port=8000)

Форма deform.Form

0.simple_form.py - Форма от Deform
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import colander
import deform
from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader('templates'))


class Contact(colander.MappingSchema):
    email = colander.SchemaNode(colander.String(), validator=colander.Email())
    name = colander.SchemaNode(colander.String())
    message = colander.SchemaNode(colander.String(),
                                  widget=deform.widget.TextAreaWidget())


def simple_form(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    from webob import Request
    request = Request(environ)

    form = deform.Form(Contact(), buttons=('submit',))
    template = env.get_template('simple.html')
    if request.POST:
        submitted = request.POST.items()
        try:
            form.validate(submitted)
        except deform.ValidationFailure as e:
            return template.render(form=e.render())
    data = {'email': 'jon.staley@fundingoptions.com',
            'name': 'Jon',
            'message': 'Hello World'}
    return template.render(form=form.render(data))


if __name__ == '__main__':
    from paste.httpserver import serve

    serve(simple_form, host='0.0.0.0', port=8000)

Шаблон simple.html

templates/simple.html
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>simple</title>
    </head>
    <body>
        {{ form }}
    </body>
</html>
../../_images/simple_form.png

Сгенерированная форма

../../_images/simple_form_validation.png

Валидация формы

Добавим стилей:

templates/simple.html с CSS стилями.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <title>simple</title>
        <link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.96.1/css/materialize.min.css" />
        <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
        <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.96.1/js/materialize.min.js"></script>
        <style type="text/css">
            .deformFormFieldset {
                border: none;
                padding: 0;
                margin: 0;
            }
            .control-label {
                left: 0 !important;
            }
            .form-group
            {
                position: relative;
                margin: 1em 0 0 0;
            }
            .alert-danger
            {
                top: -1.5em;
                position: relative;
            }
            p.help-block[id*="error"]
            {
                width: 10em;
                color: red !important;
                font-style: normal;
                padding: 0;
                margin: 0 0 1em 0;
                line-height: 1.5;
                font-family: "Roboto", sans-serif;
                font-weight: normal;
                position: absolute;
                left: -12em;
                text-align: right;
                top: 0.85em;
            }
            .required:after
            {
                content: "*";
                position: relative;
                font-family: "Roboto", sans-serif;
                font-weight: normal;
                color: red;
            }
            .errorMsgLbl
            {
                background: red;
                font-family: "Roboto", sans-serif;
                font-weight: normal;
                padding: 0.5em 1em;
                color: #fff;
                margin: 0 0 1em 0;
            }
            .alert-danger .errorMsg
            {
                display: none;
            }
        </style>
    </head>
    <body>
        <div class="form" style="width: 500px; margin: 0 auto; padding: 50px">
            <h1>Simple Form</h1>
            {{ form }}
        </div>
    </body>
    <script type="text/javascript">
        $(function() {
            $('#item-deformField1').addClass('input-field').addClass('col');
            $('#item-deformField2').addClass('input-field').addClass('col');
            $('#item-deformField3').addClass('input-field').addClass('col');
            $('#deformField3').addClass('materialize-textarea');
        });
    </script>
</html>
../../_images/simple_form_with_css.png

Сгенерированная форма с применением CSS стилей

../../_images/simple_form_validation_with_css.png

Валидация формы с применением CSS стилей

Deform и Colander в реальных проектах

Наследование схем

1.inheritance.py - Наследование Colander схем
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import colander
import deform
from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader('templates'))


class AddressSchema(colander.MappingSchema):
    line1 = colander.SchemaNode(colander.String(), title='Address line 1')
    line2 = colander.SchemaNode(colander.String(), title='Address line 2',
                                missing=None)
    line3 = colander.SchemaNode(colander.String(), title='Address line 3',
                                missing=None)
    town = colander.SchemaNode(colander.String(), title='Town')
    postcode = colander.SchemaNode(colander.String(), title='Postcode')


class Business(AddressSchema):
    business_name = colander.SchemaNode(colander.String(),
                                        title='Business Name',
                                        insert_before='line1')


def inheritance_form(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    from webob import Request
    request = Request(environ)

    form = deform.Form(Business(), buttons=('submit',))
    template = env.get_template('simple_with_css.html')
    if request.POST:
        submitted = request.POST.items()
        try:
            form.validate(submitted)
        except deform.ValidationFailure as e:
            return template.render(form=e.render())
    return template.render(form=form.render())


if __name__ == '__main__':
    from paste.httpserver import serve

    serve(inheritance_form, host='0.0.0.0', port=8000)
../../_images/inheritance_scheme.png

Наследование Colander схемы

Кастомная валидация

2.custom_validators.py - Кастомная валидация
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import colander
import deform
from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader('templates'))


def month_validator(node, month):
    if month.isdigit():
        int_month = int(month)
        if not 0 < int_month < 13:
            raise colander.Invalid(node,
                                   'Please enter a number between 1 and 12')
    else:
        raise colander.Invalid(node, 'Please enter a number')


class AddressSchema(colander.MappingSchema):
    line1 = colander.SchemaNode(colander.String(), title='Address line 1')
    line2 = colander.SchemaNode(colander.String(), title='Address line 2',
                                missing=None)
    line3 = colander.SchemaNode(colander.String(), title='Address line 3',
                                missing=None)
    town = colander.SchemaNode(colander.String(), title='Town')
    postcode = colander.SchemaNode(colander.String(), title='Postcode')


class Business(AddressSchema):
    business_name = colander.SchemaNode(colander.String(),
                                        title='Business Name',
                                        insert_before='line1')
    start_month = colander.SchemaNode(colander.String(), title='Start month',
                                      validator=month_validator)


def custom_validator_form(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    from webob import Request
    request = Request(environ)

    form = deform.Form(Business(), buttons=('submit',))
    template = env.get_template('simple_with_css.html')
    if request.POST:
        submitted = request.POST.items()
        try:
            form.validate(submitted)
        except deform.ValidationFailure as e:
            return template.render(form=e.render()).encode("utf-8")
    return template.render(form=form.render()).encode("utf-8")


if __name__ == '__main__':
    from paste.httpserver import serve

    serve(custom_validator_form, host='0.0.0.0', port=8000)
../../_images/custom_validator_form.png

Кастомная валидация поля

Отложенная валидация

См.также

3.defered_validators.py - добавление CSRF токена
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import colander
import deform
from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader('templates'))


def get_session(request):
    return request.environ.get('paste.session.factory', lambda: {})()


def get_csrf_token(session):
    if 'csrf' not in session:
        from uuid import uuid4
        session['csrf'] = uuid4().hex
    return session['csrf']


@colander.deferred
def deferred_csrf_default(node, kw):
    request = kw.get('request')
    session = get_session(request)
    csrf_token = get_csrf_token(session)
    return csrf_token


@colander.deferred
def deferred_csrf_validator(node, kw):
    def validate_csrf_token(node, value):
        request = kw.get('request')
        session = get_session(request)
        csrf_token = get_csrf_token(session)
        if value != csrf_token:
            raise colander.Invalid(node, 'Bad CSRF token')

    return validate_csrf_token


class CSRFSchema(colander.Schema):
    csrf = colander.SchemaNode(colander.String(),
                               default=deferred_csrf_default,
                               validator=deferred_csrf_validator,
                               # widget=deform.widget.HiddenWidget(), )
                               )


class Contact(CSRFSchema):
    email = colander.SchemaNode(colander.String(), validator=colander.Email())
    name = colander.SchemaNode(colander.String())
    message = colander.SchemaNode(colander.String(),
                                  widget=deform.widget.TextAreaWidget())


def custom_validator_form(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    from webob import Request
    request = Request(environ)
    session = get_session(request)
    session['csrf'] = get_csrf_token(session)

    schema = Contact().bind(request=request)
    form = deform.Form(schema, buttons=('submit',))
    template = env.get_template('simple_with_css.html')
    if request.POST:
        submitted = request.POST.items()
        try:
            form.validate(submitted)
        except deform.ValidationFailure as e:
            return template.render(form=e.render()).encode("utf-8")
        session.pop('csrf')
        return template.render(form='OK')
    return template.render(form=form.render()).encode("utf-8")


if __name__ == '__main__':
    from paste.httpserver import serve
    from paste.session import SessionMiddleware

    app = SessionMiddleware(custom_validator_form)

    serve(app, host='0.0.0.0', port=8000)
../../_images/defered_validator_form_csrf_token.png

Ключ CSRF

3.defered_validators.py - отложенная валидация
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import colander
import deform
from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader('templates'))


def get_session(request):
    return request.environ.get('paste.session.factory', lambda: {})()


def get_csrf_token(session):
    if 'csrf' not in session:
        from uuid import uuid4
        session['csrf'] = uuid4().hex
    return session['csrf']


@colander.deferred
def deferred_csrf_default(node, kw):
    request = kw.get('request')
    session = get_session(request)
    csrf_token = get_csrf_token(session)
    return csrf_token


@colander.deferred
def deferred_csrf_validator(node, kw):
    def validate_csrf_token(node, value):
        request = kw.get('request')
        session = get_session(request)
        csrf_token = get_csrf_token(session)
        if value != csrf_token:
            raise colander.Invalid(node, 'Bad CSRF token')

    return validate_csrf_token


class CSRFSchema(colander.Schema):
    csrf = colander.SchemaNode(colander.String(),
                               default=deferred_csrf_default,
                               validator=deferred_csrf_validator,
                               # widget=deform.widget.HiddenWidget(), )
                               )


class Contact(CSRFSchema):
    email = colander.SchemaNode(colander.String(), validator=colander.Email())
    name = colander.SchemaNode(colander.String())
    message = colander.SchemaNode(colander.String(),
                                  widget=deform.widget.TextAreaWidget())


def custom_validator_form(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    from webob import Request
    request = Request(environ)
    session = get_session(request)
    session['csrf'] = get_csrf_token(session)

    schema = Contact().bind(request=request)
    form = deform.Form(schema, buttons=('submit',))
    template = env.get_template('simple_with_css.html')
    if request.POST:
        submitted = request.POST.items()
        try:
            form.validate(submitted)
        except deform.ValidationFailure as e:
            return template.render(form=e.render()).encode("utf-8")
        session.pop('csrf')
        return template.render(form='OK')
    return template.render(form=form.render()).encode("utf-8")


if __name__ == '__main__':
    from paste.httpserver import serve
    from paste.session import SessionMiddleware

    app = SessionMiddleware(custom_validator_form)

    serve(app, host='0.0.0.0', port=8000)
../../_images/defered_validator_form_bad_token.png

Ключ CSRF

Переопределение стандартных шаблонов

4.custom_templates.py - переопределение стандартных шаблонов формы
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import os

import colander
import deform
from jinja2 import Environment, FileSystemLoader
from pkg_resources import resource_filename

env = Environment(loader=FileSystemLoader('templates'))


deform_path = os.path.abspath('templates/deform')
deform_templates = resource_filename('deform', 'templates')
print(deform_templates)
print(deform_path)
search_path = (deform_path, deform_templates)
renderer = deform.ZPTRendererFactory(search_path)


class Contact(colander.MappingSchema):
    email = colander.SchemaNode(colander.String(), validator=colander.Email())
    name = colander.SchemaNode(colander.String())
    message = colander.SchemaNode(colander.String(),
                                  widget=deform.widget.TextAreaWidget())


def custom_template_form(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    from webob import Request
    request = Request(environ)

    form = deform.Form(Contact(), buttons=('submit',), renderer=renderer)
    template = env.get_template('simple_with_css.html')
    if request.POST:
        submitted = request.POST.items()
        try:
            form.validate(submitted)
        except deform.ValidationFailure as e:
            return template.render(form=e.render()).encode("utf-8")
    data = {'email': 'jon.staley@fundingoptions.com',
            'name': 'Jon',
            'message': 'Hello World'}
    return template.render(form=form.render(data)).encode("utf-8")


if __name__ == '__main__':
    from paste.httpserver import serve

    serve(custom_template_form, host='0.0.0.0', port=8000)
$ tree templates/deform/
templates/deform/
├── form.pt
└── mapping_item.pt

0 directories, 2 files
../../_images/custom_templates.png

Переопределенный шаблон form.pt

Новые шаблоны на Jinja2

5.custom_jinja2_templates.py - переопределение стандартных шаблонов формы, на свои Jinja2 шаблоны
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import os

import colander
import deform
from jinja2 import Environment, FileSystemLoader

env = Environment(loader=FileSystemLoader('templates'))


def jinja2_renderer(template_name, **kw):
    kw['_'] = str  # Hook for translation string with gettext

    from jinja2 import Template
    deform_jinja_path = os.path.abspath('templates/deform_jinja2')
    jinja2_template = os.path.join(deform_jinja_path,
                                   template_name + '.jinja2')
    template = Template(open(jinja2_template).read())
    return template.render(**kw)


class Contact(colander.MappingSchema):
    email = colander.SchemaNode(colander.String(), validator=colander.Email())
    name = colander.SchemaNode(colander.String())
    message = colander.SchemaNode(colander.String(),
                                  widget=deform.widget.TextAreaWidget())


def custom_template_form(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/html')])
    from webob import Request
    request = Request(environ)

    form = deform.Form(Contact(), buttons=('submit',),
                       renderer=jinja2_renderer)
    template = env.get_template('simple_with_css.html')
    if request.POST:
        submitted = request.POST.items()
        try:
            form.validate(submitted)
        except deform.ValidationFailure as e:
            return template.render(form=e.render()).encode("utf-8")
    data = {'email': 'jon.staley@fundingoptions.com',
            'name': 'Jon',
            'message': 'Hello World'}
    return template.render(form=form.render(data)).encode("utf-8")


if __name__ == '__main__':
    from paste.httpserver import serve

    serve(custom_template_form, host='0.0.0.0', port=8000)
$ tree templates/deform_jinja2/
templates/deform_jinja2/
├── autocomplete_input.jinja2
├── checkbox_choice.jinja2
├── checkbox.jinja2
├── checked_input.jinja2
├── checked_password.jinja2
├── dateinput.jinja2
├── dateparts.jinja2
├── datetimeinput.jinja2
├── file_upload.jinja2
├── form.jinja2
├── hidden.jinja2
├── mapping_item.jinja2
├── mapping.jinja2
├── moneyinput.jinja2
├── password.jinja2
├── radio_choice.jinja2
├── readonly
│   ├── checkbox_choice.jinja2
│   ├── checkbox.jinja2
│   ├── checked_input.jinja2
│   ├── checked_password.jinja2
│   ├── dateparts.jinja2
│   ├── file_upload.jinja2
│   ├── form.jinja2
│   ├── mapping_item.jinja2
│   ├── mapping.jinja2
│   ├── password.jinja2
│   ├── radio_choice.jinja2
│   ├── richtext.jinja2
│   ├── select.jinja2
│   ├── sequence_item.jinja2
│   ├── sequence.jinja2
│   ├── textarea.jinja2
│   └── textinput.jinja2
├── richtext.jinja2
├── select.jinja2
├── sequence_item.jinja2
├── sequence.jinja2
├── textarea.jinja2
└── textinput.jinja2

1 directory, 39 files
Стандартный шаблон textarea.pt из Deform
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<textarea tal:define="rows rows|field.widget.rows;
                      cols cols|field.widget.cols;
                      css_class css_class|field.widget.css_class;
                      oid oid|field.oid;
                      name name|field.name;
                      style style|field.widget.style"
          tal:attributes="rows rows;
                          cols cols;
                          class string: form-control ${css_class or ''};
                          style style"
          id="${oid}"
          name="${name}">${cstruct}</textarea>
templates/deform_jinja2/textarea.jinja2 - Переопределенный нами шаблон textarea на Jinja2
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<textarea
        style="background: green;color:white;"
        {% if field.widget.rows %}
            rows="{{ field.widget.rows }}"
        {% endif %}
        {% if field.widget.cols %}
            cols="{{ field.widget.cols }}"
        {% endif %}
        {% if field.widget.css_class %}
            class="{{ field.widget.css_class }}"
        {% endif %}
            id="{{ field.oid }}"
            name="{{ field.name }}"
        {% if field.description %}
            placeholder="{{ _(field.description) }}"
        {% endif %}>{{ cstruct }}</textarea>
../../_images/custom_jinja2_templates.png

Переопределенный шаблон textarea.jinja2

Блог

forms.py - Форма для создания статьи
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import deform
import colander

from common import get_csrf_token, get_session


@colander.deferred
def deferred_csrf_default(node, kw):
    request = kw.get('request')
    session = get_session(request)
    csrf_token = get_csrf_token(session)
    return csrf_token


@colander.deferred
def deferred_csrf_validator(node, kw):
    def validate_csrf_token(node, value):
        request = kw.get('request')
        session = get_session(request)
        csrf_token = get_csrf_token(session)
        if value != csrf_token:
            raise colander.Invalid(node, 'Bad CSRF token')

    return validate_csrf_token


class CSRFSchema(colander.Schema):
    csrf = colander.SchemaNode(colander.String(),
                               default=deferred_csrf_default,
                               validator=deferred_csrf_validator,
                               widget=deform.widget.HiddenWidget(), )


class CreateArticle(CSRFSchema):
    title = colander.SchemaNode(colander.String())
    content = colander.SchemaNode(
        colander.String(),
        widget=deform.widget.TextAreaWidget(
            css_class="blog-form-field__textarea")
    )
common.py - Функции get_session и get_csrf_token
1
2
3
4
5
6
7
8
9
def get_session(request):
    return request.environ.get('paste.session.factory', lambda: {})()


def get_csrf_token(session):
    if 'csrf' not in session:
        from uuid import uuid4
        session['csrf'] = uuid4().hex
    return session['csrf']
__init__.py - Добавляем механизм сессии в наше WSGI-приложение
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.

"""
Simple blog
"""
from paste.auth.basic import AuthBasicHandler

import selector
from views import BlogCreate, BlogDelete, BlogIndex, BlogRead, BlogUpdate


def authfunc(environ, username, password):
    return username == 'admin' and password == '123'


def make_wsgi_app():
    # BasicAuth applications
    create = AuthBasicHandler(BlogCreate, 'www', authfunc)
    update = AuthBasicHandler(BlogUpdate, 'www', authfunc)
    delete = AuthBasicHandler(BlogDelete, 'www', authfunc)

    # URL dispatching middleware
    dispatch = selector.Selector()
    dispatch.add('/', GET=BlogIndex)
    dispatch.prefix = '/article'
    dispatch.add('/add', GET=create, POST=create)
    dispatch.add('/{id:digits}', GET=BlogRead)
    dispatch.add('/{id:digits}/edit', GET=update, POST=update)
    dispatch.add('/{id:digits}/delete', GET=delete)

    # Static files
    from paste.urlparser import StaticURLParser
    static_app = StaticURLParser("static/")

    from paste import urlmap
    mapping = urlmap.URLMap()
    mapping['/static'] = static_app

    from paste.cascade import Cascade
    app = Cascade([mapping, dispatch])

    return app

if __name__ == '__main__':
    from paste.httpserver import serve
    from paste.session import SessionMiddleware

    app = make_wsgi_app()
    app = SessionMiddleware(app)
    serve(app, host='0.0.0.0', port=8000)
views.py - Генерация форм в представлениях при помощи Deform
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
import deform
from jinja2 import Environment, FileSystemLoader
from webob import Request, Response

from common import get_csrf_token, get_session
from models import ARTICLES

env = Environment(loader=FileSystemLoader('templates'))


def wsgify(view):
    def wrapped(environ, start_response):
        request = Request(environ)
        app = view(request).response()
        return app(environ, start_response)
    return wrapped


class BaseArticle(object):

    def __init__(self, request):
        self.request = request
        article_id = self.request.environ['wsgiorg.routing_args'][1]['id']
        (self.index,
         self.article) = next(((i, art) for i, art in enumerate(ARTICLES)
                               if art['id'] == int(article_id)),
                              (None, None))


class BaseArticleForm(object):

    def get_form(self):
        from forms import CreateArticle
        self.session = get_session(self.request)
        self.session['csrf'] = get_csrf_token(self.session)
        schema = CreateArticle().bind(request=self.request)
        submit = deform.Button(name='submit',
                               css_class='blog-form__button')
        self.form = deform.Form(schema, buttons=(submit,))
        return self.form


@wsgify
class BlogIndex(object):

    def __init__(self, request):
        self.page = request.GET.get('page', '1')
        from paginate import Page
        self.paged_articles = Page(
            ARTICLES,
            page=self.page,
            items_per_page=8,
        )

    def response(self):
        return Response(env.get_template('index.html')
                        .render(articles=self.paged_articles))


@wsgify
class BlogCreate(BaseArticleForm):

    def __init__(self, request):
        self.request = request

    def response(self):
        if self.request.method == 'POST':
            submitted = self.request.POST.items()
            try:
                self.get_form().validate(submitted)
            except deform.ValidationFailure as e:
                return Response(
                    env.get_template('create.html').render(form=e.render()))
            max_id = max([art['id'] for art in ARTICLES])
            ARTICLES.append(
                {'id': max_id+1,
                 'title': self.request.POST['title'],
                 'content': self.request.POST['content']
                 }
            )
            self.session = get_session(self.request).pop('csrf')
            return Response(status=302, location='/')
        return Response(env.get_template('create.html')
                        .render(form=self.get_form().render()))


@wsgify
class BlogRead(BaseArticle):

    def response(self):
        if not self.article:
            return Response(status=404)
        return Response(env.get_template('read.html')
                        .render(article=self.article))


@wsgify
class BlogUpdate(BaseArticle, BaseArticleForm):

    def response(self):
        if self.request.method == 'POST':
            submitted = self.request.POST.items()
            try:
                self.get_form().validate(submitted)
            except deform.ValidationFailure as e:
                return Response(
                    env.get_template('create.html').render(form=e.render()))
            self.article['title'] = self.request.POST['title']
            self.article['content'] = self.request.POST['content']
            self.session = get_session(self.request).pop('csrf')
            return Response(status=302, location='/')
        return Response(
            env.get_template('create.html')
            .render(form=self.get_form().render(self.article)))


@wsgify
class BlogDelete(BaseArticle):

    def response(self):
        ARTICLES.pop(self.index)
        return Response(status=302, location='/')
create.html - форма генерируется автоматически
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
{% extends "base.html" %}

{% block title %}Create{% endblock %}

{% block content %}
    <div class="blog__title">
        <a href="/" class="blog__title-link">Simple Blog</a>
        <span class="blog__title-text">Edit</span>
    </div>
    <form action="" method="POST" class="blog-form">
        {{ form }}
    </form>
{% endblock %}

Теперь форма имеет валидацию, защиту от CSRF атак и генерируется автоматически при помощи Deform.

../../_images/blog_validation.png

Валидация формы

Previous: WebOb Next: Кэширование