We have now seen how to create fake content in a hierarchy. Let’s hook this up with the UX we finished with in Step 10: Re-usable Template Macros.
In particular, let’s make a series of screens that let you add new people, new companies, new projects, and new folders and documents inside projects and folders.
Along the way we do a lot of refactoring. Since we are accumulating a lot of code, we will dial back a bit on the tests.
$ cd ../../resources; mkdir step05; cd step05
Copy the following into step05/application.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | from pyramid.config import Configurator
from wsgiref.simple_server import make_server
from resources import bootstrap
def main():
config = Configurator(root_factory=bootstrap)
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(host='0.0.0.0', port=8080, app=app)
server.serve_forever()
|
$ mkdir static
Copy the following into step05/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 | 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;
}
#message {
background-color: yellow;
text-align: center;
width: 100px;
margin-left: 100px;
}
|
Copy the following into step05/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 step05/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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | from random import randint
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from layouts import Layouts
from resources import Company
from resources import Document
from resources import Folder
from resources import People
from resources import Person
from resources import Project
from resources import Site
class ProjectorViews(Layouts):
def __init__(self, context, request):
self.context = context
self.request = request
def _add_container(self, klass):
title = self.request.params['title']
name = str(randint(0, 10000))
self.context[name] = klass(name, self.context, title)
url = self.request.resource_url(self.context,
query={'msg': 'Added'})
return HTTPFound(location=url)
def _add_document(self, klass=Document):
title = self.request.params['title']
name = str(randint(0, 10000))
self.context[name] = klass(name, self.context, title,
'<p>Default</p>')
url = self.request.resource_url(self.context,
query={'msg': 'Added'})
return HTTPFound(location=url)
@view_config(renderer="templates/site.pt", context=Site)
def site_view(self):
if 'submit' in self.request.POST:
return self._add_container(Company)
return {"page_title": "Home"}
@view_config(renderer="templates/company.pt", context=Company)
def company_view(self):
if 'submit' in self.request.POST:
return self._add_container(Project)
return {"page_title": self.context.title}
@view_config(renderer="templates/project.pt", context=Project)
def project_view(self):
if 'folder' in self.request.POST:
return self._add_container(Folder)
if 'document' in self.request.POST:
return self._add_document()
return {"page_title": self.context.title}
@view_config(renderer="templates/folder.pt", context=Folder)
def folder_view(self):
if 'folder' in self.request.POST:
return self._add_container(Folder)
if 'document' in self.request.POST:
return self._add_document()
return {"page_title": self.context.title}
@view_config(renderer="templates/document.pt", context=Document)
def document_view(self):
return {"page_title": self.context.title}
@view_config(renderer="templates/people.pt", context=People)
def people_view(self):
if 'submit' in self.request.POST:
return self._add_document(Person)
return {"page_title": self.context.title}
@view_config(renderer="templates/person.pt", context=Person)
def person_view(self):
return {"page_title": self.context.title}
@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 step05/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 33 34 35 36 37 38 39 40 41 42 43 44 45 | from pyramid.decorator import reify
from pyramid.location import lineage
from pyramid.renderers import get_renderer
from resources import Company
from resources import Site
class Layouts(object):
@reify
def global_template(self):
renderer = get_renderer("templates/global_layout.pt")
return renderer.implementation().macros['layout']
@reify
def global_macros(self):
renderer = get_renderer("templates/macros.pt")
return renderer.implementation().macros
@reify
def site(self):
# From somewhere deep in hierarchy, reach up and grab site
for l in lineage(self.context):
if isinstance(l, Site):
return l
return None
@reify
def company(self):
# From somewhere deep in hierarchy, reach up and grab company
for l in lineage(self.context):
if isinstance(l, Company):
return l
return None
@reify
def message(self):
return self.request.GET.get('msg', None)
@reify
def site_menu(self):
new_menu = []
for c in [self.site,] + self.site.values():
url = self.request.resource_url(c)
new_menu.append({'href': url, 'title': c.title})
return new_menu
|
Copy the following into step05/resources.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 | class Folder(dict):
def __init__(self, name, parent, title):
self.__name__ = name
self.__parent__ = parent
self.title = title
class Document(object):
def __init__(self, name, parent, title, body):
self.__name__ = name
self.__parent__ = parent
self.title = title
self.body = body
class Site(Folder):
def bootstrap(self):
# Document
body = "<p>Project is a <em>project management system.</p>"
root['about'] = Document('about', root, 'About Projector', body)
# Some people
people = People('people', root, 'People')
root['people'] = people
people['sstanton'] = Person('sstanton', people, 'Susan Stanton',
'<p>Hello <em>Susan bio<em></p>')
people['bbarker'] = Person('bbarker', people, 'Bob Barker',
'<p>The <em>Bob bio</em> goes here</p>')
# Some companies and projects and docs
acme = Company('acme', root, 'ACME, Inc.')
root['acme'] = acme
project01 = Project('project01', acme, 'Project 01')
acme['project01'] = project01
project02 = Project('project02', acme, 'Project 02')
acme['project02'] = project02
project01['doc1'] = Document('doc1', project01, 'Document 01',
'<p>Some doc of <em>stuff</em></p>')
project01['doc2'] = Document('doc2', project01, 'Document 02',
'<p>More <em>stuff</em></p>')
folder1 = Folder('folder1', project01, 'Folder 1')
project01['folder1'] = folder1
folder1['doc3'] = Document('doc3', folder1, 'Document 3',
'<p>A <em>really</em> deep down doc')
class People(Folder):
pass
class Person(Document):
pass
class Company(Folder):
pass
class Project(Folder):
pass
root = Site('', None, 'Home')
root.bootstrap()
def bootstrap(request):
return root
|
Copy the following into step05/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 | <!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">
<span><a href="${menu.href}">${menu.title}</a></span>
</li>
</ul>
</div>
<div id="sidebar">
<h2>Updates</h2>
<ul></ul>
<p><a href="#">reload</a></p>
</div>
<tal:block condition="view.message">
<p id="message" tal:content="view.message">Message</p>
</tal:block>
<div id="main"><h1>${page_title}</h1>
<div metal:define-slot="content">
</div>
</div>
</body>
</html>
|
Copy the following into step05/templates/macros.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 | <!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 tal:condition="view.company" id="company_menu">
<li>Projects:</li>
<li tal:repeat="child view.company.values()">
<a href="${view.request.resource_url(child)}">${child.title}</a>
</li>
</ul>
</metal:company_menu>
<metal:children_listing define-macro="children_listing">
<tal:block condition="view.context.values() == []">
<em>No items.</em>
</tal:block>
<tal:block condition="view.context.values() != []">
<ul>
<li tal:repeat="child view.context.values()">
<a href="${view.request.resource_url(child)}">${child.title}</a>
</li>
</ul>
</tal:block>
</metal:children_listing>
</body>
</html>
|
Copy the following into step05/templates/company.pt:
1 2 3 4 5 6 7 8 9 10 11 12 | <div metal:use-macro="view.global_template">
<div metal:fill-slot="content">
<p>Welcome to our company project management system.</p>
<form action="." method="POST">
<label for="title">Title:</label>
<input name="title"/>
<input name="submit" type="submit" value="Add Project"/>
</form>
<div metal:use-macro="view.global_macros['company_menu']"></div>
</div>
</div>
|
Copy the following into step05/templates/document.pt:
1 2 3 4 5 6 7 | <div metal:use-macro="view.global_template">
<div metal:fill-slot="content">
<div tal:replace="structure view.context.body"/>
<div metal:use-macro="view.global_macros['company_menu']"></div>
</div>
</div>
|
Copy the following into step05/templates/folder.pt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <div metal:use-macro="view.global_template">
<div metal:fill-slot="content">
<h2>Folder Contents</h2>
<div metal:use-macro="view.global_macros['children_listing']"></div>
<form action="." method="POST">
<label for="title">Title:</label>
<input name="title"/>
<input name="folder" type="submit" value="Add Folder"/>
</form>
<form action="." method="POST">
<label for="title">Title:</label>
<input name="title"/>
<input name="document" type="submit" value="Add Document"/>
</form>
<div metal:use-macro="view.global_macros['company_menu']"></div>
</div>
</div>
|
Copy the following into step05/templates/people.pt:
1 2 3 4 5 6 7 8 9 10 | <div metal:use-macro="view.global_template">
<div metal:fill-slot="content">
<div metal:use-macro="view.global_macros['children_listing']"></div>
<form action="." method="POST">
<label for="title">Title:</label>
<input name="title"/>
<input name="submit" type="submit" value="Add Person"/>
</form>
</div>
</div>
|
Copy the following into step05/templates/person.pt:
1 2 3 4 5 | <div metal:use-macro="view.global_template">
<div metal:fill-slot="content">
<div tal:replace="structure view.context.body"/>
</div>
</div>
|
Copy the following into step05/templates/project.pt:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <div metal:use-macro="view.global_template">
<div metal:fill-slot="content">
<h2>Project Content</h2>
<div metal:use-macro="view.global_macros['children_listing']"></div>
<form action="." method="POST">
<label for="title">Title:</label>
<input name="title"/>
<input name="folder" type="submit" value="Add Folder"/>
</form>
<form action="." method="POST">
<label for="title">Title:</label>
<input name="title"/>
<input name="document" type="submit" value="Add Document"/>
</form>
<div metal:use-macro="view.global_macros['company_menu']"></div>
</div>
</div>
|
Copy the following into step05/templates/site.pt:
1 2 3 4 5 6 7 8 9 10 | <div metal:use-macro="view.global_template">
<div metal:fill-slot="content">
<p>Home page content goes here.</p>
<form action="." method="POST">
<label for="title">Title:</label>
<input name="title"/>
<input name="submit" type="submit" value="Add Company"/>
</form>
</div>
</div>
|
Copy the following into step05/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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 | import unittest
from pyramid.testing import DummyRequest
from pyramid.testing import setUp
from pyramid.testing import tearDown
class DummyContext(object):
title = "Dummy Context"
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
context = DummyContext()
inst = ProjectorViews(context, request)
return inst
def test_site_view(self):
request = DummyRequest()
inst = self._makeOne(request)
result = inst.site_view()
self.assertEqual(result['page_title'], 'Home')
def test_company_view(self):
request = DummyRequest()
inst = self._makeOne(request)
result = inst.company_view()
self.assertEqual(result["page_title"], "Dummy Context")
def test_project_view(self):
request = DummyRequest()
inst = self._makeOne(request)
result = inst.project_view()
self.assertEqual(result["page_title"], "Dummy Context")
def test_folder_view(self):
request = DummyRequest()
inst = self._makeOne(request)
result = inst.folder_view()
self.assertEqual(result["page_title"], "Dummy Context")
def test_document_view(self):
request = DummyRequest()
inst = self._makeOne(request)
result = inst.document_view()
self.assertEqual(result["page_title"], "Dummy Context")
def test_people_view(self):
request = DummyRequest()
inst = self._makeOne(request)
result = inst.people_view()
self.assertEqual(result["page_title"], "Dummy Context")
def test_person_view(self):
request = DummyRequest()
inst = self._makeOne(request)
result = inst.person_view()
self.assertEqual(result["page_title"], "Dummy Context")
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('Home page content' in res.body)
res = self.testapp.get('/about', status=200)
self.assertTrue('project management system' in res.body)
res = self.testapp.get('/acme', status=200)
self.assertTrue('our company' in res.body)
res = self.testapp.get('/acme/project01', status=200)
self.assertTrue('Project Content' in res.body)
res = self.testapp.get('/acme/project01/folder1', status=200)
self.assertTrue('Folder Contents' in res.body)
res = self.testapp.get('/acme/project01/folder1/doc3',
status=200)
self.assertTrue('deep down' in res.body)
res = self.testapp.get('/people', status=200)
self.assertTrue('Add Person' in res.body)
res = self.testapp.get('/people/bbarker', status=200)
self.assertTrue('goes here' in res.body)
res = self.testapp.get('/updates.json', status=200)
self.assertTrue('888' in res.body)
|
$ nosetests should report running 10 tests.
$ python application.py
Open http://127.0.0.1:8080/ in your browser.
Our site menu is no longer hard coded. As you add Company resources to the SiteFolder, they will automatically appear in the menu.
All of the container views have templates with one or more <form> nodes in them. These let us quickly add a particular type of resource to a container. We don’t make these into a macro because the name on submit button guides us to which kind of thing to add.
We are using self-posting forms in the views. That is, the same view acts both as a GET and a POST handler. If you post data to the view, we create a resource then redirect back to the GET view, but with a message to be displayed.
We could have repeated a lot of the boilerplate on content creation in each view. That means a lot more tests to write. Instead, we made two “factory” functions. You pass in the class of the resource you want created. The factory returns the redirect information.
layouts.py gained some helper functions used to look up the hierarchy. (We put these in the layout because it is needed by the layout macros.) Walking up to find the site, or walking up to find which company you are in, are common operations.
Listing items in a container is a repetitive task, so we made a macro for it so we could use it in various templates.