18:变形的形式和验证

模式驱动的、带有验证的自动生成的表单。

背景

现代Web应用程序广泛处理表单。然而,开发人员对于框架应该如何帮助他们使用表单有着广泛的哲学。因此,Pyramid并不直接捆绑一个特定的表单库。相反,Pyramid中有各种各样的表单库,很容易使用。

Deform 就是这样一个类库。在这一步中,我们为我们的形式引入deform。这也给了我们 Colander 用于模式和验证。

目标

  • 使用colander(变形的伴生)制作一个模式。

  • 使用deform创建一个表单,并更改我们的视图以处理验证。

步骤

  1. 首先我们复制 view_classes 步骤:

    cd ..; cp -r view_classes forms; cd forms
    
  2. 让我们编辑 forms/setup.py 要声明对deform的依赖,而deform又将colander作为依赖项拉入:

     1from setuptools import setup
     2
     3# List of dependencies installed via `pip install -e .`
     4# by virtue of the Setuptools `install_requires` value below.
     5requires = [
     6    'deform',
     7    'pyramid',
     8    'pyramid_chameleon',
     9    'waitress',
    10]
    11
    12# List of dependencies installed via `pip install -e ".[dev]"`
    13# by virtue of the Setuptools `extras_require` value in the Python
    14# dictionary below.
    15dev_requires = [
    16    'pyramid_debugtoolbar',
    17    'pytest',
    18    'webtest',
    19]
    20
    21setup(
    22    name='tutorial',
    23    install_requires=requires,
    24    extras_require={
    25        'dev': dev_requires,
    26    },
    27    entry_points={
    28        'paste.app_factory': [
    29            'main = tutorial:main'
    30        ],
    31    },
    32)
    
  3. 我们现在可以在开发模式下安装我们的项目:

    $VENV/bin/pip install -e .
    
  4. 在中注册静态视图 forms/tutorial/__init__.py 对于deform的css、javascript等,以及我们的演示wiki页面的视图:

     1from pyramid.config import Configurator
     2
     3
     4def main(global_config, **settings):
     5    config = Configurator(settings=settings)
     6    config.include('pyramid_chameleon')
     7    config.add_route('wiki_view', '/')
     8    config.add_route('wikipage_add', '/add')
     9    config.add_route('wikipage_view', '/{uid}')
    10    config.add_route('wikipage_edit', '/{uid}/edit')
    11    config.add_static_view('deform_static', 'deform:static/')
    12    config.scan('.views')
    13    return config.make_wsgi_app()
    
  5. 在中实现新视图、表单架构和一些虚拟数据。 forms/tutorial/views.py

     1import colander
     2import deform.widget
     3
     4from pyramid.httpexceptions import HTTPFound
     5from pyramid.view import view_config
     6
     7pages = {
     8    '100': dict(uid='100', title='Page 100', body='<em>100</em>'),
     9    '101': dict(uid='101', title='Page 101', body='<em>101</em>'),
    10    '102': dict(uid='102', title='Page 102', body='<em>102</em>')
    11}
    12
    13class WikiPage(colander.MappingSchema):
    14    title = colander.SchemaNode(colander.String())
    15    body = colander.SchemaNode(
    16        colander.String(),
    17        widget=deform.widget.RichTextWidget()
    18    )
    19
    20
    21class WikiViews:
    22    def __init__(self, request):
    23        self.request = request
    24
    25    @property
    26    def wiki_form(self):
    27        schema = WikiPage()
    28        return deform.Form(schema, buttons=('submit',))
    29
    30    @property
    31    def reqts(self):
    32        return self.wiki_form.get_widget_resources()
    33
    34    @view_config(route_name='wiki_view', renderer='wiki_view.pt')
    35    def wiki_view(self):
    36        return dict(pages=pages.values())
    37
    38    @view_config(route_name='wikipage_add',
    39                 renderer='wikipage_addedit.pt')
    40    def wikipage_add(self):
    41        form = self.wiki_form.render()
    42
    43        if 'submit' in self.request.params:
    44            controls = self.request.POST.items()
    45            try:
    46                appstruct = self.wiki_form.validate(controls)
    47            except deform.ValidationFailure as e:
    48                # Form is NOT valid
    49                return dict(form=e.render())
    50
    51            # Form is valid, make a new identifier and add to list
    52            last_uid = int(sorted(pages.keys())[-1])
    53            new_uid = str(last_uid + 1)
    54            pages[new_uid] = dict(
    55                uid=new_uid, title=appstruct['title'],
    56                body=appstruct['body']
    57            )
    58
    59            # Now visit new page
    60            url = self.request.route_url('wikipage_view', uid=new_uid)
    61            return HTTPFound(url)
    62
    63        return dict(form=form)
    64
    65    @view_config(route_name='wikipage_view', renderer='wikipage_view.pt')
    66    def wikipage_view(self):
    67        uid = self.request.matchdict['uid']
    68        page = pages[uid]
    69        return dict(page=page)
    70
    71    @view_config(route_name='wikipage_edit',
    72                 renderer='wikipage_addedit.pt')
    73    def wikipage_edit(self):
    74        uid = self.request.matchdict['uid']
    75        page = pages[uid]
    76
    77        wiki_form = self.wiki_form
    78
    79        if 'submit' in self.request.params:
    80            controls = self.request.POST.items()
    81            try:
    82                appstruct = wiki_form.validate(controls)
    83            except deform.ValidationFailure as e:
    84                return dict(page=page, form=e.render())
    85
    86            # Change the content and redirect to the view
    87            page['title'] = appstruct['title']
    88            page['body'] = appstruct['body']
    89
    90            url = self.request.route_url('wikipage_view',
    91                                         uid=page['uid'])
    92            return HTTPFound(url)
    93
    94        form = wiki_form.render(page)
    95
    96        return dict(page=page, form=form)
    
  6. 中“wiki”顶部的模板 forms/tutorial/wiki_view.pt

     1<!DOCTYPE html>
     2<html lang="en">
     3<head>
     4    <title>Wiki: View</title>
     5</head>
     6<body>
     7<h1>Wiki</h1>
     8
     9<a href="${request.route_url('wikipage_add')}">Add
    10    WikiPage</a>
    11<ul>
    12    <li tal:repeat="page pages">
    13        <a href="${request.route_url('wikipage_view', uid=page.uid)}">
    14                ${page.title}
    15        </a>
    16    </li>
    17</ul>
    18</body>
    19</html>
    
  7. 用于添加/编辑的另一个模板 forms/tutorial/wikipage_addedit.pt

     1<!DOCTYPE html>
     2<html lang="en">
     3<head>
     4    <title>WikiPage: Add/Edit</title>
     5    <link rel="stylesheet"
     6          href="${request.static_url('deform:static/css/bootstrap.min.css')}"
     7          type="text/css" media="screen" charset="utf-8"/>
     8    <link rel="stylesheet"
     9          href="${request.static_url('deform:static/css/form.css')}"
    10          type="text/css"/>
    11    <tal:block tal:repeat="reqt view.reqts['css']">
    12        <link rel="stylesheet" type="text/css"
    13              href="${request.static_url(reqt)}"/>
    14    </tal:block>
    15    <script src="${request.static_url('deform:static/scripts/jquery-2.0.3.min.js')}"
    16            type="text/javascript"></script>
    17    <script src="${request.static_url('deform:static/scripts/bootstrap.min.js')}"
    18            type="text/javascript"></script>
    19
    20    <tal:block tal:repeat="reqt view.reqts['js']">
    21        <script src="${request.static_url(reqt)}"
    22                type="text/javascript"></script>
    23    </tal:block>
    24</head>
    25<body>
    26<h1>Wiki</h1>
    27
    28<p>${structure: form}</p>
    29<script type="text/javascript">
    30    deform.load()
    31</script>
    32</body>
    33</html>
    
  8. 在处添加模板 forms/tutorial/wikipage_view.pt 查看wiki页面:

     1<!DOCTYPE html>
     2<html lang="en">
     3<head>
     4    <title>WikiPage: View</title>
     5</head>
     6<body>
     7<a href="${request.route_url('wiki_view')}">
     8    Up
     9</a> |
    10<a href="${request.route_url('wikipage_edit', uid=page.uid)}">
    11    Edit
    12</a>
    13
    14<h1>${page.title}</h1>
    15<p>${structure: page.body}</p>
    16</body>
    17</html>
    
  9. 我们的测试 forms/tutorial/tests.py 不要运行,我们来修改它们:

     1import unittest
     2
     3from pyramid import testing
     4
     5
     6class TutorialViewTests(unittest.TestCase):
     7    def setUp(self):
     8        self.config = testing.setUp()
     9
    10    def tearDown(self):
    11        testing.tearDown()
    12
    13    def test_home(self):
    14        from .views import WikiViews
    15
    16        request = testing.DummyRequest()
    17        inst = WikiViews(request)
    18        response = inst.wiki_view()
    19        self.assertEqual(len(response['pages']), 3)
    20
    21
    22class TutorialFunctionalTests(unittest.TestCase):
    23    def setUp(self):
    24        from tutorial import main
    25
    26        app = main({})
    27        from webtest import TestApp
    28
    29        self.testapp = TestApp(app)
    30
    31    def tearDown(self):
    32        testing.tearDown()
    33
    34    def test_home(self):
    35        res = self.testapp.get('/', status=200)
    36        self.assertIn(b'<title>Wiki: View</title>', res.body)
    37
    38    def test_add_page(self):
    39        res = self.testapp.get('/add', status=200)
    40        self.assertIn(b'<h1>Wiki</h1>', res.body)
    41
    42    def test_edit_page(self):
    43        res = self.testapp.get('/101/edit', status=200)
    44        self.assertIn(b'<h1>Wiki</h1>', res.body)
    45
    46    def test_post_wiki(self):
    47        self.testapp.post('/add', {
    48            "title": "New Title",
    49            "body": "<p>New Body</p>",
    50            "submit": "submit"
    51        }, status=302)
    52
    53        res = self.testapp.get('/103', status=200)
    54        self.assertIn(b'<h1>New Title</h1>', res.body)
    55        self.assertIn(b'<p>New Body</p>', res.body)
    56
    57    def test_edit_wiki(self):
    58        self.testapp.post('/102/edit', {
    59            "title": "New Title",
    60            "body": "<p>New Body</p>",
    61            "submit": "submit"
    62        }, status=302)
    63
    64        res = self.testapp.get('/102', status=200)
    65        self.assertIn(b'<h1>New Title</h1>', res.body)
    66        self.assertIn(b'<p>New Body</p>', res.body)
    
  10. 运行测试:

    $VENV/bin/pytest tutorial/tests.py -q
    ..
    6 passed in 0.81 seconds
    
  11. 运行 Pyramid 应用程序时使用:

    $VENV/bin/pserve development.ini --reload
    
  12. 在浏览器中打开http://localhost:6543/。

