8: 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.

Objectives

  • Use add_static_view serve a project directory at a URL
  • Introduce some dummy data to provide dynamicism

Steps

  1. Again, let’s use the previous package as a starting point for a new distribution, plus make a new directory for the static assets:

    (env33)$ cd ..; cp -r step07 step08; cd step08
    (env33)$ mkdir tutorial/static
    (env33)$ python3.3 setup.py develop
    
  2. The Configurator needs to be told in tutorial/__init__.py about the static files by calling its add_static_view:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    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.add_static_view(name='static', path='tutorial:static')
        config.scan()
        return config.make_wsgi_app()
    
  3. Our tutorial/views.py has some extras with dummy data driving the listing and viewing:

     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
    from pyramid.httpexceptions import HTTPFound
    from pyramid.renderers import get_renderer
    from pyramid.view import view_config
    
    pages = [
        dict(uid='100', title='Page 100', body='<em>100</em>'),
        dict(uid='101', title='Page 101', body='<em>101</em>'),
        dict(uid='102', title='Page 102', body='<em>102</em>'),
    ]
    
    
    class WikiViews(object):
        def __init__(self, request):
            self.request = request
            renderer = get_renderer("templates/layout.pt")
            self.layout = renderer.implementation().macros['layout']
    
        def get_pages(self):
            return pages
    
        @view_config(route_name='wiki_view',
                     renderer='templates/wiki_view.pt')
        def wiki_view(self):
            return dict(title='Welcome to the Wiki', pages=pages)
    
        @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']
            page = [page for page in pages if page['uid'] == uid][0]
            title = page['title']
            return dict(page=page, title=title)
    
        @view_config(route_name='wikipage_edit',
                     renderer='templates/wikipage_addedit.pt')
        def wikipage_edit(self):
            uid = self.request.matchdict['uid']
            page = [page for page in pages if page['uid'] == uid][0]
            title = 'Edit ' + page['title']
            return dict(title=title)
    
        @view_config(route_name='wikipage_delete')
        def wikipage_delete(self):
            url = self.request.route_url('wiki_view')
            return HTTPFound(url)
    
  4. Make a tutorial/static/wiki.css for the styling:

    body {
        font-family: sans-serif;
        margin: 2em;
    }
    
    h1 a {
        vertical-align: bottom;
    }
    
  5. We also have a logo PNG file that we need saved at tutorial/static/logo.png. Click this link to download and save the image.

  6. The tutorial/templates/layout.pt includes the CSS on all pages, plus adds a logo that goes back to the wiki home:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <!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>
        <link rel="stylesheet"
              href="${request.static_url('tutorial:static/wiki.css')}"/>
    </head>
    <body>
    <div id="main">
        <h1>
            <a href="${request.route_url('wiki_view')}">
                <img src="${request.static_url('tutorial:static/logo.png')}"
                     alt="Logo"/></a>
            ${title}</h1>
    
        <div metal:define-slot="content">
        </div>
    </div>
    </body>
    </html>
    
  7. tutorial/templates/wiki_view.pt switches to a more-general syntax for computing URLs, plus iterates over the actual (dummy) data for listing each wiki page:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    <div metal:use-macro="view.layout">
        <div metal:fill-slot="content">
            <a href="${request.route_url('wikipage_add')}">Add
                WikiPage</a>
            <ul>
                <li tal:repeat="page pages">
                    <a href="${request.route_url('wikipage_view', uid=page.uid)}">
                        ${page.title}
                    </a>
                </li>
            </ul>
        </div>
    </div>
    
  8. We also change tutorial/templates/wikipage_view.pt to use the route_url approach to URLs:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    <div metal:use-macro="view.layout">
        <div metal:fill-slot="content">
            <a href="${request.route_url('wikipage_edit', uid=page.uid)}">
                Edit
            </a> |
            <a href="${request.route_url('wikipage_delete', uid=page.uid)}">
                Delete
            </a>
    
            <p>${structure: page.body}</p>
        </div>
    </div>
    
  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

We made files and directories in tutorial/static available at the URL static. However, we used tutorial:static as the argument in add_static_view. Pyramid uses a robust scheme called asset specifications to work with static assets.

In our templates, we resolved the full path to a static asset in a package by using request.static_url and passing in an asset specification. route_url, static_url, and friends let you refactor your URL structure, or even publish to a different root URL, without breaking the links in your templates.

Finally, we’re cheating by having mutable dummy data at module scope. We will replace this shortly with database-driven data.

Extra Credit

  1. Can you use add_static_view to serve up a directory listing with links to the contents in a directory?
  2. Does Pyramid have support for setting cache parameters on static assets?
  3. Can you also use asset specifications when naming the template for a view?
  4. Can I provide a one-liner for including static assets in my Pyramid libraries?

Table Of Contents