Edit me on GitHub

Step 06: View Classes

Free-standing functions are the regular way to do views. Many times, though, you have several views that are closely related. For example, a content type might have many different ways to look at it.

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

Even better, from a UX person’s perspective, the methods on the view class look like a “Template API” from the inside the namespace of the view.

Goals

  • Explain the why as well as the what on view classes
  • Show how templates interact with the app via the view class

Objectives

  • Move templates to their own directory
  • Understand the structure of a view class’s __init__ and methods
  • See how the @reify decorator can form a “Template API”
  • Adapt template expressions to point through the view
  • Change tests to instantiate the view class then call it
  • Move the repetitive dummy data into its own module

Steps

  1. $ cd ../../creatingux; mkdir step06; cd step06

  2. (Unchanged) Copy the following into step06/application.py:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    from wsgiref.simple_server import make_server
    
    from pyramid.config import Configurator
    
    def main():
        config = Configurator()
        config.include('pyramid_chameleon')
        config.scan("views")
        app = config.make_wsgi_app()
        return app
    
    if __name__ == '__main__':
        app = main()
        server = make_server('0.0.0.0', 8080, app)
        server.serve_forever()
    
  3. Copy the following into step06/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
    from pyramid.renderers import get_renderer
    from pyramid.decorator import reify
    from pyramid.view import view_config
    
    from dummy_data import COMPANY
    from dummy_data import PEOPLE
    from dummy_data import PROJECTS
    from dummy_data import SITE_MENU
    
    class ProjectorViews(object):
    
        def __init__(self, request):
            self.request = request
            renderer = get_renderer("templates/global_layout.pt")
            self.global_template = renderer.implementation().macros['layout']
    
        @reify
        def company_name(self):
            return COMPANY
    
        @reify
        def site_menu(self):
            new_menu = SITE_MENU[:]
            url = self.request.url
            for menu in new_menu:
                if menu['title'] == 'Home':
                    menu['current'] = url.endswith('/')
                else:
                    menu['current'] = url.endswith(menu['href'])
            return new_menu
    
        @view_config(renderer="templates/index.pt")
        def index_view(self):
            return {"page_title": "Home"}
    
        @view_config(renderer="templates/about.pt", name="about.html")
        def about_view(self):
            return {"page_title": "About"}
    
        @view_config(renderer="templates/company.pt",
                     name="acme")
        def company_view(self):
            return {"page_title": COMPANY + " Projects",
                    "projects": PROJECTS}
    
        @view_config(renderer="templates/people.pt", name="people")
        def people_view(self):
            return {"page_title": "People", "people": PEOPLE}
    
  4. Copy the following into step06/dummy_data.py:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    # Dummy data
    COMPANY = "ACME, Inc."
    
    PEOPLE = [
            {'name': 'sstanton', 'title': 'Susan Stanton'},
            {'name': 'bbarker', 'title': 'Bob Barker'},
    ]
    
    PROJECTS = [
            {'name': 'sillyslogans', 'title': 'Silly Slogans'},
            {'name': 'meaninglessmissions', 'title': 'Meaningless Missions'},
    ]
    
    SITE_MENU = [
            {'href': '', 'title': 'Home'},
            {'href': 'about.html', 'title': 'About Projector'},
            {'href': 'acme', 'title': COMPANY},
            {'href': 'people', 'title': 'People'},
    ]
    
  5. Copy the following “global template” into step06/templates/global_layout.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
    <!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>Projector - ${page_title}</title>
    </head>
    <body>
    <ul>
        <li tal:repeat="menu view.site_menu">
            <tal:block tal:condition="menu.current">
                <span>${menu.title}</span>
            </tal:block>
            <tal:block tal:condition="not menu.current">
                <span><a href="/${menu.href}">${menu.title}</a></span>
            </tal:block>
        </li>
    </ul>
    <h1>${page_title}</h1>
    
    <div metal:define-slot="content">
    </div>
    </body>
    </html>
    
  6. Copy the following into step06/templates/index.pt:

    1
    2
    3
    4
    5
    <div metal:use-macro="view.global_template">
        <div metal:fill-slot="content">
            <p>Home page content goes here.</p>
        </div>
    </div>
    
  7. Copy the following into step06/templates/about.pt:

    1
    2
    3
    4
    5
    6
    7
    <div metal:use-macro="view.global_template">
        <div metal:fill-slot="content">
            <p>Projector is a simple project management tool capable of hosting
                multiple projects for multiple independent companies,
                sharing a developer pool between autonomous companies.</p>
        </div>
    </div>
    
  8. Copy the following into step06/templates/company.pt:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div metal:use-macro="view.global_template">
        <div metal:fill-slot="content">
            <ul>
                <li tal:repeat="project projects">
                    <a href="${project.name}">${project.title}</a>
                </li>
            </ul>
        </div>
    </div>
    
  9. Copy the following into step06/templates/people.pt:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div metal:use-macro="view.global_template">
        <div metal:fill-slot="content">
            <ul>
                <li tal:repeat="person people">
                    <a href="${person.name}">${person.title}</a>
                </li>
            </ul>
        </div>
    </div>
    
  10. Copy the following into step06/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
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    import unittest
    
    from pyramid.testing import DummyRequest
    from pyramid.testing import setUp
    from pyramid.testing import tearDown
    
    class ProjectorViewsUnitTests(unittest.TestCase):
        def setUp(self):
            request = DummyRequest()
            self.config = setUp(request=request)
            self.config.include('pyramid_chameleon')
            
        def tearDown(self):
            tearDown()
    
        def _makeOne(self, request):
            from views import ProjectorViews
    
            inst = ProjectorViews(request)
            return inst
    
        def test_index_view(self):
            request = DummyRequest()
            inst = self._makeOne(request)
            result = inst.index_view()
            self.assertEqual(result['page_title'], 'Home')
    
        def test_about_view(self):
            request = DummyRequest()
            inst = self._makeOne(request)
            result = inst.about_view()
            self.assertEqual(result['page_title'], 'About')
    
        def test_company_view(self):
            request = DummyRequest()
            inst = self._makeOne(request)
            result = inst.company_view()
            self.assertEqual(result["page_title"], "ACME, Inc. Projects")
            self.assertEqual(len(result["projects"]), 2)
    
        def test_people_view(self):
            request = DummyRequest()
            inst = self._makeOne(request)
            result = inst.people_view()
            self.assertEqual(result["page_title"], "People")
            self.assertEqual(len(result["people"]), 2)
    
    
    class ProjectorFunctionalTests(unittest.TestCase):
        def setUp(self):
            from application import main
    
            app = main()
            from webtest import TestApp
    
            self.testapp = TestApp(app)
    
        def test_it(self):
            res = self.testapp.get('/', status=200)
            self.assertTrue(b'Home' in res.body)
            res = self.testapp.get('/about.html', status=200)
            self.assertTrue(b'autonomous' in res.body)
            res = self.testapp.get('/people', status=200)
            self.assertTrue(b'Susan' in res.body)
            res = self.testapp.get('/acme', status=200)
            self.assertTrue(b'Silly Slogans' in res.body)
    
  11. $ nosetests should report running 5 tests.

  12. $ python application.py

  13. Open http://127.0.0.1:8080 in your browser.

Extra Credit

  1. Why do some ZPT expressions need view. and some don’t?
  2. What exactly does @reify do?
  3. Could you shorten your unit tests by making a DummyRequest () in the test’s __init__?
  4. If you do an expensive calculation for one view, does that increase performance in another view that doesn’t need to recalculate it?
  5. Where does @reify store the cached value?

Analysis

The idea of a view class can be used to form different patterns. In this case, we want a unit of related work, join up the views for that work, and craft our own little API that our templates use.

The test writing gets a little bit harder.

Discussion

  • What was the original need that spawned view classes?
  • How do other system approach the idea?
  • What is a “push page” and what need was it addressing?

Table Of Contents

Previous topic

Step 05: Making a Main Template

Next topic

Step 07: Layouts