commit | author | age
|
93a7a1
|
1 |
.. _wiki_defining_views: |
SP |
2 |
|
e53e13
|
3 |
============== |
CM |
4 |
Defining Views |
|
5 |
============== |
|
6 |
|
beb4f1
|
7 |
A :term:`view callable` in a :term:`traversal`-based :app:`Pyramid` |
d63eaf
|
8 |
application is typically a simple Python function that accepts two |
PP |
9 |
parameters: :term:`context` and :term:`request`. A view callable is |
|
10 |
assumed to return a :term:`response` object. |
e53e13
|
11 |
|
012b97
|
12 |
.. note:: |
M |
13 |
|
|
14 |
A :app:`Pyramid` view can also be defined as callable |
d63eaf
|
15 |
which accepts *only* a :term:`request` argument. You'll see |
PP |
16 |
this one-argument pattern used in other :app:`Pyramid` tutorials |
|
17 |
and applications. Either calling convention will work in any |
|
18 |
:app:`Pyramid` application; the calling conventions can be used |
beb4f1
|
19 |
interchangeably as necessary. In :term:`traversal`-based applications, |
d63eaf
|
20 |
URLs are mapped to a context :term:`resource`, and since our |
PP |
21 |
:term:`resource tree` also represents our application's |
a940e1
|
22 |
"domain model", we're often interested in the context because |
d63eaf
|
23 |
it represents the persistent storage of our application. For |
PP |
24 |
this reason, in this tutorial we define views as callables that |
|
25 |
accept ``context`` in the callable argument list. If you do |
|
26 |
need the ``context`` within a view function that only takes |
|
27 |
the request as a single argument, you can obtain it via |
|
28 |
``request.context``. |
2a5ae0
|
29 |
|
879bb5
|
30 |
We're going to define several :term:`view callable` functions, then wire them |
2a5ae0
|
31 |
into :app:`Pyramid` using some :term:`view configuration`. |
e53e13
|
32 |
|
CM |
33 |
|
db638c
|
34 |
Declaring Dependencies in Our ``setup.py`` File |
279ba5
|
35 |
=============================================== |
db638c
|
36 |
|
279ba5
|
37 |
The view code in our application will depend on a package which is not a |
PP |
38 |
dependency of the original "tutorial" application. The original "tutorial" |
38cf5b
|
39 |
application was generated by the cookiecutter; it doesn't know |
a940e1
|
40 |
about our custom application requirements. |
db638c
|
41 |
|
a940e1
|
42 |
We need to add a dependency on the ``docutils`` package to our ``tutorial`` |
SP |
43 |
package's ``setup.py`` file by assigning this dependency to the ``requires`` |
|
44 |
parameter in the ``setup()`` function. |
|
45 |
|
b8f579
|
46 |
Open ``setup.py`` and edit it to look like the following: |
db638c
|
47 |
|
b1e277
|
48 |
.. literalinclude:: src/views/setup.py |
db638c
|
49 |
:linenos: |
909ae0
|
50 |
:emphasize-lines: 22 |
db638c
|
51 |
:language: python |
PP |
52 |
|
a940e1
|
53 |
Only the highlighted line needs to be added. |
279ba5
|
54 |
|
b01a02
|
55 |
.. _wiki-running-pip-install: |
b8f579
|
56 |
|
SP |
57 |
Running ``pip install -e .`` |
a940e1
|
58 |
============================ |
e53e13
|
59 |
|
b8f579
|
60 |
Since a new software dependency was added, you will need to run ``pip install |
SP |
61 |
-e .`` again inside the root of the ``tutorial`` package to obtain and register |
|
62 |
the newly added dependency distribution. |
a940e1
|
63 |
|
SP |
64 |
Make sure your current working directory is the root of the project (the |
|
65 |
directory in which ``setup.py`` lives) and execute the following command. |
|
66 |
|
|
67 |
On UNIX: |
|
68 |
|
b8f579
|
69 |
.. code-block:: bash |
a940e1
|
70 |
|
SP |
71 |
$ cd tutorial |
b8f579
|
72 |
$ $VENV/bin/pip install -e . |
a940e1
|
73 |
|
SP |
74 |
On Windows: |
|
75 |
|
a651b3
|
76 |
.. code-block:: doscon |
a940e1
|
77 |
|
beb4f1
|
78 |
c:\> cd tutorial |
SP |
79 |
c:\tutorial> %VENV%\Scripts\pip install -e . |
a940e1
|
80 |
|
SP |
81 |
Success executing this command will end with a line to the console something |
b8f579
|
82 |
like: |
a940e1
|
83 |
|
b8f579
|
84 |
.. code-block:: text |
SP |
85 |
|
beb4f1
|
86 |
Successfully installed docutils-0.13.1 tutorial |
b8f579
|
87 |
|
a940e1
|
88 |
|
SP |
89 |
Adding view functions in ``views.py`` |
|
90 |
===================================== |
|
91 |
|
b8f579
|
92 |
It's time for a major change. Open ``tutorial/views.py`` and edit it to look |
SP |
93 |
like the following: |
a940e1
|
94 |
|
SP |
95 |
.. literalinclude:: src/views/tutorial/views.py |
|
96 |
:linenos: |
|
97 |
:language: python |
|
98 |
|
|
99 |
We added some imports and created a regular expression to find "WikiWords". |
|
100 |
|
|
101 |
We got rid of the ``my_view`` view function and its decorator that was added |
beb4f1
|
102 |
when we originally rendered the ``zodb`` cookiecutter. It was only an example and |
a940e1
|
103 |
isn't relevant to our application. |
SP |
104 |
|
|
105 |
Then we added four :term:`view callable` functions to our ``views.py`` |
beb4f1
|
106 |
module: |
a940e1
|
107 |
|
SP |
108 |
* ``view_wiki()`` - Displays the wiki itself. It will answer on the root URL. |
|
109 |
* ``view_page()`` - Displays an individual page. |
|
110 |
* ``add_page()`` - Allows the user to add a page. |
|
111 |
* ``edit_page()`` - Allows the user to edit a page. |
|
112 |
|
|
113 |
We'll describe each one briefly in the following sections. |
e53e13
|
114 |
|
a4e90c
|
115 |
.. note:: |
PP |
116 |
|
|
117 |
There is nothing special about the filename ``views.py``. A project may |
a940e1
|
118 |
have many view callables throughout its codebase in arbitrarily named |
a4e90c
|
119 |
files. Files implementing view callables often have ``view`` in their |
PP |
120 |
filenames (or may live in a Python subpackage of your application package |
|
121 |
named ``views``), but this is only by convention. |
|
122 |
|
e53e13
|
123 |
The ``view_wiki`` view function |
CM |
124 |
------------------------------- |
|
125 |
|
a940e1
|
126 |
Following is the code for the ``view_wiki`` view function and its decorator: |
f56480
|
127 |
|
SP |
128 |
.. literalinclude:: src/views/tutorial/views.py |
|
129 |
:lines: 12-14 |
beb4f1
|
130 |
:lineno-match: |
f56480
|
131 |
:language: python |
SP |
132 |
|
a940e1
|
133 |
.. note:: In our code, we use an *import* that is *relative* to our package |
SP |
134 |
named ``tutorial``, meaning we can omit the name of the package in the |
|
135 |
``import`` and ``context`` statements. In our narrative, however, we refer |
|
136 |
to a *class* and thus we use the *absolute* form, meaning that the name of |
|
137 |
the package is included. |
2a5ae0
|
138 |
|
a940e1
|
139 |
``view_wiki()`` is the :term:`default view` that gets called when a request is |
SP |
140 |
made to the root URL of our wiki. It always redirects to an URL which |
|
141 |
represents the path to our "FrontPage". |
|
142 |
|
|
143 |
We provide it with a ``@view_config`` decorator which names the class |
|
144 |
``tutorial.models.Wiki`` as its context. This means that when a Wiki resource |
|
145 |
is the context and no :term:`view name` exists in the request, then this view |
|
146 |
will be used. The view configuration associated with ``view_wiki`` does not |
|
147 |
use a ``renderer`` because the view callable always returns a :term:`response` |
|
148 |
object rather than a dictionary. No renderer is necessary when a view returns |
|
149 |
a response object. |
|
150 |
|
|
151 |
The ``view_wiki`` view callable always redirects to the URL of a Page resource |
|
152 |
named "FrontPage". To do so, it returns an instance of the |
b743bb
|
153 |
:class:`pyramid.httpexceptions.HTTPFound` class (instances of which implement |
a940e1
|
154 |
the :class:`pyramid.interfaces.IResponse` interface, like |
SP |
155 |
:class:`pyramid.response.Response` does). It uses the |
|
156 |
:meth:`pyramid.request.Request.route_url` API to construct an URL to the |
f56480
|
157 |
``FrontPage`` page resource (i.e., ``http://localhost:6543/FrontPage``), and |
a940e1
|
158 |
uses it as the "location" of the ``HTTPFound`` response, forming an HTTP |
42d31c
|
159 |
redirect. |
e53e13
|
160 |
|
CM |
161 |
The ``view_page`` view function |
|
162 |
------------------------------- |
f56480
|
163 |
|
a940e1
|
164 |
Here is the code for the ``view_page`` view function and its decorator: |
f56480
|
165 |
|
SP |
166 |
.. literalinclude:: src/views/tutorial/views.py |
|
167 |
:lines: 16-33 |
beb4f1
|
168 |
:lineno-match: |
f56480
|
169 |
:language: python |
e53e13
|
170 |
|
a940e1
|
171 |
The ``view_page`` function is configured to respond as the default view |
SP |
172 |
of a Page resource. We provide it with a ``@view_config`` decorator which |
455196
|
173 |
names the class ``tutorial.models.Page`` as its context. This means that |
b743bb
|
174 |
when a Page resource is the context, and no :term:`view name` exists in the |
CM |
175 |
request, this view will be used. We inform :app:`Pyramid` this view will use |
|
176 |
the ``templates/view.pt`` template file as a ``renderer``. |
2a5ae0
|
177 |
|
f56480
|
178 |
The ``view_page`` function generates the :term:`reStructuredText` body of a |
2a5ae0
|
179 |
page (stored as the ``data`` attribute of the context passed to the view; the |
a940e1
|
180 |
context will be a ``Page`` resource) as HTML. Then it substitutes an HTML |
SP |
181 |
anchor for each *WikiWord* reference in the rendered HTML using a compiled |
|
182 |
regular expression. |
e53e13
|
183 |
|
CM |
184 |
The curried function named ``check`` is used as the first argument to |
2a5ae0
|
185 |
``wikiwords.sub``, indicating that it should be called to provide a value for |
CM |
186 |
each WikiWord match found in the content. If the wiki (our page's |
|
187 |
``__parent__``) already contains a page with the matched WikiWord name, the |
|
188 |
``check`` function generates a view link to be used as the substitution value |
043ccd
|
189 |
and returns it. If the wiki does not already contain a page with the |
2a5ae0
|
190 |
matched WikiWord name, the function generates an "add" link as the |
CM |
191 |
substitution value and returns it. |
|
192 |
|
|
193 |
As a result, the ``content`` variable is now a fully formed bit of HTML |
|
194 |
containing various view and add links for WikiWords based on the content of |
b743bb
|
195 |
our current page resource. |
2a5ae0
|
196 |
|
a940e1
|
197 |
We then generate an edit URL because it's easier to do here than in the |
SP |
198 |
template, and we wrap up a number of arguments in a dictionary and return |
e53e13
|
199 |
it. |
CM |
200 |
|
2a5ae0
|
201 |
The arguments we wrap into a dictionary include ``page``, ``content``, and |
CM |
202 |
``edit_url``. As a result, the *template* associated with this view callable |
|
203 |
(via ``renderer=`` in its configuration) will be able to use these names to |
|
204 |
perform various rendering tasks. The template associated with this view |
|
205 |
callable will be a template which lives in ``templates/view.pt``. |
e53e13
|
206 |
|
2a5ae0
|
207 |
Note the contrast between this view callable and the ``view_wiki`` view |
CM |
208 |
callable. In the ``view_wiki`` view callable, we unconditionally return a |
|
209 |
:term:`response` object. In the ``view_page`` view callable, we return a |
|
210 |
*dictionary*. It is *always* fine to return a :term:`response` object from a |
|
211 |
:app:`Pyramid` view. Returning a dictionary is allowed only when there is a |
|
212 |
:term:`renderer` associated with the view callable in the view configuration. |
e53e13
|
213 |
|
CM |
214 |
The ``add_page`` view function |
|
215 |
------------------------------ |
|
216 |
|
a940e1
|
217 |
Here is the code for the ``add_page`` view function and its decorator: |
f56480
|
218 |
|
SP |
219 |
.. literalinclude:: src/views/tutorial/views.py |
|
220 |
:lines: 35-50 |
beb4f1
|
221 |
:lineno-match: |
f56480
|
222 |
:language: python |
SP |
223 |
|
a940e1
|
224 |
The ``add_page`` function is configured to respond when the context resource |
SP |
225 |
is a Wiki and the :term:`view name` is ``add_page``. We provide it with a |
|
226 |
``@view_config`` decorator which names the string ``add_page`` as its |
|
227 |
:term:`view name` (via ``name=``), the class ``tutorial.models.Wiki`` as its |
|
228 |
context, and the renderer named ``templates/edit.pt``. This means that when a |
|
229 |
Wiki resource is the context, and a :term:`view name` named ``add_page`` |
b743bb
|
230 |
exists as the result of traversal, this view will be used. We inform |
a940e1
|
231 |
:app:`Pyramid` this view will use the ``templates/edit.pt`` template file as a |
SP |
232 |
``renderer``. We share the same template between add and edit views, thus |
2a5ae0
|
233 |
``edit.pt`` instead of ``add.pt``. |
e53e13
|
234 |
|
2a5ae0
|
235 |
The ``add_page`` function will be invoked when a user clicks on a WikiWord |
CM |
236 |
which isn't yet represented as a page in the system. The ``check`` function |
|
237 |
within the ``view_page`` view generates URLs to this view. It also acts as a |
b743bb
|
238 |
handler for the form that is generated when we want to add a page resource. |
CM |
239 |
The ``context`` of the ``add_page`` view is always a Wiki resource (*not* a |
|
240 |
Page resource). |
e53e13
|
241 |
|
2a5ae0
|
242 |
The request :term:`subpath` in :app:`Pyramid` is the sequence of names that |
CM |
243 |
are found *after* the :term:`view name` in the URL segments given in the |
|
244 |
``PATH_INFO`` of the WSGI request as the result of :term:`traversal`. If our |
a940e1
|
245 |
add view is invoked via, e.g., ``http://localhost:6543/add_page/SomeName``, |
2a5ae0
|
246 |
the :term:`subpath` will be a tuple: ``('SomeName',)``. |
CM |
247 |
|
beb4f1
|
248 |
The add view takes the zero\ :sup:`th` element of the subpath (the wiki page name), |
2a5ae0
|
249 |
and aliases it to the name attribute in order to know the name of the page |
CM |
250 |
we're trying to add. |
e53e13
|
251 |
|
CM |
252 |
If the view rendering is *not* a result of a form submission (if the |
2a5ae0
|
253 |
expression ``'form.submitted' in request.params`` is ``False``), the view |
CM |
254 |
renders a template. To do so, it generates a "save url" which the template |
f56480
|
255 |
uses as the form post URL during rendering. We're lazy here, so we're trying |
2a5ae0
|
256 |
to use the same template (``templates/edit.pt``) for the add view as well as |
b743bb
|
257 |
the page edit view. To do so, we create a dummy Page resource object in |
CM |
258 |
order to satisfy the edit form's desire to have *some* page object exposed as |
2a5ae0
|
259 |
``page``, and we'll render the template to a response. |
e53e13
|
260 |
|
2a5ae0
|
261 |
If the view rendering *is* a result of a form submission (if the expression |
a940e1
|
262 |
``'form.submitted' in request.params`` is ``True``), we grab the page body |
2a5ae0
|
263 |
from the form data, create a Page object using the name in the subpath and |
b743bb
|
264 |
the page body, and save it into "our context" (the Wiki) using the |
2a5ae0
|
265 |
``__setitem__`` method of the context. We then redirect back to the |
CM |
266 |
``view_page`` view (the default view for a page) for the newly created page. |
e53e13
|
267 |
|
CM |
268 |
The ``edit_page`` view function |
|
269 |
------------------------------- |
|
270 |
|
a940e1
|
271 |
Here is the code for the ``edit_page`` view function and its decorator: |
f56480
|
272 |
|
SP |
273 |
.. literalinclude:: src/views/tutorial/views.py |
|
274 |
:lines: 52-60 |
beb4f1
|
275 |
:lineno-match: |
f56480
|
276 |
:language: python |
SP |
277 |
|
a940e1
|
278 |
The ``edit_page`` function is configured to respond when the context is |
SP |
279 |
a Page resource and the :term:`view name` is ``edit_page``. We provide it |
b743bb
|
280 |
with a ``@view_config`` decorator which names the string ``edit_page`` as its |
CM |
281 |
:term:`view name` (via ``name=``), the class ``tutorial.models.Page`` as its |
2a5ae0
|
282 |
context, and the renderer named ``templates/edit.pt``. This means that when |
b743bb
|
283 |
a Page resource is the context, and a :term:`view name` exists as the result |
b1e277
|
284 |
of traversal named ``edit_page``, this view will be used. We inform |
2a5ae0
|
285 |
:app:`Pyramid` this view will use the ``templates/edit.pt`` template file as |
CM |
286 |
a ``renderer``. |
|
287 |
|
|
288 |
The ``edit_page`` function will be invoked when a user clicks the "Edit this |
|
289 |
Page" button on the view form. It renders an edit form but it also acts as |
b743bb
|
290 |
the form post view callable for the form it renders. The ``context`` of the |
CM |
291 |
``edit_page`` view will *always* be a Page resource (never a Wiki resource). |
e53e13
|
292 |
|
CM |
293 |
If the view execution is *not* a result of a form submission (if the |
2a5ae0
|
294 |
expression ``'form.submitted' in request.params`` is ``False``), the view |
879bb5
|
295 |
simply renders the edit form, passing the page resource, and a ``save_url`` |
CZ |
296 |
which will be used as the action of the generated form. |
e53e13
|
297 |
|
2a5ae0
|
298 |
If the view execution *is* a result of a form submission (if the expression |
CM |
299 |
``'form.submitted' in request.params`` is ``True``), the view grabs the |
|
300 |
``body`` element of the request parameter and sets it as the ``data`` |
|
301 |
attribute of the page context. It then redirects to the default view of the |
|
302 |
context (the page), which will always be the ``view_page`` view. |
e53e13
|
303 |
|
a940e1
|
304 |
Adding templates |
e53e13
|
305 |
================ |
CM |
306 |
|
f97aa6
|
307 |
The ``view_page``, ``add_page`` and ``edit_page`` views that we've added |
a940e1
|
308 |
reference a :term:`template`. Each template is a :term:`Chameleon` |
SP |
309 |
:term:`ZPT` template. These templates will live in the ``templates`` |
|
310 |
directory of our tutorial package. Chameleon templates must have a ``.pt`` |
|
311 |
extension to be recognized as such. |
e53e13
|
312 |
|
a940e1
|
313 |
The ``view.pt`` template |
e53e13
|
314 |
------------------------ |
CM |
315 |
|
beb4f1
|
316 |
Rename ``tutorial/templates/mytemplate.pt`` to ``tutorial/templates/view.pt`` and edit the emphasized lines to look like the following: |
e53e13
|
317 |
|
CM |
318 |
.. literalinclude:: src/views/tutorial/templates/view.pt |
8e7df0
|
319 |
:linenos: |
a940e1
|
320 |
:language: html |
beb4f1
|
321 |
:emphasize-lines: 11-12,37-52 |
e53e13
|
322 |
|
8e7df0
|
323 |
This template is used by ``view_page()`` for displaying a single |
PP |
324 |
wiki page. It includes: |
012b97
|
325 |
|
a940e1
|
326 |
- A ``div`` element that is replaced with the ``content`` value provided by |
beb4f1
|
327 |
the view (lines 37-39). ``content`` contains HTML, so the ``structure`` |
a940e1
|
328 |
keyword is used to prevent escaping it (i.e., changing ">" to ">", etc.) |
SP |
329 |
- A link that points at the "edit" URL which invokes the ``edit_page`` view |
beb4f1
|
330 |
for the page being viewed (lines 41-43). |
e53e13
|
331 |
|
a940e1
|
332 |
The ``edit.pt`` template |
e53e13
|
333 |
------------------------ |
CM |
334 |
|
beb4f1
|
335 |
Copy ``tutorial/templates/view.pt`` to ``tutorial/templates/edit.pt`` and edit the emphasized lines to look like the following: |
e53e13
|
336 |
|
CM |
337 |
.. literalinclude:: src/views/tutorial/templates/edit.pt |
8e7df0
|
338 |
:linenos: |
a940e1
|
339 |
:language: html |
e53e13
|
340 |
|
a940e1
|
341 |
This template is used by ``add_page()`` and ``edit_page()`` for adding and |
SP |
342 |
editing a wiki page. It displays a page containing a form that includes: |
8e7df0
|
343 |
|
beb4f1
|
344 |
- A 10-row by 60-column ``textarea`` field named ``body`` that is filled |
SP |
345 |
with any existing page data when it is rendered (line 46). |
|
346 |
- A submit button that has the name ``form.submitted`` (line 49). |
8e7df0
|
347 |
|
a940e1
|
348 |
The form POSTs back to the ``save_url`` argument supplied by the view (line |
beb4f1
|
349 |
44). The view will use the ``body`` and ``form.submitted`` values. |
8e7df0
|
350 |
|
a940e1
|
351 |
.. note:: Our templates use a ``request`` object that none of our tutorial |
SP |
352 |
views return in their dictionary. ``request`` is one of several names that |
|
353 |
are available "by default" in a template when a template renderer is used. |
|
354 |
See :ref:`renderer_system_values` for information about other names that |
|
355 |
are available by default when a template is used as a renderer. |
8e7df0
|
356 |
|
b8f579
|
357 |
|
6901d7
|
358 |
Static assets |
b743bb
|
359 |
------------- |
e53e13
|
360 |
|
a940e1
|
361 |
Our templates name static assets, including CSS and images. We don't need |
SP |
362 |
to create these files within our package's ``static`` directory because they |
|
363 |
were provided at the time we created the project. |
e53e13
|
364 |
|
a940e1
|
365 |
As an example, the CSS file will be accessed via |
SP |
366 |
``http://localhost:6543/static/theme.css`` by virtue of the call to the |
b1e277
|
367 |
``add_static_view`` directive we've made in the ``__init__.py`` file. Any |
b743bb
|
368 |
number and type of static assets can be placed in this directory (or |
a940e1
|
369 |
subdirectories) and are just referred to by URL or by using the convenience |
SP |
370 |
method ``static_url``, e.g., |
|
371 |
``request.static_url('<package>:static/foo.css')`` within templates. |
e53e13
|
372 |
|
b8f579
|
373 |
|
6901d7
|
374 |
Viewing the application in a browser |
04ecdb
|
375 |
==================================== |
PP |
376 |
|
218ad4
|
377 |
We can finally examine our application in a browser (See |
607524
|
378 |
:ref:`wiki-start-the-application`). Launch a browser and visit |
a940e1
|
379 |
each of the following URLs, checking that the result is as expected: |
04ecdb
|
380 |
|
6901d7
|
381 |
- http://localhost:6543/ invokes the ``view_wiki`` view. This always |
a940e1
|
382 |
redirects to the ``view_page`` view of the ``FrontPage`` Page resource. |
04ecdb
|
383 |
|
6901d7
|
384 |
- http://localhost:6543/FrontPage/ invokes the ``view_page`` view of the front |
SP |
385 |
page resource. This is because it's the :term:`default view` (a view |
a940e1
|
386 |
without a ``name``) for Page resources. |
04ecdb
|
387 |
|
6901d7
|
388 |
- http://localhost:6543/FrontPage/edit_page invokes the edit view for the |
a940e1
|
389 |
``FrontPage`` Page resource. |
04ecdb
|
390 |
|
6901d7
|
391 |
- http://localhost:6543/add_page/SomePageName invokes the add view for a Page. |
04ecdb
|
392 |
|
6901d7
|
393 |
- To generate an error, visit http://localhost:6543/add_page which will |
617f19
|
394 |
generate an ``IndexError: tuple index out of range`` error. You'll see an |
a940e1
|
395 |
interactive traceback facility provided by :term:`pyramid_debugtoolbar`. |