Edit me on GitHub

Step 05: Making a Main Template

Making a templated UX, as we’ve seen, is easy to get started on. Quickly, though, the UX designer starts to repeat things. In our example, the “site menu” <ul> appears in each page template.

We want a “main” template with all the common stuff. Then, each view’s template simply jams the markup for that screen into a spot in the main template.

ZPT has a “macros” facility for this, which we use in this step.

Goals

  • Productive UX development through re-use

Objectives

  • Move templates to their own directory
  • Using ZPT macros, create a “main template” as a global layout which has the stuff common to all pages
  • Change each view’s template to grab the main template and fill in its slot with stuff unique to that view

Steps

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

  2. (Unchanged) Copy the following into step05/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 step05/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
    from pyramid.renderers import get_renderer
    from pyramid.view import view_config
    
    def site_layout():
        renderer = get_renderer("templates/global_layout.pt")
        layout = renderer.implementation().macros['layout']
        return layout
    
    
    @view_config(renderer="templates/index.pt")
    def index_view(request):
        return {"layout": site_layout(),
                "page_title": "Home"}
    
    
    @view_config(renderer="templates/about.pt", name="about.html")
    def about_view(request):
        return {"layout": site_layout(),
                "page_title": "About"}
    
    
    @view_config(renderer="templates/company.pt", name="acme")
    def company_view(request):
        return {"layout": site_layout(),
                "page_title": COMPANY + " Projects",
                "company": COMPANY,
                "projects": PROJECTS}
    
    
    @view_config(renderer="templates/people.pt", name="people")
    def people_view(request):
        return {"layout": site_layout(),
                "page_title": "People", "company": COMPANY, "people": PEOPLE}
    
    # 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'},
    ]
    
  4. Copy the following “global template” into step05/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
    <!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><a href="/">Home</a></li>
        <li><a href="/about.html">About Projector</a></li>
        <li><a href="/acme">ACME, Inc.</a></li>
        <li><a href="/people">People</a></li>
    </ul>
    <h1>${page_title}</h1>
    <div metal:define-slot="content">
    </div>
    </body>
    </html>
    
  5. Copy the following into step05/templates/index.pt:

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

    1
    2
    3
    4
    5
    6
    7
    <div metal:use-macro="layout">
        <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>
    
  7. Copy the following into step05/templates/company.pt:

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <div metal:use-macro="layout">
        <div metal:fill-slot="content">
            <ul>
                <li tal:repeat="person people">
                    <a href="${person.name}">${person.title}</a>
                </li>
            </ul>
        </div>
    </div>
    
  9. Copy the following into step05/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
    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 test_hello_view(self):
            from views import index_view
    
            result = index_view({})
            self.assertEqual(result['page_title'], 'Home')
    
        def test_about_view(self):
            from views import about_view
    
            result = about_view({})
            self.assertEqual(result['page_title'], 'About')
    
        def test_company_view(self):
            from views import company_view
    
            result = company_view({})
            self.assertEqual(result["company"], "ACME, Inc.")
            self.assertEqual(len(result["projects"]), 2)
    
        def test_people_view(self):
            from views import people_view
    
            result = people_view({})
            self.assertEqual(result["company"], "ACME, Inc.")
            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)
    
  10. $ nosetests should report running 5 tests.

  11. $ python application.py

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

Extra Credit

  1. Do we really need to declare the XML namespaces for metal and tal?
  2. What happens if you try to fill a slot that doesn’t exist?

Analysis

We had to add a little more ceremony to the unit tests, to get a proper configuration.

Discussion

  • ZPT and macros have a very long history in Zope. Discuss the origins and lessons learned.

Table Of Contents

Previous topic

Step 04: Skeleton Views

Next topic

Step 06: View Classes