Let’s see Pyramid Layout in action with the demo application provided in demo.
Normal Pyramid stuff:
Now let’s look at some of the code.
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
pyramid_jinja2
pyramid_layout
The development.ini entry point starts in demo/__init__.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | from pyramid.config import Configurator
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config = Configurator(settings=settings)
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.
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 | 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 = []
@property
def project_title(self):
return 'Pyramid Layout App!'
def add_heading(self, name, *args, **kw):
self.headings.append((name, args, kw))
|
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.
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>
<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.
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',
'nav': nav
}
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'
)
def hero(context, request, title='Hello, world!'):
return {'title': title}
|
This shows that a panel can be parameterized and used in different places in different ways.