15: View Classesの詳細(15: More With View Classes)

ビューをクラスにグループ化し、構成、状態、およびロジックを共有します。

背景(Background)

より挑戦的なWebアプリケーションの構築を支援するという使命の一環として、Pyramidではビューやビュークラスにさらに多くの機能を提供しています。

PyramidのドキュメントではビューをPythonの「呼び出し可能」として説明しています。この呼び出し可能関数は、関数、aを持つオブジェクト __call__ 、またはPythonクラスです。この最後のケースで``@view_config`` はクラスのメソッドを装飾してして、 configurator をビューとしてクラスメソッドを登録できます。

当初はビューは単純で自由な関数でした。多くの場合、同じデータを表示または操作するさまざまな方法や複数の操作を処理するREST APIなど、さまざまなビューが関連しています。これらをまとめて view class としてまとめると意味があります。

  • グループビュー。
  • いくつかの繰り返しのデフォルトを一元化する。
  • ステートやヘルパーを共有します。

Pyramidビューにはリクエストメソッド、フォームパラメータなどの要素に基づいて、リクエストに一致するビューを決定する view predicates があります。これらのpredicatesは多くの柔軟性の軸を提供します。

次の4つの操作の簡単な例を示します。フォームにつながるホームページを表示して、変更を保存して削除ボタンを押します。

目的(Objectives)

  • 関連するビューをビュークラスにグループ化します。
  • クラスレベル @view_defaults で構成を一元化します。
  • リクエストデータに基づいて、1つのルート/URLを複数のビューにディスパッチします。
  • ビュークラスをかいしてビューとテンプレート間の状態とロジックを共有します。

手順(Steps)

  1. 最初に前の手順の結果をコピーします:

    $ cd ..; cp -r templating more_view_classes; cd more_view_classes
    $ $VENV/bin/pip install -e .
    
  2. more_view_classes/tutorial/__init__.py のルートにはいくつかの置換パターンが必要です:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    from pyramid.config import Configurator
    
    
    def main(global_config, **settings):
        config = Configurator(settings=settings)
        config.include('pyramid_chameleon')
        config.add_route('home', '/')
        config.add_route('hello', '/howdy/{first}/{last}')
        config.scan('.views')
        return config.make_wsgi_app()
    
  3. more_view_classes/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
    from pyramid.view import (
        view_config,
        view_defaults
        )
    
    
    @view_defaults(route_name='hello')
    class TutorialViews(object):
        def __init__(self, request):
            self.request = request
            self.view_name = 'TutorialViews'
    
        @property
        def full_name(self):
            first = self.request.matchdict['first']
            last = self.request.matchdict['last']
            return first + ' ' + last
    
        @view_config(route_name='home', renderer='home.pt')
        def home(self):
            return {'page_title': 'Home View'}
    
        # Retrieving /howdy/first/last the first time
        @view_config(renderer='hello.pt')
        def hello(self):
            return {'page_title': 'Hello View'}
    
        # Posting to /howdy/first/last via the "Edit" submit button
        @view_config(request_method='POST', renderer='edit.pt')
        def edit(self):
            new_name = self.request.params['new_name']
            return {'page_title': 'Edit View', 'new_name': new_name}
    
        # Posting to /howdy/first/last via the "Delete" submit button
        @view_config(request_method='POST', request_param='form.delete',
                     renderer='delete.pt')
        def delete(self):
            print ('Deleted')
            return {'page_title': 'Delete View'}
    
  4. メインビューにはテンプレート more_view_classes/tutorial/home.pt が必要です:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>Quick Tutorial: ${view.view_name} - ${page_title}</title>
    </head>
    <body>
    <h1>${view.view_name} - ${page_title}</h1>
    
    <p>Go to the <a href="${request.route_url('hello', first='jane',
            last='doe')}">form</a>.</p>
    </body>
    </html>
    
  5. more_view_classes/tutorial/hello.pt の前のセクションの他のビューについては:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>Quick Tutorial: ${view.view_name} - ${page_title}</title>
    </head>
    <body>
    <h1>${view.view_name} - ${page_title}</h1>
    <p>Welcome, ${view.full_name}</p>
    <form method="POST"
          action="${request.current_route_url()}">
        <input name="new_name"/>
        <input type="submit" name="form.edit" value="Save"/>
        <input type="submit" name="form.delete" value="Delete"/>
    </form>
    </body>
    </html>
    
  6. 編集ビューもあります。このビューにはテンプレート more_view_classes/tutorial/edit.pt が必要です:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>Quick Tutorial: ${view.view_name} - ${page_title}</title>
    </head>
    <body>
    <h1>${view.view_name} - ${page_title}</h1>
    <p>You submitted <code>${new_name}</code></p>
    </body>
    </html>
    
  7. 最後に、ビューのテンプレート more_view_classes/tutorial/delete.pt を削除します:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <title>Quick Tutorial: ${page_title}</title>
    </head>
    <body>
    <h1>${view.view_name} - ${page_title}</h1>
    </body>
    </html>
    
  8. more_view_classes/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
    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 TutorialViews
    
            request = testing.DummyRequest()
            inst = TutorialViews(request)
            response = inst.home()
            self.assertEqual('Home View', response['page_title'])
    
    class TutorialFunctionalTests(unittest.TestCase):
        def setUp(self):
            from tutorial import main
            app = main({})
            from webtest import TestApp
    
            self.testapp = TestApp(app)
    
        def test_home(self):
            res = self.testapp.get('/', status=200)
            self.assertIn(b'TutorialViews - Home View', res.body)
    
  9. テストを実行します:

    $ $VENV/bin/py.test tutorial/tests.py -q
    ..
    2 passed in 0.40 seconds
    
  10. Pyramidアプリケーションを以下のように実行します:

    $ $VENV/bin/pserve development.ini --reload
    
  11. ブラウザで http://localhost:6543/howdy/jane/doe を開いてみます。「Save」ボタンと「Delete」ボタンをクリックしてコンソールウィンドウで出力を確認します。

