1. Blaise Laflamme
  2. PylonsWikiNG


PylonsWikiNG / definingviews.rst

Defining Views

A :term:`view callable` in a :term:`url dispatch` -based :mod:`repoze.bfg` application is typically a simple Python function that accepts a single parameter named :term:`request`. A view callable is assumed to return a :term:`response` object.


A :mod:`repoze.bfg` view can also be defined as callable which accepts two arguments: a :term:`context` and a :term:`request`. You'll see this two-argument pattern used in other :mod:`repoze.bfg` tutorials and applications. Either calling convention will work in any :mod:`repoze.bfg` application; the calling conventions can be used interchangeably as necessary. In :term:`url dispatch` based applications, however, the context object is rarely used in the view body itself, so within this tutorial we define views as callables that accept only a request to avoid the visual "noise". 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.

The request passed to every view that is called as the result of a route match has an attribute named matchdict that contains the elements placed into the URL by the path of a route statement. For instance, if a route statement in configure.zcml had the path :one/:two, and the URL at http://example.com/foo/bar was invoked, matching this path, the matchdict dictionary attached to the request passed to the view would have a one key with the value foo and a two key with the value bar.

The source code for this tutorial stage can be browsed at docs.repoze.org.

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:


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'll get rid of our my_view view function in our views.py file. It's only an example and isn't relevant to our application.

Then we're going to add four :term:`view callable` functions to our views.py module. One view callable (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 callable named edit_page will allow a page to be edited. We'll describe each one briefly and show the resulting views.py file afterward.


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 respond as the :term:`default view` of a Wiki model object. It always redirects to a URL which represents the path to our "FrontPage". It returns an instance of the :class:`webob.exc.HTTPFound` class (instances of which implement the WebOb :term:`response` interface), It will use the :func:`repoze.bfg.url.route_url` API to construct a URL to the FrontPage page (e.g. http://localhost:6543/FrontPage), and will use it as the "location" of the HTTPFound response, forming an HTTP redirect.

The view_page view function

The view_page function will respond as the :term:`default view` of a Page object. The view_page function renders the :term:`ReStructuredText` body of a page (stored as the data attribute of a Page object) 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 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 object.

We then generate an edit URL (because it's easier to do here than in the template), and we return a dictionary with a number of arguments. The fact that this view returns a dictionary (as opposed to a :term:`response` object) is a cue to :mod:`repoze.bfg` that it should try to use a :term:`renderer` associated with the view configuration to render a template. In our case, the template which will be rendered will be the templates/view.pt template, as per the configuration put into effect in configure.zcml.

The add_page view function

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 object. The matchdict attribute of the request passed to the add_page view will have the values we need to construct URLs and find model objects.

The matchdict will have a pagename key that matches the name of the page we'd like to add. If our add view is invoked via, e.g. http://localhost:6543/add_page/SomeName, the pagename value in the matchdict will be SomeName.

If the view execution is not a result of a form submission (if the expression 'form.submitted' in request.params is False), the view callable 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, so we create a dummy Page object in order to satisfy the edit form's desire to have some page object exposed as page, and :mod:`repoze.bfg` will render the template associated with this view to a response.

If the view execution 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 matchdict pagename, and obtain the page body from the request, and save it into the database using session.add. We then redirect back to the view_page view (the :term:`default view` for a Page) for the newly created page.

The edit_page view function

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 handler for the form it renders. The matchdict attribute of the request passed to the add_page view will have a pagename key matching the name of the page the user wants to edit.

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 request, the page object, 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 key in the matchdict. It then redirects to the default view of the wiki page, which will always be the view_page view.

Viewing the Result of Our Edits to views.py

The result of all of our edits to views.py will leave it looking like this:

Adding Templates

The views we've added all reference a :term:`template`. Each template is a :term:`Chameleon` template. The default templating system in :mod:`repoze.bfg` is a variant of :term:`ZPT` provided by :term:`Chameleon`. These templates will live in the templates directory of our tutorial package.

The view.pt Template

The view.pt template is used for viewing a single wiki 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:


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 :ref:`chameleon_template_renderers` for more information about other names that are available by default in a template when a Chameleon template is used as a renderer.

The edit.pt Template

The edit.pt template is used for adding and editing a wiki 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:

Static Resources

Our templates name a single static resource named style.css. We need to create this and place it in a file named style.css within our package's templates/static directory. 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/style.css by virtue of the <static> directive we've defined in the configure.zcml file. Any number and type of static resources can be placed in this directory (or subdirectories) and are just referred to by URL within templates.

Mapping Views to URLs in configure.zcml

The configure.zcml file contains route declarations (and a lone view declaration) which serve to map URLs via :term:`url dispatch` to view functions. First, we’ll get rid of the existing route created by the template using the name home. It’s only an example and isn’t relevant to our application.

We then need to add four route declarations to configure.zcml. Note that the ordering of these declarations is very important. route declarations are matched in the order they're found in the configure.zcml file.

  1. Add a declaration which maps the empty path (signifying the root URL) to the view named view_wiki in our views.py file with the name view_wiki. This is the :term:`default view` for the wiki.
  2. Add a declaration which maps the path pattern :pagename to the view named view_page in our views.py file with the view name view_page. This is the regular view for a page.
  3. Add a declaration which maps the path pattern :pagename/edit_page to the view named edit_page in our views.py file with the name edit_page. This is the edit view for a page.
  4. Add a declaration which maps the path pattern add_page/:pagename to the view named add_page in our views.py file with the name add_page. This is the add view for a new page.

As a result of our edits, the configure.zcml file should look something like so:

The WSGI Pipeline

Within tutorial.ini, note the existence of a [pipeline:main] section which specifies our WSGI pipeline. This "pipeline" will be served up as our WSGI application. As far as the WSGI server is concerned the pipeline is our application. Simpler configurations don't use a pipeline: instead they expose a single WSGI application as "main". Our setup is more complicated, so we use a pipeline.

egg:repoze.tm2#tm is at the "top" of the pipeline. This is a piece of middleware which commits a transaction if no exception occurs; if an exception occurs, the transaction will be aborted. This is the piece of software that allows us to forget about needing to do manual commits and aborts of our database connection in view code.

Adding an Element to the Pipeline

Let's add a piece of middleware to the WSGI pipeline. We'll add egg:Paste#evalerror middleware which displays debuggable errors in the browser while you're developing (this is not recommended for deployment as it is a security risk). Let's insert evalerror into the pipeline right above egg:repoze.tm2#tm, making our resulting tutorial.ini file look like so:

Viewing the Application in a Browser

Once we've set up the WSGI pipeline properly, 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 the view_wiki view. This always redirects to the view_page view of the FrontPage page object.
  • Visiting http://localhost:6543/FrontPage in a browser invokes the view_page view of the front page page object.
  • Visiting http://localhost:6543/FrontPage/edit_page in a browser invokes the edit view for the front page object.
  • Visiting http://localhost:6543/add_page/SomePageName in a browser invokes the add view for a page.

Try generating an error within the body of a view by adding code to the top of it that generates an exception (e.g. raise Exception('Forced Exception')). Then visit the error-raising view in a browser. You should see an interactive exception handler in the browser which allows you to examine values in a post-mortem mode.

Adding Tests

Since we've added a good bit of imperative code here, it's useful to define tests for the views we've created. We'll change our tests.py module to look like this:

We can then run the tests using something like:

The expected output is something like: