Merge branch 'master' of github.com:pecan/pecan

This commit is contained in:
Alfredo Deza
2011-04-20 11:54:49 -04:00
9 changed files with 424 additions and 129 deletions

View File

@@ -0,0 +1,139 @@
.. _rest:
REST Controller
===============
If you need to write controllers to interact with objects, using the
``RestController`` may help speed things up. It follows the Representational
State Transfer Protocol, also known as REST, by routing the standard HTTP
verbs of GET, POST, PUT, and DELETE to individual methods::
from pecan import expose
from pecan.rest import RestController
from mymodel import Book
class BooksController(RestController):
@expose()
def get(self, id):
book = Book.get(id)
if not book:
abort(404)
return book.title
URL Mapping
-----------
By default, the ``RestController`` routes as follows:
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| Method | Description | Example Method(s) / URL(s) |
+=================+==============================================================+============================================+
| get_one | Display one record. | GET /books/1 |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| get_all | Display all records in a resource. | GET /books/ |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| get | A combo of get_one and get_all. | GET /books/ |
| | +--------------------------------------------+
| | | GET /books/1 |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| new | Display a page to create a new resource. | GET /books/new |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| edit | Display a page to edit an existing resource. | GET /books/1/edit |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| post | Create a new record. | POST /books/ |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| put | Update an existing record. | POST /books/1?_method=put |
| | +--------------------------------------------+
| | | PUT /books/1 |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| get_delete | Display a delete confirmation page. | GET /books/1/delete |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| delete | Delete an existing record. | POST /books/1?_method=delete |
| | +--------------------------------------------+
| | | DELETE /books/1 |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
Pecan's ``RestController`` uses the de-facto standard ``?_method=`` query
string hack to work around the lack of PUT/DELETE support in current browsers.
The ``RestController`` still supports the ``index``, ``_default``, and
``_lookup`` routing overrides. If you need to override ``_route``, however,
make sure to call ``RestController._route`` at the end of your custom
``_route`` method so that the REST routing described above still occurs.
Nesting
-------
``RestController`` instances can be nested so that child resources get the
parameters necessary to look up parent resources. For example::
from pecan import expose
from pecan.rest import RestController
from mymodel import Author, Book
class BooksController(RestController):
@expose()
def get(self, author_id, id):
author = Author.get(author_id)
if not author_id:
abort(404)
book = author.get_book(id)
if not book:
abort(404)
return book.title
class AuthorsController(RestController):
books = BooksController()
@expose()
def get(self, id):
author = Author.get(id)
if not author:
abort(404)
return author.name
class RootController(object):
authors = AuthorsController()
Accessing ``/authors/1/books/2`` would call ``BooksController.get`` with an
``author_id`` of 1 and ``id`` of 2.
To determine which arguments are associated with the parent resource, Pecan
looks at the ``get_one`` or ``get`` method signatures, in that order, in the
parent controller. If the parent resource takes a variable number of arguments,
Pecan will hand it everything up to the child resource controller name (e.g.,
``books`` in the above example).
Custom Actions
--------------
In addition to the default methods defined above, you can add additional
behaviors to a ``RestController`` by defining a special ``_custom_actions``
dictionary. For example::
from pecan import expose
from pecan.rest import RestController
from mymodel import Book
class BooksController(RestController):
_custom_actions = {
'checkout': ['POST']
}
@expose()
def checkout(self, id):
book = Book.get(id)
if not book:
abort(404)
book.checkout()
Additional method names are the keys in the dictionary. The values are lists
of valid HTTP verbs for those custom actions, including PUT and DELETE.

View File

@@ -218,72 +218,3 @@ requests ``/hello.json``. The second tells the templating engine to use
tells Pecan to use the html_template.mako when the client requests
``/hello.html``. If the client requests ``/hello``, Pecan will use the
text/html template.
.. _advanced_routing:
Advanced Routing
================
Pecan offers a few ways to control and extend routing.
.. _restcontroller:
RestController
--------------
A Decorated Controller that dispatches in a RESTful Manner.
This controller was designed to follow Representational State Transfer protocol, also known as REST.
The goal of this controller method is to provide the developer a way to map
RESTful URLS to controller methods directly, while still allowing Normal Object Dispatch to occur.
Here is a brief rundown of the methods which are called on dispatch along with an example URL.
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| Method | Description | Example Method(s) / URL(s) |
+=================+==============================================================+============================================+
| get_one | Display one record. | GET /movies/1 |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| get_all | Display all records in a resource. | GET /movies/ |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| get | A combo of get_one and get_all. | GET /movies/ |
| | +--------------------------------------------+
| | | GET /movies/1 |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| new | Display a page to prompt the User for resource creation. | GET /movies/new |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| edit | Display a page to prompt the User for resource modification. | GET /movies/1/edit |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| post | Create a new record. | POST /movies/ |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| put | Update an existing record. | POST /movies/1?_method=PUT |
| | +--------------------------------------------+
| | | PUT /movies/1 |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| post_delete | Delete an existing record. | POST /movies/1?_method=DELETE |
| | +--------------------------------------------+
| | | DELETE /movies/1 |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| get_delete | Display a delete Confirmation page. | GET /movies/1/delete |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| delete | A combination of post_delete and get_delete. | GET /movies/delete |
| | +--------------------------------------------+
| | | DELETE /movies/1 |
| | +--------------------------------------------+
| | | DELETE /movies/ |
| | +--------------------------------------------+
| | | POST /movies/1/delete |
| | +--------------------------------------------+
| | | POST /movies/delete |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
You may note the ?_method on some of the URLs. This is basically a hack because exiting browsers
do not support the PUT and DELETE methods. Just note that if you decide to use a this resource with a web browser,
you will likely have to add a _method as a hidden field in your forms for these items. Also note that RestController differs
from a base Pecan controller in that it offers no index, default, or lookup. It is intended primarily for resource management.
.. _note:
The following items are still pending:
* Hooks
* Security

