18: Deformでのフォームとバリテーション (18: Forms and Validation with Deform)

スキーマ主導によるバリテーションつきのフォームが自動生成されます。

背景(Background)

モダンなWebアプリケーションはフォームを広く扱っています。しかしながら開発者はフレームワークがフォームについてどのように手助けすべきかについて幅広い哲学を持っています。そのためPyramidは特定のフォームライブラリを直接はバンドルしません。代わりにPyramidで使いやすい様々なフォームライブラリがあります。

Deform はこのようなライブラリの1つです。このステップではDeformによるフォームの作成を紹介します。これはスキーマとバリテーションのための Colander も提供します。

目的(Objectives)

  • Deformの仲間であるColanderを使用してスキーマを作成します。
  • Deformを使用してフォームを作成してビューを変更してをバリテーションをバンドルします。

手順(Steps)

  1. 最初に view_classes での手順の結果をコピーします。

    $ cd ..; cp -r view_classes forms; cd forms
    
  2. forms/setup.py を編集してDeformへの依存性を宣言しましょう(Colanderを依存関係として取得します):

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    from setuptools import setup
    
    requires = [
        'deform',
        'pyramid',
        'pyramid_chameleon',
        'waitress',
    ]
    
    setup(name='tutorial',
          install_requires=requires,
          entry_points="""\
          [paste.app_factory]
          main = tutorial:main
          """,
    )
    
  3. プロジェクトに開発者モードでインストールします:

    $ $VENV/bin/pip install -e .
    
  4. DeformのCSS、JavaScriptなどのための forms/tutorial/__init__.py の静的ビューと、デモwikiページのビューを登録します。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    from pyramid.config import Configurator
    
    
    def main(global_config, **settings):
        config = Configurator(settings=settings)
        config.include('pyramid_chameleon')
        config.add_route('wiki_view', '/')
        config.add_route('wikipage_add', '/add')
        config.add_route('wikipage_view', '/{uid}')
        config.add_route('wikipage_edit', '/{uid}/edit')
        config.add_static_view('deform_static', 'deform:static/')
        config.scan('.views')
        return config.make_wsgi_app()
    
  5. forms/tutorial/views.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
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    import colander
    import deform.widget
    
    from pyramid.httpexceptions import HTTPFound
    from pyramid.view import view_config
    
    pages = {
        '100': dict(uid='100', title='Page 100', body='<em>100</em>'),
        '101': dict(uid='101', title='Page 101', body='<em>101</em>'),
        '102': dict(uid='102', title='Page 102', body='<em>102</em>')
    }
    
    class WikiPage(colander.MappingSchema):
        title = colander.SchemaNode(colander.String())
        body = colander.SchemaNode(
            colander.String(),
            widget=deform.widget.RichTextWidget()
        )
    
    
    class WikiViews(object):
        def __init__(self, request):
            self.request = request
    
        @property
        def wiki_form(self):
            schema = WikiPage()
            return deform.Form(schema, buttons=('submit',))
    
        @property
        def reqts(self):
            return self.wiki_form.get_widget_resources()
    
        @view_config(route_name='wiki_view', renderer='wiki_view.pt')
        def wiki_view(self):
            return dict(pages=pages.values())
    
        @view_config(route_name='wikipage_add',
                     renderer='wikipage_addedit.pt')
        def wikipage_add(self):
            form = self.wiki_form.render()
    
            if 'submit' in self.request.params:
                controls = self.request.POST.items()
                try:
                    appstruct = self.wiki_form.validate(controls)
                except deform.ValidationFailure as e:
                    # Form is NOT valid
                    return dict(form=e.render())
    
                # Form is valid, make a new identifier and add to list
                last_uid = int(sorted(pages.keys())[-1])
                new_uid = str(last_uid + 1)
                pages[new_uid] = dict(
                    uid=new_uid, title=appstruct['title'],
                    body=appstruct['body']
                )
    
                # Now visit new page
                url = self.request.route_url('wikipage_view', uid=new_uid)
                return HTTPFound(url)
    
            return dict(form=form)
    
        @view_config(route_name='wikipage_view', renderer='wikipage_view.pt')
        def wikipage_view(self):
            uid = self.request.matchdict['uid']
            page = pages[uid]
            return dict(page=page)
    
        @view_config(route_name='wikipage_edit',
                     renderer='wikipage_addedit.pt')
        def wikipage_edit(self):
            uid = self.request.matchdict['uid']
            page = pages[uid]
    
            wiki_form = self.wiki_form
    
            if 'submit' in self.request.params:
                controls = self.request.POST.items()
                try:
                    appstruct = wiki_form.validate(controls)
                except deform.ValidationFailure as e:
                    return dict(page=page, form=e.render())
    
                # Change the content and redirect to the view
                page['title'] = appstruct['title']
                page['body'] = appstruct['body']
    
                url = self.request.route_url('wikipage_view',
                                             uid=page['uid'])
                return HTTPFound(url)
    
            form = wiki_form.render(page)
    
            return dict(page=page, form=form)
    
  6. forms/tutorial/wiki_view.pt の 「wiki」のトップのテンプレート:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>Wiki: View</title>
    </head>
    <body>
    <h1>Wiki</h1>
    
    <a href="${request.route_url('wikipage_add')}">Add
        WikiPage</a>
    <ul>
        <li tal:repeat="page pages">
            <a href="${request.route_url('wikipage_view', uid=page.uid)}">
                    ${page.title}
            </a>
        </li>
    </ul>
    </body>
    </html>
    
  7. forms/tutorial/wikipage_addedit.pt に追加/編集するための別のテンプレート:

     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
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>WikiPage: Add/Edit</title>
        <link rel="stylesheet"
              href="${request.static_url('deform:static/css/bootstrap.min.css')}"
              type="text/css" media="screen" charset="utf-8"/>
        <link rel="stylesheet"
              href="${request.static_url('deform:static/css/form.css')}"
              type="text/css"/>
        <tal:block tal:repeat="reqt view.reqts['css']">
            <link rel="stylesheet" type="text/css"
                  href="${request.static_url(reqt)}"/>
        </tal:block>
        <script src="${request.static_url('deform:static/scripts/jquery-2.0.3.min.js')}"
                type="text/javascript"></script>
        <script src="${request.static_url('deform:static/scripts/bootstrap.min.js')}"
                type="text/javascript"></script>
    
        <tal:block tal:repeat="reqt view.reqts['js']">
            <script src="${request.static_url(reqt)}"
                    type="text/javascript"></script>
        </tal:block>
    </head>
    <body>
    <h1>Wiki</h1>
    
    <p>${structure: form}</p>
    <script type="text/javascript">
        deform.load()
    </script>
    </body>
    </html>
    
  8. wikiページを表示するためにテンプレート forms/tutorial/wikipage_view.pt を追加します:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>WikiPage: View</title>
    </head>
    <body>
    <a href="${request.route_url('wiki_view')}">
        Up
    </a> |
    <a href="${request.route_url('wikipage_edit', uid=page.uid)}">
        Edit
    </a>
    
    <h1>${page.title}</h1>
    <p>${structure: page.body}</p>
    </body>
    </html>
    
  9. forms/tutorial/tests.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
    import unittest
    
    from pyramid import testing
    
    
    class TutorialViewTests(unittest.TestCase):
        def setUp(self):
            self.config = testing.setUp()
    
        def tearDown(self):
            testing.tearDown()
    
        def test_home(self):
            from .views import WikiViews
    
            request = testing.DummyRequest()
            inst = WikiViews(request)
            response = inst.wiki_view()
            self.assertEqual(len(response['pages']), 3)
    
    
    class TutorialFunctionalTests(unittest.TestCase):
        def setUp(self):
            from tutorial import main
    
            app = main({})
            from webtest import TestApp
    
            self.testapp = TestApp(app)
    
        def tearDown(self):
            testing.tearDown()
    
        def test_home(self):
            res = self.testapp.get('/', status=200)
            self.assertIn(b'<title>Wiki: View</title>', res.body)
    
  10. テストを実行します:

    $ $VENV/bin/py.test tutorial/tests.py -q
    ..
    2 passed in 0.45 seconds
    
  11. Pyramidアプリケーションを実行します:

    $ $VENV/bin/pserve development.ini --reload
    
  12. http://localhost:6543/ をブランチで開きます。

