diff options
| author | Chris McDonough <chrism@plope.com> | 2010-10-25 17:58:11 -0400 |
|---|---|---|
| committer | Chris McDonough <chrism@plope.com> | 2010-10-25 17:58:11 -0400 |
| commit | e26700528995b1807c5e55a4295a6f788a5603de (patch) | |
| tree | 439e71c28d716bf5d5b742cee2578e6247da4dc3 /docs/tutorials/wiki2/definingviews.rst | |
| parent | 4b679b4d7cf4a045293f9e652aee818cd8649dc3 (diff) | |
| download | pyramid-e26700528995b1807c5e55a4295a6f788a5603de.tar.gz pyramid-e26700528995b1807c5e55a4295a6f788a5603de.tar.bz2 pyramid-e26700528995b1807c5e55a4295a6f788a5603de.zip | |
adjust wiki2 tutorial for pyramid
Diffstat (limited to 'docs/tutorials/wiki2/definingviews.rst')
| -rw-r--r-- | docs/tutorials/wiki2/definingviews.rst | 404 |
1 files changed, 404 insertions, 0 deletions
diff --git a/docs/tutorials/wiki2/definingviews.rst b/docs/tutorials/wiki2/definingviews.rst new file mode 100644 index 000000000..1bba7efea --- /dev/null +++ b/docs/tutorials/wiki2/definingviews.rst @@ -0,0 +1,404 @@ +============== +Defining Views +============== + +A :term:`view callable` in a :term:`url dispatch` -based +:mod:`pyramid` application is typically a simple Python function that +accepts a single parameter named :term:`request`. A view callable is +assumed to return a :term:`response` object. + +.. note:: A :mod:`pyramid` 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:`pyramid` tutorials and applications. Either calling + convention will work in any :mod:`pyramid` application; the + calling conventions can be used interchangeably as necessary. 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 ``pattern`` of a ``route`` +statement. For instance, if a route statement in ``configure.zcml`` +had the pattern ``:one/:two``, and the URL at +``http://example.com/foo/bar`` was invoked, matching this pattern, 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``. + +The source code for this tutorial stage can be browsed at +`http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/views/ +<http://github.com/Pylons/pyramid/tree/master/docs/tutorials/wiki2/src/views/>`_. + +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 callable` functions to our +``views.py`` module. One view callable (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 callable +named ``edit_page`` will allow a page to be edited. We'll describe +each one briefly and show the resulting ``views.py`` file afterward. + +.. 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 +------------------------------- + +The ``view_wiki`` function will respond as the :term:`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 +:class:`webob.exc.HTTPFound` class (instances of which implement the +WebOb :term:`response` interface), It will use the +:func:`pyramid.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 :term:`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:`pyramid` 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 callable 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 +:mod:`pyramid` 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 :term:`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:`pyramid` is a variant of :term:`ZPT` provided by +:term:`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. This file is a little +too long to replicate within the body of this guide, however it is +available `online +<http://github.com/Pylons/pyramid/blob/master/docs/tutorials/wiki2/src/views/tutorial/templates/static/style.css>`_. + + +This CSS file will be accessed via +e.g. ``http://localhost:6543/static/style.css`` by virtue of the +``<static>`` 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 pattern (signifying the root + URL) to the view named ``view_wiki`` in our ``views.py`` file with + the name ``view_wiki``. This is the :term:`default view` for the + wiki. + +#. Add a declaration which maps the 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 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 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:: text + :linenos: + + $ python setup.py test -q + +The expected output is something like: + +.. code-block:: text + :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 + + + + |