分析(Analysis)

ご覧のとおり4つのビューは論理的にグループ化されています。具体的には以下の通りです:

  • http://localhost:6543/ にある home ビューを hello ビューへのクリック可能なリンクと共に利用できます。
  • /howdy/jane/doe に行くと2番目のビューが返却されます。 このURLは、オプションの @view_defaults を使用して一元に設定する hello ルートにマップされます。
  • POST メソッドを使用してフォームを送信すると3番目のビューが返却されます。 このルールは、そのビューの @view_config で指定されています。
  • <input type="submit" name="form.delete" value="Delete"/> のようなボタンをクリックすると、4番目のビューが返却されます

このステップでは以下の情報を基準として、、どのビューを使用するかを決定する方法を説明します。

  • HTTPリクエスト(GET, POST,など) の方法
  • リクエスト(送信されたフォームフィールド名)内のパラメータ情報

また、 @view_defaults を使用してビュー構成の一部をクラスレベルに一元化させて、あるビューでそのビューのデフォルトをオーバーライドします。 最後にこの共通性をビュー間で共有することによってビュークラス内で機能させます

  • TutorialViews.__init__ 内に割り当てられたステート
  • 計算した値

これらは、ビューメソッドとテンプレート(たとえば、${view.view_name}${view.full_name})の両方で使用できます 。

テンプレートとしてURLの生成方法を変更しました。以前は以下ののようなURLをハードコードしていました。

<a href="/howdy/jane/doe">Howdy</a>

home.pt では以下のように切り替えました:

<a href="${request.route_url('hello', first='jane',
      last='doe')}">form</a>

Pyramidには、柔軟でエラーのない方法でURLを生成するための豊富な機能があります。

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

  1. テンプレートが ${view.full_name} を行い、${view.full_name()} を実行する必要がないのはなぜですか?
  2. 「編集」ビューと「削除」ビューはどちらも「POST」リクエストを受信します。 「編集」ビューの設定で「削除」で使用されたPOSTが検出されないのはなぜですか
  3. 「full_name」にPythonの @property を使用しました。テンプレートやビューコードでこれを何度も参照すると毎回これを再計算します。Pyramidはプロパティの初期計算をキャッシュするものを提供していますか?
  4. 同じビューに複数のルートを関連付けできますか?
  5. request.route_path があります。 これは request.route_url とどう違うのですか?