Defining Views¶
A view callable in a traversal -based Pyramid application is typically a simple Python function that accepts two parameters: context and request. A view callable is assumed to return a response object.
Note
A Pyramid view can also be defined as callable
which accepts only a request argument. You’ll see
this one-argument pattern used in other Pyramid tutorials
and applications. Either calling convention will work in any
Pyramid application; the calling conventions can be used
interchangeably as necessary. In traversal based applications,
URLs are mapped to a context resource, and since our
resource tree also represents our application’s
“domain model”, we’re often interested in the context, because
it represents the persistent storage of our application. For
this reason, in this tutorial we define views as callables that
accept context
in the callable argument list. If you do
need the context
within a view function that only takes
the request as a single argument, you can obtain it via
request.context
.
We’re going to define several view callable functions, then wire them into Pyramid using some view configuration.
The source code for this tutorial stage can be browsed via http://github.com/Pylons/pyramid/tree/1.2-branch/docs/tutorials/wiki/src/views/.
Declaring Dependencies in Our setup.py
File¶
The view code in our application will depend on a package which is not a
dependency of the original “tutorial” application. The original “tutorial”
application was generated by the paster create
command; it doesn’t know
about our custom application requirements. We need to add a dependency on
the docutils
package to our tutorial
package’s setup.py
file by
assigning this dependency to the install_requires
parameter in the
setup
function.
Our resulting setup.py
should look like so:
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 | import os
from setuptools import setup, find_packages
here = os.path.abspath(os.path.dirname(__file__))
README = open(os.path.join(here, 'README.txt')).read()
CHANGES = open(os.path.join(here, 'CHANGES.txt')).read()
requires = [
'pyramid',
'pyramid_zodbconn',
'pyramid_tm',
'pyramid_debugtoolbar',
'ZODB3',
'docutils',
]
setup(name='tutorial',
version='0.0',
description='tutorial',
long_description=README + '\n\n' + CHANGES,
classifiers=[
"Intended Audience :: Developers",
"Framework :: Pylons",
"Programming Language :: Python",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Internet :: WWW/HTTP :: WSGI :: Application",
],
author='',
author_email='',
url='',
keywords='web pylons pyramid',
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=requires,
tests_require=requires,
test_suite="tutorial",
entry_points = """\
[paste.app_factory]
main = tutorial:main
""",
paster_plugins=['pyramid'],
)
|
Note
After these new dependencies are added, you will need to
rerun python setup.py develop
inside the root of the
tutorial
package to obtain and register the newly added
dependency package.
Adding View Functions¶
We’re going to add four view callable functions to our views.py
module. One view named view_wiki
will display the wiki itself (it will
answer on the root URL), another named view_page
will display an
individual page, another named add_page
will allow a page to be added,
and a final view named edit_page
will allow a page to be edited.
Note
There is nothing special about the filename views.py
. A project may
have many view callables throughout its codebase in arbitrarily-named
files. Files implementing view callables often have view
in their
filenames (or may live in a Python subpackage of your application package
named views
), but this is only by convention.
The view_wiki
view function¶
The view_wiki
function will be configured to respond as the default view
callable for a Wiki resource. We’ll provide it with a @view_config
decorator which names the class tutorial.models.Wiki
as its context.
This means that when a Wiki resource is the context, and no view name
exists in the request, this view will be used. The view configuration
associated with view_wiki
does not use a renderer
because the view
callable always returns a response object rather than a dictionary.
No renderer is necessary when a view returns a response object.
The view_wiki
view callable always redirects to the URL of a Page
resource named “FrontPage”. To do so, it returns an instance of the
pyramid.httpexceptions.HTTPFound
class (instances of which implement
the pyramid.interfaces.IResponse
interface like
pyramid.response.Response
does). The
pyramid.request.Request.resource_url()
API.
pyramid.request.Request.resource_url()
constructs a URL to the
FrontPage
page resource (e.g. http://localhost:6543/FrontPage
), and
uses it as the “location” of the HTTPFound response, forming an HTTP
redirect.
The view_page
view function¶
The view_page
function will be configured to respond as the default view
of a Page resource. We’ll provide it with a @view_config
decorator which
names the class tutorial.models.Page
as its context. This means that
when a Page resource is the context, and no view name exists in the
request, this view will be used. We inform Pyramid this view will use
the templates/view.pt
template file as a renderer
.
The view_page
function generates the ReStructuredText body of a
page (stored as the data
attribute of the context passed to the view; the
context will be a Page resource) as HTML. Then it substitutes an HTML anchor
for each WikiWord reference in the rendered HTML using a compiled regular
expression.
The curried function named check
is used as the first argument to
wikiwords.sub
, indicating that it should be called to provide a value for
each WikiWord match found in the content. If the wiki (our page’s
__parent__
) already contains a page with the matched WikiWord name, the
check
function generates a view link to be used as the substitution value
and returns it. If the wiki does not already contain a page with with the
matched WikiWord name, the function generates an “add” link as the
substitution value and returns it.
As a result, the content
variable is now a fully formed bit of HTML
containing various view and add links for WikiWords based on the content of
our current page resource.
We then generate an edit URL (because it’s easier to do here than in the template), and we wrap up a number of arguments in a dictionary and return it.
The arguments we wrap into a dictionary include page
, content
, and
edit_url
. As a result, the template associated with this view callable
(via renderer=
in its configuration) will be able to use these names to
perform various rendering tasks. The template associated with this view
callable will be a template which lives in templates/view.pt
.
Note the contrast between this view callable and the view_wiki
view
callable. In the view_wiki
view callable, we unconditionally return a
response object. In the view_page
view callable, we return a
dictionary. It is always fine to return a response object from a
Pyramid view. Returning a dictionary is allowed only when there is a
renderer associated with the view callable in the view configuration.
The add_page
view function¶
The add_page
function will be configured to respond when the context
resource is a Wiki and the view name is add_page
. We’ll provide
it with a @view_config
decorator which names the string add_page
as
its view name (via name=), the class tutorial.models.Wiki
as its
context, and the renderer named templates/edit.pt
. This means that when
a Wiki resource is the context, and a view name named add_page
exists as the result of traversal, this view will be used. We inform
Pyramid this view will use the templates/edit.pt
template file as
a renderer
. We share the same template between add and edit views, thus
edit.pt
instead of add.pt
.
The add_page
function will be invoked when a user clicks on a WikiWord
which isn’t yet represented as a page in the system. The check
function
within the view_page
view generates URLs to this view. It also acts as a
handler for the form that is generated when we want to add a page resource.
The context
of the add_page
view is always a Wiki resource (not a
Page resource).
The request subpath in Pyramid is the sequence of names that
are found after the view name in the URL segments given in the
PATH_INFO
of the WSGI request as the result of traversal. If our
add view is invoked via, e.g. http://localhost:6543/add_page/SomeName
,
the subpath will be a tuple: ('SomeName',)
.
The add view takes the zeroth element of the subpath (the wiki page name), and aliases it to the name attribute in order to know the name of the page we’re trying to add.
If the view rendering is not a result of a form submission (if the
expression 'form.submitted' in request.params
is False
), the view
renders a template. To do so, it generates a “save url” which the template
use as the form post URL during rendering. We’re lazy here, so we’re trying
to use the same template (templates/edit.pt
) for the add view as well as
the page edit view. To do so, we create a dummy Page resource object in
order to satisfy the edit form’s desire to have some page object exposed as
page
, and we’ll render the template to a response.
If the view rendering is a result of a form submission (if the expression
'form.submitted' in request.params
is True
), we scrape the page body
from the form data, create a Page object using the name in the subpath and
the page body, and save it into “our context” (the Wiki) using the
__setitem__
method of the context. We then redirect back to the
view_page
view (the default view for a page) for the newly created page.
The edit_page
view function¶
The edit_page
function will be configured to respond when the context is
a Page resource and the view name is edit_page
. We’ll provide it
with a @view_config
decorator which names the string edit_page
as its
view name (via name=
), the class tutorial.models.Page
as its
context, and the renderer named templates/edit.pt
. This means that when
a Page resource is the context, and a view name exists as the result
of traversal named edit_page
, this view will be used. We inform
Pyramid this view will use the templates/edit.pt
template file as
a renderer
.
The edit_page
function will be invoked when a user clicks the “Edit this
Page” button on the view form. It renders an edit form but it also acts as
the form post view callable for the form it renders. The context
of the
edit_page
view will always be a Page resource (never a Wiki resource).
If the view execution is not a result of a form submission (if the
expression 'form.submitted' in request.params
is False
), the view
simply renders the edit form, passing the page resource, and a save_url
which will be used as the action of the generated form.
If the view execution is a result of a form submission (if the expression
'form.submitted' in request.params
is True
), the view grabs the
body
element of the request parameter and sets it as the data
attribute of the page context. It then redirects to the default view of the
context (the page), which will always be the view_page
view.
Viewing the Result of all Our Edits to views.py
¶
The result of all of our edits to views.py
will leave it looking like
this:
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 | from docutils.core import publish_parts
import re
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
from tutorial.models import Page
# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
@view_config(context='tutorial.models.Wiki')
def view_wiki(context, request):
return HTTPFound(location=request.resource_url(context, 'FrontPage'))
@view_config(context='tutorial.models.Page',
renderer='tutorial:templates/view.pt')
def view_page(context, request):
wiki = context.__parent__
def check(match):
word = match.group(1)
if word in wiki:
page = wiki[word]
view_url = request.resource_url(page)
return '<a href="%s">%s</a>' % (view_url, word)
else:
add_url = request.application_url + '/add_page/' + word
return '<a href="%s">%s</a>' % (add_url, word)
content = publish_parts(context.data, writer_name='html')['html_body']
content = wikiwords.sub(check, content)
edit_url = request.resource_url(context, 'edit_page')
return dict(page = context, content = content, edit_url = edit_url)
@view_config(name='add_page', context='tutorial.models.Wiki',
renderer='tutorial:templates/edit.pt')
def add_page(context, request):
name = request.subpath[0]
if 'form.submitted' in request.params:
body = request.params['body']
page = Page(body)
page.__name__ = name
page.__parent__ = context
context[name] = page
return HTTPFound(location = request.resource_url(page))
save_url = request.resource_url(context, 'add_page', name)
page = Page('')
page.__name__ = name
page.__parent__ = context
return dict(page = page, save_url = save_url)
@view_config(name='edit_page', context='tutorial.models.Page',
renderer='tutorial:templates/edit.pt')
def edit_page(context, request):
if 'form.submitted' in request.params:
context.data = request.params['body']
return HTTPFound(location = request.resource_url(context))
return dict(page = context,
save_url = request.resource_url(context, 'edit_page'))
|
Adding Templates¶
Most view callables we’ve added expected to be rendered via a template. The default templating systems in Pyramid are Chameleon and Mako. Chameleon is a variant of ZPT, which is an XML-based templating language. Mako is a non-XML-based templating language. Because we had to pick one, we chose Chameleon for this tutorial.
The templates we create will live in the templates
directory of our
tutorial package. Chameleon templates must have a .pt
extension to be
recognized as such.
The view.pt
Template¶
The view.pt
template is used for viewing a single Page. It is used by
the view_page
view function. It should have a div that is “structure
replaced” with the content
value provided by the view. It should also
have a link on the rendered page that points at the “edit” URL (the URL which
invokes the edit_page
view for the page being viewed).
Once we’re done with the view.pt
template, it will look a lot like
the below:
<!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" xml:lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<head>
<title>${page.__name__} - Pyramid tutorial wiki (based on
TurboGears 20-Minute Wiki)</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<meta name="keywords" content="python web application" />
<meta name="description" content="pyramid web application" />
<link rel="shortcut icon"
href="${request.static_url('tutorial:static/favicon.ico')}" />
<link rel="stylesheet"
href="${request.static_url('tutorial:static/pylons.css')}"
type="text/css" media="screen" charset="utf-8" />
<!--[if lte IE 6]>
<link rel="stylesheet"
href="${request.static_url('tutorial:static/ie6.css')}"
type="text/css" media="screen" charset="utf-8" />
<![endif]-->
</head>
<body>
<div id="wrap">
<div id="top-small">
<div class="top-small align-center">
<div>
<img width="220" height="50" alt="pyramid"
src="${request.static_url('tutorial:static/pyramid-small.png')}" />
</div>
</div>
</div>
<div id="middle">
<div class="middle align-right">
<div id="left" class="app-welcome align-left">
Viewing <b><span tal:replace="page.__name__">Page Name Goes
Here</span></b><br/>
You can return to the
<a href="${request.application_url}">FrontPage</a>.<br/>
</div>
<div id="right" class="app-welcome align-right"></div>
</div>
</div>
<div id="bottom">
<div class="bottom">
<div tal:replace="structure content">
Page text goes here.
</div>
<p>
<a tal:attributes="href edit_url" href="">
Edit this page
</a>
</p>
</div>
</div>
</div>
<div id="footer">
<div class="footer"
>© Copyright 2008-2011, Agendaless Consulting.</div>
</div>
</body>
</html>
Note
The names available for our use in a template are always those that
are present in the dictionary returned by the view callable. But our
templates make use of a request
object that none of our tutorial views
return in their dictionary. This value appears as if “by magic”.
However, request
is one of several names that are available “by
default” in a template when a template renderer is used. See
*.pt or *.txt: Chameleon Template Renderers for more information about other names
that are available by default in a template when a template is used as a
renderer.
The edit.pt
Template¶
The edit.pt
template is used for adding and editing a Page. It is used
by the add_page
and edit_page
view functions. It should display a
page containing a form that POSTs back to the “save_url” argument supplied by
the view. The form should have a “body” textarea field (the page data), and
a submit button that has the name “form.submitted”. The textarea in the form
should be filled with any existing page data when it is rendered.
Once we’re done with the edit.pt
template, it will look a lot like the
below:
<!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" xml:lang="en"
xmlns:tal="http://xml.zope.org/namespaces/tal">
<head>
<title>${page.__name__} - Pyramid tutorial wiki (based on
TurboGears 20-Minute Wiki)</title>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
<meta name="keywords" content="python web application" />
<meta name="description" content="pyramid web application" />
<link rel="shortcut icon"
href="${request.static_url('tutorial:static/favicon.ico')}" />
<link rel="stylesheet"
href="${request.static_url('tutorial:static/pylons.css')}"
type="text/css" media="screen" charset="utf-8" />
<!--[if lte IE 6]>
<link rel="stylesheet"
href="${request.static_url('tutorial:static/ie6.css')}"
type="text/css" media="screen" charset="utf-8" />
<![endif]-->
</head>
<body>
<div id="wrap">
<div id="top-small">
<div class="top-small align-center">
<div>
<img width="220" height="50" alt="pyramid"
src="${request.static_url('tutorial:static/pyramid-small.png')}" />
</div>
</div>
</div>
<div id="middle">
<div class="middle align-right">
<div id="left" class="app-welcome align-left">
Editing <b><span tal:replace="page.__name__">Page Name Goes
Here</span></b><br/>
You can return to the
<a href="${request.application_url}">FrontPage</a>.<br/>
</div>
<div id="right" class="app-welcome align-right"></div>
</div>
</div>
<div id="bottom">
<div class="bottom">
<form action="${save_url}" method="post">
<textarea name="body" tal:content="page.data" rows="10"
cols="60"/><br/>
<input type="submit" name="form.submitted" value="Save"/>
</form>
</div>
</div>
</div>
<div id="footer">
<div class="footer"
>© Copyright 2008-2011, Agendaless Consulting.</div>
</div>
</body>
</html>
Static Assets¶
Our templates name a single static asset named pylons.css
. We don’t need
to create this file within our package’s static
directory because it was
provided at the time we created the project. This file is a little too long to
replicate within the body of this guide, however it is available online.
This CSS file will be accessed via
e.g. http://localhost:6543/static/pylons.css
by virtue of the call to
add_static_view
directive we’ve made in the __init__.py
file. Any
number and type of static assets can be placed in this directory (or
subdirectories) and are just referred to by URL or by using the convenience
method static_url
e.g. request.static_url('{{package}}:static/foo.css')
within templates.
Viewing the Application in a Browser¶
We can finally examine our application in a browser. The views we’ll try are as follows:
- Visiting
http://localhost:6543/
in a browser invokes theview_wiki
view. This always redirects to theview_page
view of theFrontPage
Page resource. - Visiting
http://localhost:6543/FrontPage/
in a browser invokes theview_page
view of the front page resource. This is because it’s the default view (a view without aname
) for Page resources. - Visiting
http://localhost:6543/FrontPage/edit_page
in a browser invokes the edit view for theFrontPage
Page resource. - Visiting
http://localhost:6543/add_page/SomePageName
in a browser invokes the add view for a Page. - To generate an error, visit
http://localhost:6543/add_page
which will generate anIndexError
for the expressionrequest.subpath[0]
. You’ll see an interactive traceback facility provided by pyramid_debugtoolbar.