Modern web development means AJAX. Through its renderer support, Pyramid makes return JSON data from a view very easy and coherent with the rest of the Pyramid architecture.
In this step we add a box to each screen which fetches, formats, and re-fetches site news updates.
Note
Our templates will include jQuery from the Google CDN.
$ cd ../../creatingux; mkdir step09; cd step09
Copy the following into step09/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 step09/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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | 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;
}
#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;
}
#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 step09/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 step09/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 step09/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
|
Copy the following into step09/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 step09/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 into step09/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 step09/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 step09/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>
|
Copy the following into step09/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 step09/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 step09/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))
|
$ nosetests should report running 9 tests.
$ python application.py
Open http://127.0.0.1:8080 in your browser.
The JSON view is pretty fun. It looks very similar to our other views, which is good. In fact, the whole pattern of simply returning data from your view, and letting the machinery pass it into a renderer, provides consistency and simplicity. Plus, tests are a lot easier to write.