分析

此步骤有助于说明Static Assets的资产规范的实用性。我们有一个名为deform的外部包,其中包含需要发布的Static Assets。我们不需要知道它在磁盘上的位置。我们指向包,然后指向包内的路径。

我们只需要打个电话给 add_static_view 使该目录在URL上可用。对于特定于Pyramid的包,Pyramid提供了一个设施 (config.include() )这甚至使包装的消费者不需要这样做。(变形不特定于棱锥。)

我们的表单有丰富的小部件,它们需要刚才提到的静态CSS和JavaScript。变形有 resource registry 它允许小部件指定需要哪些JavaScript和CSS。我们的 wikipage_addedit.pt 模板显示了我们如何迭代这些数据以生成包含所需资源的标记。

我们的添加和编辑视图使用一个名为 self-posting forms . 也就是说,相同的URL用于 GET 用于 POST 形式。无论您是第一次走向它还是单击了一个按钮,路由、视图和模板都是相同的URL。

在我们的视野里 if 'submit' in self.request.params: 看看这个表格是不是 POST 用户单击特定按钮的位置 <input name="submit"> .

然后表单控制器遵循典型模式:

  • 如果你正在做 GET ,跳过并返回表单。

  • 如果你正在做 POST ,验证表单内容。

  • 如果表单无效,则通过使用提供的 POST 数据。

  • 如果验证成功,请执行一些操作并通过 HTTPFound .

本质上,我们是在编写自己的表单控制器。其他基于Pyramid的系统,包括 pyramid_deform ,提供一个以表单为中心的视图类,它可以自动执行大部分分支和路由。

额外credit

  1. 尝试一个按钮,该按钮将转到特定wiki页面的“删除”视图。