.. _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 will 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`. Since our :term:`resource tree` also represents our application's "domain model", we are 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 will define several :term:`view callable` functions, then wire them into :app:`Pyramid` using some :term:`view configuration`. .. seealso:: This chapter will introduce more concepts, as did the previous. See also the chapter :ref:`resources_chapter` for a complete description of resources and the chapter :ref:`traversal_chapter` for the technical details of how traversal works in Pyramid. 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 does not 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 :lines: 11-29 :lineno-match: :emphasize-lines: 2 :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 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 cd tutorial %VENV%\Scripts\pip install -e . Success executing this command will end with a line to the console similar to the following: .. code-block:: text Successfully installed docutils-0.16 tutorial Adding view functions in the ``views`` package ============================================== It is time for a major change. Open ``tutorial/views/default.py`` and edit it to look like the following: .. literalinclude:: src/views/tutorial/views/default.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 originally rendered after we selected the ``zodb`` backend option in the cookiecutter. It was only an example and is not relevant to our application. Then we added four :term:`view callable` functions to our ``default.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 will describe each one briefly in the following sections. .. note:: There is nothing special about the filename ``default.py``. A project may have many view callables throughout its codebase in arbitrarily named files. Files that implement view callables often have ``view`` in their names (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/default.py :lines: 12-14 :lineno-match: :language: python .. note:: In our code, we use an *import* that is *relative* to our package named ``tutorial``. This means 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. This means 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.HTTPSeeOther` class. Instances of this class implement the :class:`pyramid.interfaces.IResponse` interface, similar to :class:`pyramid.response.Response`. It uses the :meth:`pyramid.request.Request.route_url` API to construct an URL to the ``FrontPage`` page resource (in other words, ``http://localhost:6543/FrontPage``), and uses it as the ``location`` of the ``HTTPSeeOther`` 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/default.py :lines: 17-34 :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 as HTML. The body is stored as the ``data`` attribute of the context passed to the view. The context will be a ``Page`` resource. 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 ``page_text`` variable is now a fully formed bit of HTML containing various view and add links for ``WikiWord``\s based on the content of our current page resource. We then generate an edit URL because it is easier to do here than in the template. Finally we wrap up a number of arguments in a dictionary and return it. The arguments we wrap into a dictionary include ``page``, ``page_text``, 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/default.py :lines: 37-52 :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, then 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`` that is not 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, for example, ``http://localhost:6543/add_page/SomeName``, then the :term:`subpath` will be a tuple ``('SomeName',)``. The add view takes the zero\ :sup:`th` element of the subpath (the wiki page name), then aliases it to the name attribute to know the name of the page we are 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``), then 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 are lazy here, so we try to use the same template (``templates/edit.pt``) for both the add and edit views. To do so, we create a dummy ``Page`` resource object to satisfy the edit form's desire to have *some* page object exposed as ``page``. We then set the ``Page`` object's ``__name__`` and ``__parent__``. Then we will 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``), then do the following: - Grab the page body from the form data as ``body``. - Create a ``Page`` object using the name in the subpath and the page body as ``page``. - Set the ``Page`` object's ``__name__`` and ``__parent__``. - 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. .. seealso:: In the :ref:`previous chapter `, we mentioned that all objects in a traversal graph must have a ``__name__`` and a ``__parent__`` attribute. That provides location awareness for resources. See also the section on :ref:`location_aware` in the :ref:`resources_chapter` chapter for a complete discussion. The ``edit_page`` view function ------------------------------- Here is the code for the ``edit_page`` view function and its decorator: .. literalinclude:: src/views/tutorial/views/default.py :lines: 55- :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. 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``), then the view 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. Modifying the ``notfound_view`` in ``notfound.py`` -------------------------------------------------- We have one more view to modify. Open ``tutorial/views/notfound.py`` and make the changes shown by the emphasized lines. .. literalinclude:: src/views/tutorial/views/notfound.py :linenos: :language: python :emphasize-lines: 3, 9-12 We need to import the ``Page`` from our models. We eventually return a ``Page`` object as ``page`` into the template ``layout.pt`` to display its name in the title tag. Adding templates ================ The ``view_page``, ``add_page``, and ``edit_page`` views that we 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 ``layout.pt`` template -------------------------- Update ``tutorial/templates/layout.pt`` with the following content, as indicated by the emphasized lines: .. literalinclude:: src/views/tutorial/templates/layout.pt :linenos: :emphasize-lines: 11, 36-40 :language: html Since we are using a templating engine, we can factor common boilerplate out of our page templates into reusable components. We can do this via :term:`METAL` macros and slots. - The cookiecutter defined a macro named ``layout`` (line 1). This macro consists of the entire template. - We changed the ``title`` tag to use the ``name`` attribute of a ``page`` object, or if it does not exist then the page title (line 11). - The cookiecutter defined a macro customization point or `slot` (line 35). This slot is inside the macro ``layout``. Therefore it can be replaced by content, customizing the macro. - We added a ``div`` element with a link to allow the user to return to the front page (lines 36-40). .. seealso:: Please refer to the Chameleon documentation for more information about using `METAL `_ for defining and using macros and slots. 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: 5-16 This template is used by ``view_page()`` for displaying a single wiki page. It includes: - The use of a macro to load the entire template ``layout.pt``. - The template fills the slot named ``content`` (line 2) with a ``div`` element. - A ``div`` element that is replaced with the ``page_text`` value provided by the view (line 5). ``page_text`` contains HTML, so the ``structure`` keyword is used to prevent escaping HTML entities, such as changing ``>`` to ``>``. - A link that points at the "edit" URL, which invokes the ``edit_page`` view for the page being viewed (lines 9-11). - A ``span`` whose content is replaced by the name of the page, if present. 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 :emphasize-lines: 5-20 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 (lines 11-13). - A submit button that has the name ``form.submitted`` (lines 16-18). When submitted, the form sends a POST request to the ``save_url`` argument supplied by the view (line 9). 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 by the cookiecutter 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 in the ``routes.py`` file. Any number and type of static assets can be placed in this directory (or subdirectories). They are referred to by either URL or using the convenience method ``static_url``, for example ``request.static_url(':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 is 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 will see an interactive traceback facility provided by :term:`pyramid_debugtoolbar`. - To generate a not found error, visit http://localhost:6543/wakawaka which will invoke the ``notfound_view`` view provided by the cookiecutter.