Step 08: CSS and JS With Static Assets

Web applications include many static assets in the UX: CSS and JS files, images, etc. Web frameworks need to support productive development by the UX team, but also the richness and complexity required by the core developers and the deployment team.

It’s a surprisingly hard problem, supporting all these needs while keeping the simple case easy.

Pyramid accomplishes this using the view machinery and static assets.

Goals

  • Show Pyramid’s support for static assets

Objectives

  • Add a static view to Pyramid’s Configurator
  • Change the main template to includes the CSS and JS
  • Change the templates to have a nicer layout

Steps

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

  2. Copy the following into step08/application.py:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    from wsgiref.simple_server import make_server
    
    from pyramid.config import Configurator
    
    def main():
        config = Configurator()
        config.scan("views")
        config.add_static_view('static', 'static/',
                               cache_max_age=86400)
        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. $ mkdir static

  4. Copy the following into step08/static/global_layout.css:

     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
    body {
        font-family: Arial, sans-serif;
        font-size: 1em;
        padding: 0;
        margin: 0;
        background-color: white;
    }
    
    #header {
        height: 3em;
        background-color: lightgray;
    }
    
    #header ul {
        margin-left: 1em;
        padding: 0;
    }
    
    #header li {
        display: inline-block;
        margin-top: 1em;
        padding-right: 0.8em;
    }
    
    #header a {
        color: black;
    }
    #main {
        margin: 2em;
    }
    
  5. Copy the following into step08/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}
    
  6. Copy the following into step08/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
    
  7. Copy the following into step08/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'},
    ]
    
  8. Copy the following “global template” into step08/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
    27
    28
    29
    30
    <!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>
        <link rel="stylesheet" href="/static/global_layout.css"/>
    </head>
    <body>
    <div id="header">
        <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>
    </div>
    <div id="main"><h1>${page_title}</h1>
    
        <div metal:define-slot="content">
        </div>
    </div>
    </body>
    </html>
    
  9. Copy the following into step08/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>
    
  10. Copy the following into step08/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>
    
  11. Copy the following into step08/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>
    
  12. Copy the following into step08/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>
    
  13. Copy the following into step08/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)
    
  14. Copy the following into step08/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))
    
  15. $ nosetests should report running 8 tests.

  16. $ python application.py

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

Analysis

Being able to point your Pyramid app at an entire directory and publish it is a boon for quick development. We grabbed the configurator and, with one line, published a directory of assets. No need to individually publish each file and set mime-type.

Setting expires headers is a fiddly part of the development cycle.

Extra Credit

  1. Make a static file of JSON data in the static directory, then write a jQuery AJAX function that fetches it and shoves in a <ul>.
  2. Learn about ZPT’s fill-slot to allow each view’s template to include some custom CSS into the <head>.
  3. Will Pyramid recurse sub-directories? Can you get a directory listing of files in a static directory?

Analysis

Not much to cover. We have a config method that lets us jam in a new part of the URL space, serving up static files.

Discussion

  • What does add_static_view do under the hood?
  • What are some of the weird cases for deeper development (e.g. multi-site roots) and deployment (e.g. far-future expires)?
  • Pyramid’s extra support for, in Python code, resolving the URL path to directories that were “mounted” in configuration
  • How this does or doesn’t map to ZCML

Table Of Contents