Demo App With Pyramid Layout

Let’s see Pyramid Layout in action with the demo application provided in demo.

Installation

Normal Pyramid stuff:

  1. Make a virtualenv
  2. env/bin/python demo/setup.py develop
  3. env/bin/pserve demo/development.ini
  4. Open http://0.0.0.0:6543/ in a browser
  5. Click on the Home Mako, Home Chameleon, and Home Jinja2 links in the header to see views for that use each.

Now let’s look at some of the code.

Registration

Pyramid Layout defines configuration directives and decorators you can use in your project. We need those loaded into our code. The demo does this in the etc/development.ini file:

pyramid.includes =
    pyramid_debugtoolbar

mako.directories = demo:templates

The development.ini entry point starts in demo/__init__.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
from pyramid.config import Configurator

def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    config = Configurator(settings=settings)
    config.include('pyramid_chameleon')
    config.include('pyramid_jinja2')
    config.include('pyramid_mako')
    config.include('pyramid_layout')
    config.add_static_view('static', 'static', cache_max_age=3600)
    config.add_route('home.mako', '/')
    config.add_route('home.chameleon', '/chameleon')
    config.add_route('home.jinja2', '/jinja2')
    config.scan('.layouts')
    config.scan('.panels')
    config.scan('.views')
    return config.make_wsgi_app()

This is all Configurator action. We register a route for each view. We then scan our demo/layouts.py, demo/panels.py, and demo/views.py for registrations.

Layout

Let’s start with the big picture: the global look-and-feel via a layout:

 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
from pyramid_layout.layout import layout_config


@layout_config(template='demo:templates/layouts/layout.mako')
@layout_config(
    name='chameleon',
    template='demo:templates/layouts/layout.pt'
    )
@layout_config(
    name='jinja2',
    template='demo:templates/layouts/layout.jinja2'
    )
class AppLayout(object):

    def __init__(self, context, request):
        self.context = context
        self.request = request
        self.home_url = request.application_url
        self.headings = []
        self.portlets = (Thing1(), Thing2(), LittleCat("A"))

    @property
    def project_title(self):
        return 'Pyramid Layout App!'

    def add_heading(self, name, *args, **kw):
        self.headings.append((name, args, kw))


class Thing1(object):
    title = "Thing 1"
    content = "I am Thing 1!"


class Thing2(object):
    title = "Thing 2"
    content = "I am Thing 2!"


class LittleCat(object):
    talent = "removing pink spots"

    def __init__(self, name):
        self.name = name

The @layout_config decorator comes from Pyramid Layout and allows us to define and register a layout. In this case we’ve stacked 3 decorators, thus making 3 layouts, one for each template language.

Note

The first @layout_config doesn’t have a name and is thus the layout that you will get if your view doesn’t specifically choose which layout it wants.

Lines 21-24 illustrates the concept of keeping templates and the template logic close together. All views need to show the project_title. It’s part of the global look-and-feel main template. So we put this logic on the layout, in one place as part of the global contract, rather than having each view supply that data/logic.

Let’s next look at where this is used in the template for one of the 3 layouts. In this case, the Mako template at demo/templates/layouts/layout.mako:

<title>${layout.project_title}, from Pylons Project</title>

Here we see an important concept and some important magic: the template has a top-level variable layout available. This is an instance of your layout class.

For the ZPT crowd, if you look at the master template in demo/templates/layouts/layout.pt, you might notice something weird at the top: there’s no metal:define-macro. Since Chameleon allows a template to be a top-level macro, Pyramid Layout automatically binds the entire template to the macro named main_template.

How does your view know to use a layout? Let’s take a look.

Connecting Views to a Layout

Our demo app has a very simple set of views:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pyramid.view import view_config

@view_config(
    route_name='home.mako',
    renderer='demo:templates/home.mako'
    )
@view_config(
    route_name='home.chameleon',
    renderer='demo:templates/home.pt',
    layout='chameleon'
    )
@view_config(
    route_name='home.jinja2',
    renderer='demo:templates/home.jinja2',
    layout='jinja2'
    )
def home(request):
    lm = request.layout_manager
    lm.layout.add_heading('heading-mako')
    lm.layout.add_heading('heading-chameleon')
    lm.layout.add_heading('heading-jinja2')
    return {}

We again have one callable with 3 stacked decorators. The decorators are all normal Pyramid @view_config stuff.

The second one points at a Chameleon template in demo/templates/home.pt:

<metal:block use-macro="main_template">

  <div metal:fill-slot="content">
    <!-- Main hero unit for a primary marketing message or call to action -->
    ${panel('hero', title='Chameleon')}

    <!-- Example row of columns -->
    <div class="row">
      <p>${panel('headings')}</p>
    </div>
    <div class="row">
        <p>${panel('contextual_panels')}</p>
    </div>
    <div class="row">
        <h2>User Info</h2>
        <p>${panel('usermenu',
            user_info={
            'first_name': 'Jane',
            'last_name': 'Doe',
            'username': 'jdoe'}
            )}</p>
    </div>
  </div>

</metal:block>

The first line is the one that opts the template into the layout. In home.jinja2 that line looks like:

{% extends main_template %}

For both of these, main_template is inserted by Pyramid Layout, via a Pyramid renderer global, into the template’s global namespace. After that, it’s normal semantics for that template language.

Back to views.py. The view function grabs the Layout Manager, which Pyramid Layout conveniently stashes on the request. The LayoutManager‘s primary job is getting/setting the current layout. Which, of course, we do in this function.

Our function then grabs the layout instance and manipulates some state that is needed in the global look and feel. This, of course, could have been done in our AppLayout class, but in some cases, different views have different values for the headings.

Re-Usable Snippets with Panels

Our main template has something interesting in it:

  <body>

    ${panel('navbar')}

    <div class="container">

      ${next.body()}

      <hr>

      <footer>
        ${panel('footer')}
      </footer>

    </div> <!-- /container -->

    <!-- Le javascript
    ================================================== -->
    <!-- Placed at the end of the document so the pages load faster -->
    <script src="${request.static_url('demo:static/js/jquery-1.8.0.min.js')}"></script>
    <script src="${request.static_url('demo:static/js/bootstrap.min.js')}"></script>

  </body>

Here we break our global layout into reusable parts via panels. Where do these come from? @panel_config decorators, as shown in panels.py. For example, this:

${panel('navbar')}

...comes from this:



@panel_config(
    name='navbar',
    renderer='demo:templates/panels/navbar.mako'
    )
def navbar(context, request):
    def nav_item(name, url):
        active = request.current_route_url() == url
        item = dict(
            name=name,
            url=url,
            active=active
            )
        return item
    nav = [
        nav_item('Mako', request.route_url('home.mako')),
        nav_item('Chameleon', request.route_url('home.chameleon')),
        nav_item('Jinja2', request.route_url('home.jinja2'))
        ]
    return {
        'title': 'Demo App',

The @panel_config registered a panel under the name navbar, which our template could then use or override.

The home.mako view template has a more interesting panel:

${panel('hero', title='Mako')}

...which calls:

1
2
3
4
5
6


@panel_config(
    name='hero',
    renderer='demo:templates/panels/hero.mako'
    )

This shows that a panel can be parameterized and used in different places in different ways.