Edgewall Software

Changes between Initial Version and Version 1 of TracDev/Proposals/ControllerApi


Ignore:
Timestamp:
Jun 22, 2006, 5:11:30 PM (18 years ago)
Author:
Christopher Lenz
Comment:

Comments on the controller sandbox

Legend:

Unmodified
Added
Removed
Modified
  • TracDev/Proposals/ControllerApi

    v1 v1  
     1= Web Layer Refactoring: Controller API =
     2[[PageOutline(2-3)]]
     3
     4== Status Quo ==
     5
     6The current web layer in Trac is quite low-level. It builds directly on the component architecture, and requires a “controller” to implement the `IRequestHandler` interface and expose two methods: `match_request(req)` and `process_request(req)`. The former method is called by the request dispatcher to determine which controller should handle a particular request. The latter is then called on the controller that was chosen.
     7
     8The following example defines a (heavily simplified) controller based on that API:
     9
     10{{{
     11#!python
     12class ExampleModule(Component):
     13    implements(IRequestHandler)
     14
     15    # IRequestHandler implementation
     16
     17    def match_request(self, req):
     18        match = re.match(r'/example/(\w+)/?', req.path_info)
     19        if match:
     20            req.args['name'] = match.group(1)
     21            return True
     22        else:
     23            return False
     24
     25    def process_request(self, req):
     26        action = req.args.get('action', 'view')
     27        if req.method == 'POST':
     28            if action == 'edit':
     29                self._do_save(self, req)
     30            elif action == 'delete':
     31                self._do_delete(self, req)
     32        else:
     33            if action == 'edit':
     34                self._render_form(self, req)
     35            elif action == 'delete':
     36                self._render_confirm(self, req)
     37            else:
     38                self._render_view(self, req)
     39
     40        add_stylesheet(req, 'example.css')
     41        return ('example.cs', None)
     42
     43    # Internal methods
     44
     45    def _do_save(self, req):
     46        name = req.args.get('name')
     47        # process the form submission
     48
     49    def _do_save(self, req):
     50        name = req.args.get('name')
     51        # process the form submission
     52
     53    def _render_confirm(self, req):
     54        name = req.args.get('name')
     55        req.hdf['title'] = 'Confirm deletion:'
     56
     57    def _render_editor(self, req):
     58        name = req.args.get('name')
     59        req.hdf['title'] = 'Edit me:'
     60
     61    def _render_view(self, req):
     62        name = req.args.get('name')
     63        req.hdf['title'] = 'An example: %s' % name
     64}}}
     65
     66As shown in this example, pretty much every implementation of `process_request()` dispatches to one of several internal methods depending on the request method and the action parameter. Furthermore, the different `match_request()` methods always check the request path info against a regular expression, and maybe set parameters depend on some parts of that path (for example, “/ticket/123” results in the parameter “id” being set to “123”).
     67
     68Either in the main `process_request()` function, or in the individual ''action handlers'', a common task is to extract the required parameters from the request for further processing. The method then returns the template (and optionally the content type) to use for rendering the response.
     69
     70== Proposal: Controller Class ==
     71
     72This proposal extracts these common patterns from the individual controllers, and introduces a base class called `Controller` that allows for more convenient handling of requests. That class extends `Component` and implements the `IRequestHandler` interface, so that a concrete controller can still directly extend other extension points, such as `INavigationContributor`.
     73
     74When a controller is derived from the {{{Controller}}} base class, it should not need to implement `match_request()` or `process_request()`. Instead:
     75 * it may define a class attribute called `url_pattern` that provides the regular expression that is tested against the request path info, and
     76 * it can define a number of different functions for handling requests that are marked with the `@Controller.action` decorator
     77
     78Consider the following example:
     79
     80{{{
     81#!python
     82class ExampleController(Controller):
     83    stylesheets = ['example.css']
     84
     85    @Controller.action(template='example_view.html')
     86    def _process_view(self, req, name, format=None):
     87        req.hdf['title'] = 'An example'
     88
     89    @Controller.action('delete', template='example_confirm.html')
     90    def _process_delete(self, req, name):
     91        if req.method == 'POST':
     92            # process the form submission, redirect if all is well
     93        req.hdf['title'] = 'Confirm deletion:'
     94
     95    @Controller.action('edit', template='example_form.html')
     96    def _process_edit(self, req, name, description=None):
     97        if req.method == 'POST':
     98            # process the form submission, redirect if all is well
     99        req.hdf['title'] = 'Edit me:'
     100}}}
     101
     102This sets up three different methods for handling requests, each decorated with the `@Controller.action` decorator. That decorator takes the name of the action as the first parameter. When the base class processes a request, it extracts the value of the action parameter (which defaults to “view” if not provided), and looks for a method that declares to handle that action. The decorator also accepts the template file name and a list of stylesheet names, so that those don't need to be setup in each method body.
     103
     104In addition, request parameters that the controller method declares as keyword arguments are automatically extracted from the request and passed as parameters (although always as string, so that sometimes the parameter value will still need to be casted).
     105
     106One difference to the current code in Trac is that both `GET` and `POST` requests are handled by the same method. This will make it easier to implement user-friendly validation where a `POST` request with validity errors results in the form being redisplayed.
     107
     108=== Conventions and Convenience Features ===
     109
     110The example controller above does not provide an explicit `url_pattern` class attribute. In that case, the controller base class uses a generic pattern based on the name of the class, passing additional path segments as a positional argument. So the request dispatcher would respond to a request like:
     111{{{
     112GET /example/foo?format=rss HTTP/1.1
     113}}}
     114
     115by invoking:
     116{{{
     117#!python
     118ExampleController(env)._process_view(req, 'foo', format='rss')
     119}}}
     120
     121Also, the name of the template does not need to be explicitly specified if it follows the following convention:
     122
     123{{{
     124[classname](_[actionname])?(_[format])?.cs
     125}}}
     126
     127For example, for the `ExampleController._process_edit` action above, the default template name would be `example_edit.cs`.
     128
     129For convenience, the `Controller` base class provides direct access to various chrome methods such as `add_link()` or `add_stylesheet()`. These can simply be invoked as instance methods and thus don't need to be explicitly imported.
     130
     131=== `ControllerMixin` ===
     132
     133While the `Controller` base class assumes that the controller is a top-level `IRequestHandler`, the action dispatching functionality is also available to other kinds of components, such as WebAdmin panels. To use this feature, a class should extend the `ControllerMixin` base class instead of `Controller`. In that case, no automatic matching against requests is made, but requests can be conveniently dispatched to actions by invoking the `process_request()` method.
     134
     135== Implementation Notes ==
     136
     137The proposed API leaves the low-level `IRequestHandler` mechanism in place, but adds a convenience layer between the request dispatcher and the concrete controller components. This means that the change can be implemented incrementally, by converting each controller individually.
     138
     139The code can be checked out from the [source:sandbox/controller] branch of the SubversionRepository.