分析(Analysis)

この手順は静的アセットのアセットの仕様の有用性を示すのに役立ちます。公開する必要のある静的アセットを持つDeformという外部パッケージがあります。ディスク上のどこにあるのかを知る必要はありません。パッケージを指してパッケージ内のパスを指します。

ディレクトリをURLで利用できるようにするには、 add_static_view の呼び出しをインクルードします。 Pyramid固有のパッケージの場合はPyramidはパッケージのコンシューマにとって不要なものにするファシリティ (config.include()) を提供します。 (DeformはPyramid特有のものではありません。)

フォームにはちょうど言及した静的CSSとJavaScriptが必要な豊富なウィジェットがあります。 Deformには resource registry がありウィジェットが必要なJavaScriptとCSSを指定できます。 wikipage_addedit.pt テンプレートは必要なリソースを含むマークアップを生成するために、データがどのように反復処理をおこなったかを示しています。

追加と編集のビューは、self-posting forms 呼ばれるパターンを使用します。フォームを POST するのに使用されているのと同じURLを使用してフォームを GET します。ルート、ビュー、およびテンプレートは、初回でもボタンをクリックした場合も、同じURLです

ビューの内部では、if 'submit' in self.request.params: を実行すると、このフォームが特定のボタン <input name="submit"> をクリックした POST かどうかを確認します。

フォームコントローラは、典型的なパターンに従います:

  • GET を行っている場合は、スキップしてフォームを返すだけです。
  • POST を実行している場合は、フォームの内容を検証します。
  • フォームが無効な場合は、指定された POST データを使用してフォームを再レンダリングしてください。
  • 検証が成功した場合は、何らかのアクションを実行し、HTTPFound 経由でリダイレクトを発行します。

本質的に独自のフォームコントローラを作成しています。 pyramid_deform を含む他のPyramidベースのシステムは分岐とルーティングの多くを自動化するフォーム中心のビュークラスを提供します。

エクストラクレジット(Extra credit)

  1. 特定のwikiページの削除ビューに移動するボタンを試してみてください。