18: Deformでのフォームとバリテーション (18: Forms and Validation with Deform)¶
スキーマ主導によるバリテーションつきのフォームが自動生成されます。
背景(Background)¶
モダンなWebアプリケーションはフォームを広く扱っています。しかしながら開発者はフレームワークがフォームについてどのように手助けすべきかについて幅広い哲学を持っています。そのためPyramidは特定のフォームライブラリを直接はバンドルしません。代わりにPyramidで使いやすい様々なフォームライブラリがあります。
Deform はこのようなライブラリの1つです。このステップではDeformによるフォームの作成を紹介します。これはスキーマとバリテーションのための Colander も提供します。
目的(Objectives)¶
- Deformの仲間であるColanderを使用してスキーマを作成します。
- Deformを使用してフォームを作成してビューを変更してをバリテーションをバンドルします。
手順(Steps)¶
最初に
view_classes
での手順の結果をコピーします。$ cd ..; cp -r view_classes forms; cd forms
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 """, )
プロジェクトに開発者モードでインストールします:
$ $VENV/bin/pip install -e .
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()
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)
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>
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>
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>
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)
テストを実行します:
$ $VENV/bin/py.test tutorial/tests.py -q .. 2 passed in 0.45 seconds
Pyramidアプリケーションを実行します:
$ $VENV/bin/pserve development.ini --reload
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)¶
- 特定のwikiページの削除ビューに移動するボタンを試してみてください。