View File

@@ -17,7 +17,7 @@ Structure
---------
This guide assumes that you have all your tests in a ``tests`` directory. If
you have created a project from the ``base`` project template that Pecan
provides you should already have this directory with a few tests.
provides, you should already have this directory with a few tests.
The template project uses UnitTest-type tests and some of those tests use
WebTest. We will describe how they work in the next section.
@@ -37,34 +37,54 @@ This is how running those tests with ``py.test`` would look like::
Configuration and Testing
-------------------------
When running tests, you would want to avoid as much as possible setting up test
cases by creating a Pecan app on each instance. To avoid this, you need to
create a proper test configuration file and load it at setup time.
When you create a new project using the ``base`` project template, Pecan adds
a reference to its ``py.test`` plugin to your project's ``setup.cfg`` file.
This handles loading your Pecan configuration and setting up your app as
defined by your project's ``app.py`` file.
To do this, you need to know the absolute path for your configuration file and
then call ``set_config`` with it. A typical ``setUp`` method would look like::
If you've created your own project without using Pecan's template, you can
load the plugin yourself by adding this to your ``setup.cfg`` file::
def setUp(self):
config_path = '/path/to/test_config.py'
pecan.set_config(config_path)
[pytest]
addopts = -p pecan.testing --with-config=./config.py
self.app = TestApp(
make_app(
config.app.root
template_path = config.app.template_path
)
)
Alternatively, you can just pass those options to ``py.test`` directly.
As you can see, we are loading the configuration file into Pecan first and then
creating a Pecan application with it. Any interaction after ``setUp`` will be
exactly as if your application was really running via an HTTP server.
By default, Pecan's testing plugin assumes you will be using the ``config.py``
configuration file to run your tests. To change which configuration file gets
used once, run ``py.test`` with the `--with-config` option. To make the change
permanent, modify that option in the `addopts` setting of your ``setup.cfg``
file.
Pecan's ``py.test`` plugin exposes two new variables in the ``py.test``
namespace: ``temp_dir`` and ``wsgi_app``.
``py.test.temp_dir`` is a temporary directory that you can use for your tests.
It's created at startup and deleted after all tests have completed. When using
locally distributed testing with py.test, this is guaranteed to be shared by
each test process. This is useful if you need to create some initial resource
(e.g., a database template) that is later copied by each test. If you're using
remotely distributed testing, the directory won't be shared across nodes.
``py.test.wsgi_app`` is your Pecan app loaded and configured per your project's
``app.py`` file. In your test's ``setUp`` method, you would wrap this with
``TestApp``::
from unittest import TestCase
from webtest import TestApp
import py.test
class TestRootController(TestCase):
def setUp(self):
self.app = TestApp(py.test.wsgi_app)
Using WebTest with a UnitTest
-----------------------------
Once you have a ``setUp`` method with your Pecan configuration loaded you have
a wealth of actions provided within the test class to interact with your Pecan
Once you have a ``setUp`` method with your ``TestApp`` created, you have a
wealth of actions provided within the test class to interact with your Pecan
application::
* POST => self.app.post
@@ -72,19 +92,18 @@ application::
* DELETE => self.app.delete
* PUT => self.app.put
For example, if I wanted to assert that I can get the root of my application,
I would probably do something similar to this::
For example, if you want to assert that you can get to the root of your
application, you could do something similar to this::
response = self.app.get('/')
assert response.status_int == 200
If you are expecting error responses from your application, you should make
sure that you pass the `expect_errors` flag and set it to True::
If you are expecting error responses from your application, make sure to pass
`expect_errors=True`::
response = self.app.get('/url/does/not/exist', expect_errors=True)
assert response.status_int == 404
If you would like to dig in to more examples in how to test and verify more
actions, make sure you take a look at the
actions, take a look at the
`WebTest documentation <http://pythonpaste.org/webtest/>`_