= Web Layer Refactoring: Controller API = [[PageOutline(2-3)]] == Status Quo == The 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. The following example defines a (heavily simplified) controller based on that API: {{{ #!python class ExampleModule(Component): implements(IRequestHandler) # IRequestHandler implementation def match_request(self, req): match = re.match(r'/example/(\w+)/?', req.path_info) if match: req.args['name'] = match.group(1) return True else: return False def process_request(self, req): action = req.args.get('action', 'view') if req.method == 'POST': if action == 'edit': self._do_save(self, req) elif action == 'delete': self._do_delete(self, req) else: if action == 'edit': self._render_form(self, req) elif action == 'delete': self._render_confirm(self, req) else: self._render_view(self, req) add_stylesheet(req, 'example.css') return ('example.cs', None) # Internal methods def _do_save(self, req): name = req.args.get('name') # process the form submission def _do_delete(self, req): name = req.args.get('name') # process the form submission def _render_confirm(self, req): name = req.args.get('name') req.hdf['title'] = 'Confirm deletion:' def _render_editor(self, req): name = req.args.get('name') req.hdf['title'] = 'Edit me:' def _render_view(self, req): name = req.args.get('name') req.hdf['title'] = 'An example: %s' % name }}} As 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”). Either 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. == Proposal: Controller Class == This 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`. When a controller is derived from the {{{Controller}}} base class, it should not need to implement `match_request()` or `process_request()`. Instead: * it may define a class attribute called `url_pattern` that provides the regular expression that is tested against the request path info, and * it can define a number of different functions for handling requests that are marked with the `@Controller.action` decorator Consider the following example: {{{ #!python class ExampleController(Controller): stylesheets = ['example.css'] @Controller.action(template='example_view.html') def _process_view(self, req, name, format=None): req.hdf['title'] = 'An example' @Controller.action('delete', template='example_confirm.html') def _process_delete(self, req, name): if req.method == 'POST': # process the form submission, redirect if all is well req.hdf['title'] = 'Confirm deletion:' @Controller.action('edit', template='example_form.html') def _process_edit(self, req, name, description=None): if req.method == 'POST': # process the form submission, redirect if all is well req.hdf['title'] = 'Edit me:' }}} This 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. In 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). One 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. === Conventions and Convenience Features === The 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: {{{ GET /example/foo?format=rss HTTP/1.1 }}} by invoking: {{{ #!python ExampleController(env)._process_view(req, 'foo', format='rss') }}} Also, the name of the template does not need to be explicitly specified if it follows the following convention: {{{ [classname](_[actionname])?(_[format])?.cs }}} For example, for the `ExampleController._process_edit` action above, the default template name would be `example_edit.cs`. For 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. === `ControllerMixin` === While 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. == Implementation Notes == The 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. The code can be checked out from the [source:sandbox/controller] branch of the SubversionRepository. Note that the branch currently requires Python 2.4 due to the use of decorators, but will be backported to 2.3 before/if it gets merged. ---- See also: [wiki:TracDev/Proposals], TracDev