7: View Classes

Free-standing functions are the simple way to do views. Many times, though, you have several views that are closely related. For example, a wiki might have many different ways to look at it (home page, add, view, edit, delete, table of contents, etc.)

For some people, grouping these together makes logical sense. A view class lets you group these views, sharing some state assignments and helper functions as class methods.

Objectives

  • Introduce a view class for our wiki-related views
  • Start making this more like our wiki application by adding more views and templates

Steps

  1. Let’s again use the previous package as a starting point for a new distribution:

    (env33)$ cd ..; cp -r step06 step07; cd step07
    (env33)$ python3.3 setup.py develop
    
  2. Edit tutorial/views.py to include a view class with several more views, along with a “layout” template:

     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
    from pyramid.httpexceptions import HTTPFound
    from pyramid.renderers import get_renderer
    from pyramid.view import view_config
    
    
    class WikiViews(object):
        def __init__(self, request):
            self.request = request
            renderer = get_renderer("templates/layout.pt")
            self.layout = renderer.implementation().macros['layout']
    
        @view_config(route_name='wiki_view',
                     renderer='templates/wiki_view.pt')
        def wiki_view(self):
            return dict(title='Welcome to the Wiki')
    
        @view_config(route_name='wikipage_add',
                     renderer='templates/wikipage_addedit.pt')
        def wikipage_add(self):
            return dict(title='Add Wiki Page')
    
        @view_config(route_name='wikipage_view',
                     renderer='templates/wikipage_view.pt')
        def wikipage_view(self):
            uid = self.request.matchdict['uid']
            return dict(title='View Wiki Page', uid=uid)
    
        @view_config(route_name='wikipage_edit',
                     renderer='templates/wikipage_addedit.pt')
        def wikipage_edit(self):
            return dict(title='Edit Wiki Page')
    
        @view_config(route_name='wikipage_delete')
        def wikipage_delete(self):
            return HTTPFound('/')
    
  3. Since we have new views, our __init__.py needs new routes:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    from pyramid.config import Configurator
    
    
    def main(global_config, **settings):
        config = Configurator(settings=settings)
        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_route('wikipage_delete', '/{uid}/delete')
        config.scan()
        return config.make_wsgi_app()
    
  4. Now we make some new templates. First, tutorial/templates/layout.pt:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
            "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml"
          xmlns:metal="http://xml.zope.org/namespaces/metal"
          xmlns:tal="http://xml.zope.org/namespaces/tal"
          metal:define-macro="layout">
    <head>
        <title>Wiki - ${title}</title>
    </head>
    <body>
    <div id="main">
        <div>
            <a href="/">Wiki Home</a>
        </div>
        <h1>${title}</h1>
    
        <div metal:define-slot="content">
        </div>
    </div>
    </body>
    </html>
    
  5. Next, tutorial/templates/wiki_view.pt:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <div metal:use-macro="view.layout">
        <div metal:fill-slot="content">
            <p>Some sample links:</p>
            <ul>
                <li>
                    <a href="/">Wiki Home</a>
                </li>
                <li>
                    <a href="/add">Add WikiPage</a>
                </li>
                <li>
                    <a href="/100">View WikiPage</a>
                </li>
                <li>
                    <a href="/100/edit">Edit WikiPage</a>
                </li>
                <li>
                    <a href="/100/delete">Delete WikiPage</a>
                </li>
            </ul>
        </div>
    </div>
    
  6. Next, tutorial/templates/wikipage_addedit.pt:

    1
    2
    3
    4
    5
    <div metal:use-macro="view.layout">
        <div metal:fill-slot="content">
            <p>Content here.</p>
        </div>
    </div>
    
  7. Finally, tutorial/templates/wikipage_view.pt:

    1
    2
    3
    4
    5
    <div metal:use-macro="view.layout">
        <div metal:fill-slot="content">
            <p>Content here for uid <em>${uid}</em>.</p>
        </div>
    </div>
    
  8. Since we have more views, we need more tests in 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
    37
    38
    39
    40
    41
    42
    import unittest
    
    from pyramid import testing
    
    
    class WikiViewTests(unittest.TestCase):
        def setUp(self):
            self.config = testing.setUp()
    
        def tearDown(self):
            testing.tearDown()
    
        def test_wiki_view(self):
            from tutorial.views import WikiViews
    
            request = testing.DummyRequest()
            inst = WikiViews(request)
            response = inst.wiki_view()
            self.assertEqual(response['title'], 'Welcome to the Wiki')
    
    
    class WikiFunctionalTests(unittest.TestCase):
        def setUp(self):
            from tutorial import main
    
            settings = {}
            app = main(settings)
            from webtest import TestApp
    
            self.testapp = TestApp(app)
    
        def test_it(self):
            res = self.testapp.get('/', status=200)
            self.assertIn(b'Welcome', res.body)
            res = self.testapp.get('/add', status=200)
            self.assertIn(b'Add Wiki Page', res.body)
            res = self.testapp.get('/100', status=200)
            self.assertIn(b'100', res.body)
            res = self.testapp.get('/100/edit', status=200)
            self.assertIn(b'Edit', res.body)
            res = self.testapp.get('/100/delete', status=302)
            self.assertIn(b'Found', res.body)
    
  9. Run the tests in your package using nose:

    (env33)$ nosetests .
    ..
    -----------------------------------------------------------------
    Ran 2 tests in 1.971s
    
    OK
    
  10. Run the WSGI application:

    (env33)$ pserve development.ini --reload
    
  11. Open http://127.0.0.1:6547/ in your browser.

Analysis

Our __init__ shows us a new feature we are using with our add_route configurations: named parameters. These variables, indicated with curly braces, later become available on request.matchdict.

We are adding multiple views to our site now. Rather than repeat the same stuff in <head> and the other “chrome” common to all templates, we make a master template available in the view class as layout. Attributes and methods of the view class instance are available inside the template from the view variable, as we saw with view.layout.

The master template defines slots that can be filled by the view templates. Our layout.pt used metal:define-slot="content" to make one such slot. Each view template filled this with metal:fill-slot="content".

Our wikipage_delete returns an HTTPFound. This is Pyramid’s way of issuing a redirect.

In our unit tests we have to add an extra step to create an instance of the view class, and then call the view being tested.

Extra Credit

  1. If we wanted to make a set of statements to several views in a view class at once, how would we do that?
  2. Can I put multiple named parameters in my routes?

Table Of Contents