.. _wiki_defining_views:
|
|
==============
|
Defining Views
|
==============
|
|
A :term:`view callable` in a :term:`traversal`-based :app:`Pyramid`
|
application is typically a simple Python function that accepts two
|
parameters: :term:`context` and :term:`request`. A view callable is
|
assumed to return a :term:`response` object.
|
|
.. note::
|
|
A :app:`Pyramid` view can also be defined as callable
|
which accepts *only* a :term:`request` argument. You'll see
|
this one-argument pattern used in other :app:`Pyramid` tutorials
|
and applications. Either calling convention will work in any
|
:app:`Pyramid` application; the calling conventions can be used
|
interchangeably as necessary. In :term:`traversal`-based applications,
|
URLs are mapped to a context :term:`resource`, and since our
|
:term:`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 :term:`view callable` functions, then wire them
|
into :app:`Pyramid` using some :term:`view configuration`.
|
|
|
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 cookiecutter; 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 ``requires``
|
parameter in the ``setup()`` function.
|
|
Open ``setup.py`` and edit it to look like the following:
|
|
.. literalinclude:: src/views/setup.py
|
:linenos:
|
:emphasize-lines: 22
|
:language: python
|
|
Only the highlighted line needs to be added.
|
|
.. _wiki-running-pip-install:
|
|
Running ``pip install -e .``
|
============================
|
|
Since a new software dependency was added, you will need to run ``pip install
|
-e .`` again inside the root of the ``tutorial`` package to obtain and register
|
the newly added dependency distribution.
|
|
Make sure your current working directory is the root of the project (the
|
directory in which ``setup.py`` lives) and execute the following command.
|
|
On UNIX:
|
|
.. code-block:: bash
|
|
$ cd tutorial
|
$ $VENV/bin/pip install -e .
|
|
On Windows:
|
|
.. code-block:: doscon
|
|
c:\> cd tutorial
|
c:\tutorial> %VENV%\Scripts\pip install -e .
|
|
Success executing this command will end with a line to the console something
|
like:
|
|
.. code-block:: text
|
|
Successfully installed docutils-0.13.1 tutorial
|
|
|
Adding view functions in ``views.py``
|
=====================================
|
|
It's time for a major change. Open ``tutorial/views.py`` and edit it to look
|
like the following:
|
|
.. literalinclude:: src/views/tutorial/views.py
|
:linenos:
|
:language: python
|
|
We added some imports and created a regular expression to find "WikiWords".
|
|
We got rid of the ``my_view`` view function and its decorator that was added
|
when we originally rendered the ``zodb`` cookiecutter. It was only an example and
|
isn't relevant to our application.
|
|
Then we added four :term:`view callable` functions to our ``views.py``
|
module:
|
|
* ``view_wiki()`` - Displays the wiki itself. It will answer on the root URL.
|
* ``view_page()`` - Displays an individual page.
|
* ``add_page()`` - Allows the user to add a page.
|
* ``edit_page()`` - Allows the user to edit a page.
|
|
We'll describe each one briefly in the following sections.
|
|
.. 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
|
-------------------------------
|
|
Following is the code for the ``view_wiki`` view function and its decorator:
|
|
.. literalinclude:: src/views/tutorial/views.py
|
:lines: 12-14
|
:lineno-match:
|
:language: python
|
|
.. note:: In our code, we use an *import* that is *relative* to our package
|
named ``tutorial``, meaning we can omit the name of the package in the
|
``import`` and ``context`` statements. In our narrative, however, we refer
|
to a *class* and thus we use the *absolute* form, meaning that the name of
|
the package is included.
|
|
``view_wiki()`` is the :term:`default view` that gets called when a request is
|
made to the root URL of our wiki. It always redirects to an URL which
|
represents the path to our "FrontPage".
|
|
We 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 :term:`view name` exists in the request, then this view
|
will be used. The view configuration associated with ``view_wiki`` does not
|
use a ``renderer`` because the view callable always returns a :term:`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
|
:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement
|
the :class:`pyramid.interfaces.IResponse` interface, like
|
:class:`pyramid.response.Response` does). It uses the
|
:meth:`pyramid.request.Request.route_url` API to construct an URL to the
|
``FrontPage`` page resource (i.e., ``http://localhost:6543/FrontPage``), and
|
uses it as the "location" of the ``HTTPFound`` response, forming an HTTP
|
redirect.
|
|
The ``view_page`` view function
|
-------------------------------
|
|
Here is the code for the ``view_page`` view function and its decorator:
|
|
.. literalinclude:: src/views/tutorial/views.py
|
:lines: 16-33
|
:lineno-match:
|
:language: python
|
|
The ``view_page`` function is configured to respond as the default view
|
of a Page resource. We 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 :term:`view name` exists in the
|
request, this view will be used. We inform :app:`Pyramid` this view will use
|
the ``templates/view.pt`` template file as a ``renderer``.
|
|
The ``view_page`` function generates the :term:`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 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
|
:term:`response` object. In the ``view_page`` view callable, we return a
|
*dictionary*. It is *always* fine to return a :term:`response` object from a
|
:app:`Pyramid` view. Returning a dictionary is allowed only when there is a
|
:term:`renderer` associated with the view callable in the view configuration.
|
|
The ``add_page`` view function
|
------------------------------
|
|
Here is the code for the ``add_page`` view function and its decorator:
|
|
.. literalinclude:: src/views/tutorial/views.py
|
:lines: 35-50
|
:lineno-match:
|
:language: python
|
|
The ``add_page`` function is configured to respond when the context resource
|
is a Wiki and the :term:`view name` is ``add_page``. We provide it with a
|
``@view_config`` decorator which names the string ``add_page`` as its
|
:term:`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 :term:`view name` named ``add_page``
|
exists as the result of traversal, this view will be used. We inform
|
:app:`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 :term:`subpath` in :app:`Pyramid` is the sequence of names that
|
are found *after* the :term:`view name` in the URL segments given in the
|
``PATH_INFO`` of the WSGI request as the result of :term:`traversal`. If our
|
add view is invoked via, e.g., ``http://localhost:6543/add_page/SomeName``,
|
the :term:`subpath` will be a tuple: ``('SomeName',)``.
|
|
The add view takes the zero\ :sup:`th` 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
|
uses 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 grab 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
|
-------------------------------
|
|
Here is the code for the ``edit_page`` view function and its decorator:
|
|
.. literalinclude:: src/views/tutorial/views.py
|
:lines: 52-60
|
:lineno-match:
|
:language: python
|
|
The ``edit_page`` function is configured to respond when the context is
|
a Page resource and the :term:`view name` is ``edit_page``. We provide it
|
with a ``@view_config`` decorator which names the string ``edit_page`` as its
|
:term:`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 :term:`view name` exists as the result
|
of traversal named ``edit_page``, this view will be used. We inform
|
:app:`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.
|
|
Adding templates
|
================
|
|
The ``view_page``, ``add_page`` and ``edit_page`` views that we've added
|
reference a :term:`template`. Each template is a :term:`Chameleon`
|
:term:`ZPT` template. These templates 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
|
------------------------
|
|
Rename ``tutorial/templates/mytemplate.pt`` to ``tutorial/templates/view.pt`` and edit the emphasized lines to look like the following:
|
|
.. literalinclude:: src/views/tutorial/templates/view.pt
|
:linenos:
|
:language: html
|
:emphasize-lines: 11-12,37-52
|
|
This template is used by ``view_page()`` for displaying a single
|
wiki page. It includes:
|
|
- A ``div`` element that is replaced with the ``content`` value provided by
|
the view (lines 37-39). ``content`` contains HTML, so the ``structure``
|
keyword is used to prevent escaping it (i.e., changing ">" to ">", etc.)
|
- A link that points at the "edit" URL which invokes the ``edit_page`` view
|
for the page being viewed (lines 41-43).
|
|
The ``edit.pt`` template
|
------------------------
|
|
Copy ``tutorial/templates/view.pt`` to ``tutorial/templates/edit.pt`` and edit the emphasized lines to look like the following:
|
|
.. literalinclude:: src/views/tutorial/templates/edit.pt
|
:linenos:
|
:language: html
|
|
This template is used by ``add_page()`` and ``edit_page()`` for adding and
|
editing a wiki page. It displays a page containing a form that includes:
|
|
- A 10-row by 60-column ``textarea`` field named ``body`` that is filled
|
with any existing page data when it is rendered (line 46).
|
- A submit button that has the name ``form.submitted`` (line 49).
|
|
The form POSTs back to the ``save_url`` argument supplied by the view (line
|
44). The view will use the ``body`` and ``form.submitted`` values.
|
|
.. note:: Our templates use a ``request`` object that none of our tutorial
|
views return in their dictionary. ``request`` is one of several names that
|
are available "by default" in a template when a template renderer is used.
|
See :ref:`renderer_system_values` for information about other names that
|
are available by default when a template is used as a renderer.
|
|
|
Static assets
|
-------------
|
|
Our templates name static assets, including CSS and images. We don't need
|
to create these files within our package's ``static`` directory because they
|
were provided at the time we created the project.
|
|
As an example, the CSS file will be accessed via
|
``http://localhost:6543/static/theme.css`` by virtue of the call to the
|
``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 (See
|
:ref:`wiki-start-the-application`). Launch a browser and visit
|
each of the following URLs, checking that the result is as expected:
|
|
- http://localhost:6543/ invokes the ``view_wiki`` view. This always
|
redirects to the ``view_page`` view of the ``FrontPage`` Page resource.
|
|
- http://localhost:6543/FrontPage/ invokes the ``view_page`` view of the front
|
page resource. This is because it's the :term:`default view` (a view
|
without a ``name``) for Page resources.
|
|
- http://localhost:6543/FrontPage/edit_page invokes the edit view for the
|
``FrontPage`` Page resource.
|
|
- http://localhost:6543/add_page/SomePageName invokes the add view for a Page.
|
|
- To generate an error, visit http://localhost:6543/add_page which will
|
generate an ``IndexError: tuple index out of range`` error. You'll see an
|
interactive traceback facility provided by :term:`pyramid_debugtoolbar`.
|