Step 07: Layouts

In Step 05: Making a Main Template we talked about a main template. Great idea which makes a big impact on UX productivity.

However, a little bit more is needed. The main template usually needs data and methods to fill in some of its blocks (e.g. the site_menu). With our view classes approach, the class properties we used as the “Template API” were co-mingled with view-specific properties.

Sure would be nice to have a discrete, standalone place to find the template and the template API, apart from each view.

For this tutorial we are inventing the idea of a Layout, which is exactly that combination: one (or more) templates and the logic those templates need.

Goals

  • Help UX developers keep global stuff apart from the specific stuff
  • Decrease test writing
  • Support multiple look and feels in a site

Objectives

  • Make an abstract base class (no __init__) in a layouts.py module
  • Each supported main template is its own property on that Layouts class
  • View classes then subclass from the project’s Layouts class
  • Split the tests.py into unit and functional tests for the views, and unit tests for the Layouts module

Steps

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

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

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    from wsgiref.simple_server import make_server
    
    from pyramid.config import Configurator
    
    def main():
        config = Configurator()
        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 step07/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
    from pyramid.view import view_config
    
    from dummy_data import COMPANY
    from dummy_data import PEOPLE
    from dummy_data import PROJECTS
    
    from layouts import Layouts
    
    class ProjectorViews(Layouts):
    
        def __init__(self, request):
            self.request = request
    
        @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 step07/layouts.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
    from pyramid.renderers import get_renderer
    from pyramid.decorator import reify
    
    from dummy_data import COMPANY
    from dummy_data import SITE_MENU
    
    class Layouts(object):
    
        @reify
        def global_template(self):
            renderer = get_renderer("templates/global_layout.pt")
            return 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
    
  5. Copy the following into step07/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'},
    ]
    
  6. Copy the following “global template” into step07/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>
    
  7. Copy the following into step07/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>
    
  8. Copy the following into step07/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>
    
  9. Copy the following into step07/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>
    
  10. Copy the following into step07/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>
    
  11. Copy the following into step07/test_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
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    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)
    
        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)
    
  12. Copy the following into step07/test_layout.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
    import unittest
    
    from pyramid.testing import DummyRequest
    from pyramid.testing import setUp
    from pyramid.testing import tearDown
    
    class LayoutUnitTests(unittest.TestCase):
        def setUp(self):
            request = DummyRequest()
            self.config = setUp(request=request)
    
        def tearDown(self):
            tearDown()
    
        def _makeOne(self):
            from layouts import Layouts
    
            inst = Layouts()
            return inst
    
        def test_global_template(self):
            from chameleon.zpt.template import Macro
    
            inst = self._makeOne()
            self.assertEqual(inst.global_template.__class__, Macro)
    
        def test_company_name(self):
            from dummy_data import COMPANY
    
            inst = self._makeOne()
            self.assertEqual(inst.company_name, COMPANY)
    
        def test_site_menu(self):
            from dummy_data import SITE_MENU
    
            inst = self._makeOne()
            inst.request = DummyRequest()
            self.assertEqual(len(inst.site_menu), len(SITE_MENU))
    
  13. $ nosetests should report running 8 tests.

  14. $ python application.py

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

Extra Credit

  1. How might we support multiple layouts? Give it a try.

Analysis

One might start thinking that layouts shouldn’t be hardwired. You should be able to make a layout (template and template API) and register it. Views could then grab the one they want, perhaps from a dictionary. Perhaps even support theme switching. Such pluggability is an anti-goal. Custom UX projects should specifically make and name what they create.

Discussion

  • Is such a pattern really needed?
  • What are some other, previous “Template API” ideas?
  • Would a UX developer want to know that a property came from the layout instead of the view?
  • How does this relate to the idea of a “theme”?

Table Of Contents