.. _qtut_more_view_classes:

==========================
15: More With View Classes
==========================

Group views into a class, sharing configuration, state, and logic.


Background
==========

As part of its mission to help build more ambitious web applications, Pyramid
provides many more features for views and view classes.

The Pyramid documentation discusses views as a Python "callable". This callable
can be a function, an object with a ``__call__``, or a Python class. In this
last case, methods on the class can be decorated with ``@view_config`` to
register the class methods with the :term:`configurator` as a view.

At first, our views were simple, free-standing functions. Many times your views
are related: different ways to look at or work on the same data, or a REST API
that handles multiple operations. Grouping these together as a :ref:`view class
<class_as_view>` makes sense:

- Group views.

- Centralize some repetitive defaults.

- Share some state and helpers.

Pyramid views have :ref:`view predicates <view_configuration_parameters>` that
determine which view is matched to a request, based on factors such as the
request method, the form parameters, and so on. These predicates provide many
axes of flexibility.

The following shows a simple example with four operations: view a home page
which leads to a form, save a change, and press the delete button.


Objectives
==========

- Group related views into a view class.

- Centralize configuration with class-level ``@view_defaults``.

- Dispatch one route/URL to multiple views based on request data.

- Share states and logic between views and templates via the view class.


Steps
=====

#. First we copy the results of the ``templating`` step:

   .. code-block:: bash

       cd ..; cp -r templating more_view_classes; cd more_view_classes
       $VENV/bin/pip install -e .

#. Our route in ``more_view_classes/tutorial/__init__.py`` needs some
   replacement patterns:

   .. literalinclude:: more_view_classes/tutorial/__init__.py
       :linenos:

#. Our ``more_view_classes/tutorial/views.py`` now has a view class with
   several views:

   .. literalinclude:: more_view_classes/tutorial/views.py
       :linenos:

#. Our primary view needs a template at ``more_view_classes/tutorial/home.pt``:

   .. literalinclude:: more_view_classes/tutorial/home.pt
       :language: html

#. Ditto for our other view from the previous section at
   ``more_view_classes/tutorial/hello.pt``:

   .. literalinclude:: more_view_classes/tutorial/hello.pt
       :language: html

#. We have an edit view that also needs a template at
   ``more_view_classes/tutorial/edit.pt``:

   .. literalinclude:: more_view_classes/tutorial/edit.pt
       :language: html

#. And finally the delete view's template at
   ``more_view_classes/tutorial/delete.pt``:

   .. literalinclude:: more_view_classes/tutorial/delete.pt
       :language: html

#. Our tests in ``more_view_classes/tutorial/tests.py`` fail, so let's modify
   them:

   .. literalinclude:: more_view_classes/tutorial/tests.py
       :linenos:

#. Now run the tests:

   .. code-block:: bash

       $VENV/bin/pytest tutorial/tests.py -q
       ..
       2 passed in 0.40 seconds

#. Run your Pyramid application with:

   .. code-block:: bash

       $VENV/bin/pserve development.ini --reload

#. Open http://localhost:6543/howdy/jane/doe in your browser. Click the
   ``Save`` and ``Delete`` buttons, and watch the output in the console window.


Analysis
========

As you can see, the four views are logically grouped together. Specifically:

- We have a ``home`` view available at http://localhost:6543/ with a clickable
  link to the ``hello`` view.

- The second view is returned when you go to ``/howdy/jane/doe``. This URL is
  mapped to the ``hello`` route that we centrally set using the optional
  ``@view_defaults``.

- The third view is returned when the form is submitted with a ``POST`` method.
  This rule is specified in the ``@view_config`` for that view.

- The fourth view is returned when clicking on a button such as ``<input
  type="submit" name="form.delete" value="Delete"/>``.

In this step we show, using the following information as criteria, how to
decide which view to use:

- Method of the HTTP request (``GET``, ``POST``, etc.)

- Parameter information in the request (submitted form field names)

We also centralize part of the view configuration to the class level with
``@view_defaults``, then in one view, override that default just for that one
view. Finally, we put this commonality between views to work in the view class
by sharing:

- State assigned in ``TutorialViews.__init__``

- A computed value

These are then available both in the view methods and in the templates (e.g.,
``${view.view_name}`` and ``${view.full_name}``).

As a note, we made a switch in our templates on how we generate URLs. We
previously hardcoded the URLs, such as:

.. code-block:: html

    <a href="/howdy/jane/doe">Howdy</a>

In ``home.pt`` we switched to:

.. code-block:: xml

    <a href="${request.route_url('hello', first='jane',
        last='doe')}">form</a>

Pyramid has rich facilities to help generate URLs in a flexible, non-error
prone fashion.

Extra credit
============

#. Why could our template do ``${view.full_name}`` and not have to do
   ``${view.full_name()}``?

#. The ``edit`` and ``delete`` views are both receive ``POST`` requests. Why
   does the ``edit`` view configuration not catch the ``POST`` used by
   ``delete``?

#. We used Python ``@property`` on ``full_name``. If we reference this many
   times in a template or view code, it would re-compute this every time. Does
   Pyramid provide something that will cache the initial computation on a
   property?

#. Can you associate more than one route with the same view?

#. There is also a ``request.route_path`` API. How does this differ from
   ``request.route_url``?

.. seealso:: :ref:`class_as_view`, `Weird Stuff You Can Do With
   URL Dispatch <https://web.archive.org/web/20170131192830/http://www.plope.com/weird_pyramid_urldispatch>`_
