/circuits/web/dispatchers/routes.py

https://bitbucket.org/prologic/circuits/ · Python · 176 lines · 88 code · 37 blank · 51 comment · 21 complexity · ff4186e3dc79e5b578c62a786785fc00 MD5 · raw file

  1. # Module: routes
  2. # Date: 13th September 2007
  3. # Author: James Mills, prologic at shortcircuit dot net dot au
  4. """Routes
  5. This module implements a routes based dispatcher.
  6. """
  7. from circuits.tools import tryimport
  8. routes = tryimport("routes")
  9. from circuits import handler, BaseComponent
  10. from cgi import FieldStorage
  11. from circuits.web.errors import HTTPError
  12. from circuits.web.utils import parseQueryString, dictform
  13. class RoutesError(Exception):
  14. pass
  15. class Routes(BaseComponent):
  16. """A Routes Dispatcher
  17. A dispatcher that uses the routes module to perform controller
  18. lookups for a url using pattern matching rules (connections).
  19. See: http://routes.groovie.org/docs/ for more information on Routes.
  20. """
  21. channel = "web"
  22. def __init__(self, add_default_connection=False, **kwargs):
  23. """
  24. If add_default_connection is True then a default connection
  25. of /{controller}/{action} will be added as part of
  26. the instantiation. Note that because Routes are order dependant
  27. this could obscure similar connections added at
  28. a later date, hence why it is disabled by default.
  29. """
  30. super(Routes, self).__init__(**kwargs)
  31. if routes is None:
  32. raise RuntimeError("No routes support available")
  33. # An index of controllers used by the Routes Mapper when performing
  34. # matches
  35. self.controllers = {}
  36. self.mapper = routes.Mapper(controller_scan=self.controllers.keys)
  37. if add_default_connection:
  38. self.connect("default", "/{controller}/{action}")
  39. def connect(self, name, route, **kwargs):
  40. """
  41. Connect a route to a controller
  42. Supply a name for the route and then the route itself.
  43. Naming the route allows it to be referred to easily
  44. elsewhere in the system (e.g. for generating urls).
  45. The route is the url pattern you wish to map.
  46. """
  47. controller = kwargs.pop('controller', None)
  48. if controller is not None:
  49. if not isinstance(controller, basestring):
  50. try:
  51. controller = getattr(controller, 'channel')
  52. except AttributeError:
  53. raise RoutesError("Controller %s must be " + \
  54. "either a string or have a 'channel' property " + \
  55. "defined." % controller)
  56. controller = controller.strip("/")
  57. self.mapper.connect(name, route, controller=controller, **kwargs)
  58. def _parseBody(self, request, response, params):
  59. body = request.body
  60. headers = request.headers
  61. if "Content-Type" not in headers:
  62. headers["Content-Type"] = ""
  63. try:
  64. form = FieldStorage(fp=body,
  65. headers=headers,
  66. environ={"REQUEST_METHOD": "POST"},
  67. keep_blank_values=True)
  68. except Exception as e:
  69. if e.__class__.__name__ == 'MaxSizeExceeded':
  70. # Post data is too big
  71. return HTTPError(request, response, 413)
  72. else:
  73. raise
  74. if form.file:
  75. request.body = form.file
  76. else:
  77. params.update(dictform(form))
  78. return True
  79. def _getChannel(self, request):
  80. """Find and return an appropriate channel for the given request.
  81. The channel is found by consulting the Routes mapper to return a
  82. matching controller (target) and action (channel) along with a
  83. dictionary of additional parameters to be added to the request.
  84. """
  85. path = request.path
  86. request.index = path.endswith("/")
  87. # setup routes singleton config instance
  88. config = routes.request_config()
  89. config.mapper = self.mapper
  90. config.host = request.headers.get('Host', None)
  91. config.protocol = request.scheme
  92. # Redirect handler not supported at present
  93. config.redirect = None
  94. result = self.mapper.match(path)
  95. config.mapper_dict = result
  96. channel = None
  97. target = None
  98. vpath = []
  99. if result:
  100. target = self.controllers[result['controller']]
  101. channel = result["action"]
  102. return channel, target, vpath, result.copy()
  103. @handler("request", filter=True)
  104. def _on_request(self, event, request, response):
  105. req = event
  106. # retrieve a channel (handler) for this request
  107. channel, target, vpath, params = self._getChannel(request)
  108. if channel:
  109. # add the params from the routes match
  110. req.kwargs = params
  111. # update with any query string params
  112. req.kwargs.update(parseQueryString(request.qs))
  113. v = self._parseBody(request, response, req.kwargs)
  114. if not v:
  115. return v # MaxSizeExceeded (return the HTTPError)
  116. if vpath:
  117. req.args += tuple(vpath)
  118. return self.fire(req, channel, target=target)
  119. @handler("registered", target="*")
  120. def _on_registered(self, event, component, manager):
  121. """
  122. Listen for controllers being added to the system and add them
  123. to the controller index.
  124. """
  125. if component.channel and component.channel.startswith("/"):
  126. self.controllers[component.channel.strip("/")] = component.channel
  127. @handler("unregistered", target="*")
  128. def _on_unregistered(self, event, component, manager):
  129. """
  130. Listen for controllers being removed from the system and remove them
  131. from the controller index.
  132. """
  133. if component.channel and component.channel.startswith("/"):
  134. self.controllers.pop(component.channel.strip("/"), None)