============== Defining Views ============== A :term:`view` 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 is assumed to return a :term:`response` object. .. note:: 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. 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``. 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: .. literalinclude:: src/views/setup.py :linenos: :language: python .. note:: 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` functions to our ``views.py`` module. One view (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 named ``edit_page`` will allow a page to be edited. We'll describe each one briefly and show the resulting ``views.py`` file afterwards. .. note:: There is nothing special about the filename ``views.py``. A project may have many views throughout its codebase in arbitrarily-named files. Files implementing views 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 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 ``webob.exc.HTTPFound`` class (instances of which implement the WebOb :term:`response` interface), It will use the ``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 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 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 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 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: .. literalinclude:: src/views/tutorial/views.py :linenos: :language: python 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 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: .. literalinclude:: src/views/tutorial/templates/view.pt :linenos: :language: xml .. note:: 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: .. literalinclude:: src/views/tutorial/templates/edit.pt :linenos: :language: xml 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: .. literalinclude:: src/views/tutorial/templates/static/style.css :linenos: :language: css This CSS file will be accessed via e.g. ``http://localhost:6543/static/style.css`` by virtue of the ```` 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. #. 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 default view for the wiki. #. 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. #. 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. #. 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: .. literalinclude:: src/views/tutorial/configure.zcml :linenos: :language: xml 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: .. literalinclude:: src/views/tutorial.ini :linenos: :language: ini 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: .. literalinclude:: src/views/tutorial/tests.py :linenos: :language: python We can then run the tests using something like: .. code-block:: bash :linenos: $ python setup.py test -q The expected output is something like: .. code-block:: bash :linenos: running test running egg_info writing requirements to tutorial.egg-info/requires.txt writing tutorial.egg-info/PKG-INFO writing top-level names to tutorial.egg-info/top_level.txt writing dependency_links to tutorial.egg-info/dependency_links.txt writing entry points to tutorial.egg-info/entry_points.txt unrecognized .svn/entries format in reading manifest file 'tutorial.egg-info/SOURCES.txt' writing manifest file 'tutorial.egg-info/SOURCES.txt' running build_ext ...... ---------------------------------------------------------------------- Ran 6 tests in 0.181s OK