In Step 05: Making a Main Template we made a main template that was used for all screens in the site. We used ZPT machinery as the solution.
Sometimes, though, it isn’t a global look and feel that needs re-use. It is a little snippet that is common to some of the pages, but not all. Or perhaps, a block that looks different on some screens versus others.
ZPT macros are the solution here.
$ cd ../../creatingux; mkdir step10; cd step10
Copy the following into step10/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()
|
$ mkdir static
Copy the following into step10/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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | body {
font-family: Arial, sans-serif;
font-size: 1em;
padding: 0;
margin: 0;
background-color: white;
}
#header {
height: 3em;
background-color: lightgray;
}
#header ul, #company_menu {
margin-left: 1em;
padding: 0;
}
#header li, #company_menu li {
display: inline-block;
margin-top: 1em;
padding-right: 0.8em;
}
#header a {
color: black;
}
#main {
margin: 2em;
}
#sidebar {
float:right;
width: 300px;
border-left: solid 2px gray;
border-bottom: solid 2px gray;
padding-left: 10px;
}
#sidebar ul {
height: 100px;
}
#sidebar h2 {
text-align: center;
}
#sidebar p {
text-align: center;
}
|
Copy the following into step10/static/global_layout.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | $(function() {
function get_updates () {
$.getJSON('/updates.json', function(data) {
var target = $('#sidebar ul');
target.empty();
$.each(data, function (key, val) {
target.append('<li>Update #' + val + '</li>');
});
});
}
$('#sidebar a').click(function () {
get_updates();
});
get_updates();
});
|
Copy the following into step10/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 | from random import randint
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}
@view_config(renderer="json", name="updates.json")
def updates_view(self):
return [
randint(0,100),
randint(0,100),
randint(0,100),
randint(0,100),
888,
]
|
Copy the following into step10/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 28 29 30 31 32 | 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 global_macros(self):
renderer = get_renderer("templates/macros.pt")
return renderer.implementation().macros
@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
|
Copy the following into step10/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'},
]
|
Copy the following “global template” into step10/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 31 32 33 34 35 36 37 | <!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"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script src="/static/global_layout.js"></script>
</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="sidebar">
<h2>Updates</h2>
<ul></ul>
<p><a href="#">reload</a></p>
</div>
<div id="main"><h1>${page_title}</h1>
<div metal:define-slot="content">
</div>
</div>
</body>
</html>
|
Copy the following “macros template” into step10/templates/macros.pt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <!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">
<head>
<title>Macros</title>
</head>
<body>
<metal:company_menu define-macro="company_menu">
<ul id="company_menu">
<li>Projects: </li>
<li tal:repeat="project projects">
<a href="${project.name}">${project.title}</a>
</li>
</ul>
</metal:company_menu>
</body>
</html>
|
Copy the following into step10/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>
|
Copy the following into step10/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>
|
Copy the following into step10/templates/company.pt:
1 2 3 4 5 6 | <div metal:use-macro="view.global_template">
<div metal:fill-slot="content">
<p>Welcome to our company project management system.</p>
<div metal:use-macro="view.global_macros['company_menu']"></div>
</div>
</div>
|
Copy the following into step10/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>
|
Copy the following into step10/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 63 64 65 66 67 68 69 70 | 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)
def test_updates_view(self):
request = DummyRequest()
inst = self._makeOne(request)
result = inst.updates_view()
self.assertEqual(len(result), 5)
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)
res = self.testapp.get('/updates.json', status=200)
self.assertTrue(b'888' in res.body)
|
Copy the following into step10/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 39 40 41 42 43 44 45 | 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_global_macros(self):
from chameleon.zpt.template import Macros
inst = self._makeOne()
self.assertEqual(inst.global_macros.__class__, Macros)
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))
|
$ nosetests should report running 10 tests.
$ python application.py
Open http://127.0.0.1:8080 in your browser and look at the ACME, Inc. link to see the projects menu.
Macros are a long-standing feature in the world of Zope, and as such, are a mature and well-understood way to decompose UX into re-usable snippets.
The indirections in Zope (2, 3, CMF, Plone), though, has made “Where did that pixel come from?” into a crazy adventure. But if you remove the pluggability, macros become a less mysterious and more useful tool.