| 1 | """Mapper and Sub-Mapper""" |
|---|
| 2 | import re |
|---|
| 3 | import sys |
|---|
| 4 | import threading |
|---|
| 5 | |
|---|
| 6 | from routes import request_config |
|---|
| 7 | from routes.lru import LRUCache |
|---|
| 8 | from routes.util import controller_scan, MatchException, RoutesException |
|---|
| 9 | from routes.route import Route |
|---|
| 10 | |
|---|
| 11 | |
|---|
| 12 | COLLECTION_ACTIONS = ['index', 'create', 'new'] |
|---|
| 13 | MEMBER_ACTIONS = ['show', 'update', 'delete', 'edit'] |
|---|
| 14 | |
|---|
| 15 | |
|---|
| 16 | def strip_slashes(name): |
|---|
| 17 | """Remove slashes from the beginning and end of a part/URL.""" |
|---|
| 18 | if name.startswith('/'): |
|---|
| 19 | name = name[1:] |
|---|
| 20 | if name.endswith('/'): |
|---|
| 21 | name = name[:-1] |
|---|
| 22 | return name |
|---|
| 23 | |
|---|
| 24 | |
|---|
| 25 | class SubMapperParent(object): |
|---|
| 26 | """Base class for Mapper and SubMapper, both of which may be the parent |
|---|
| 27 | of SubMapper objects |
|---|
| 28 | """ |
|---|
| 29 | |
|---|
| 30 | def submapper(self, **kargs): |
|---|
| 31 | """Create a partial version of the Mapper with the designated |
|---|
| 32 | options set |
|---|
| 33 | |
|---|
| 34 | This results in a :class:`routes.mapper.SubMapper` object. |
|---|
| 35 | |
|---|
| 36 | If keyword arguments provided to this method also exist in the |
|---|
| 37 | keyword arguments provided to the submapper, their values will |
|---|
| 38 | be merged with the saved options going first. |
|---|
| 39 | |
|---|
| 40 | In addition to :class:`routes.route.Route` arguments, submapper |
|---|
| 41 | can also take a ``path_prefix`` argument which will be |
|---|
| 42 | prepended to the path of all routes that are connected. |
|---|
| 43 | |
|---|
| 44 | Example:: |
|---|
| 45 | |
|---|
| 46 | >>> map = Mapper(controller_scan=None) |
|---|
| 47 | >>> map.connect('home', '/', controller='home', action='splash') |
|---|
| 48 | >>> map.matchlist[0].name == 'home' |
|---|
| 49 | True |
|---|
| 50 | >>> m = map.submapper(controller='home') |
|---|
| 51 | >>> m.connect('index', '/index', action='index') |
|---|
| 52 | >>> map.matchlist[1].name == 'index' |
|---|
| 53 | True |
|---|
| 54 | >>> map.matchlist[1].defaults['controller'] == 'home' |
|---|
| 55 | True |
|---|
| 56 | |
|---|
| 57 | Optional ``collection_name`` and ``resource_name`` arguments are |
|---|
| 58 | used in the generation of route names by the ``action`` and |
|---|
| 59 | ``link`` methods. These in turn are used by the ``index``, |
|---|
| 60 | ``new``, ``create``, ``show``, ``edit``, ``update`` and |
|---|
| 61 | ``delete`` methods which may be invoked indirectly by listing |
|---|
| 62 | them in the ``actions`` argument. If the ``formatted`` argument |
|---|
| 63 | is set to ``True`` (the default), generated paths are given the |
|---|
| 64 | suffix '{.format}' which matches or generates an optional format |
|---|
| 65 | extension. |
|---|
| 66 | |
|---|
| 67 | Example:: |
|---|
| 68 | |
|---|
| 69 | >>> from routes.util import url_for |
|---|
| 70 | >>> map = Mapper(controller_scan=None) |
|---|
| 71 | >>> m = map.submapper(path_prefix='/entries', collection_name='entries', resource_name='entry', actions=['index', 'new']) |
|---|
| 72 | >>> url_for('entries') == '/entries' |
|---|
| 73 | True |
|---|
| 74 | >>> url_for('new_entry', format='xml') == '/entries/new.xml' |
|---|
| 75 | True |
|---|
| 76 | |
|---|
| 77 | """ |
|---|
| 78 | return SubMapper(self, **kargs) |
|---|
| 79 | |
|---|
| 80 | def collection(self, collection_name, resource_name, path_prefix=None, |
|---|
| 81 | member_prefix='/{id}', controller=None, |
|---|
| 82 | collection_actions=COLLECTION_ACTIONS, |
|---|
| 83 | member_actions = MEMBER_ACTIONS, member_options=None, |
|---|
| 84 | **kwargs): |
|---|
| 85 | """Create a submapper that represents a collection. |
|---|
| 86 | |
|---|
| 87 | This results in a :class:`routes.mapper.SubMapper` object, with a |
|---|
| 88 | ``member`` property of the same type that represents the collection's |
|---|
| 89 | member resources. |
|---|
| 90 | |
|---|
| 91 | Its interface is the same as the ``submapper`` together with |
|---|
| 92 | ``member_prefix``, ``member_actions`` and ``member_options`` |
|---|
| 93 | which are passed to the ``member` submatter as ``path_prefix``, |
|---|
| 94 | ``actions`` and keyword arguments respectively. |
|---|
| 95 | |
|---|
| 96 | Example:: |
|---|
| 97 | |
|---|
| 98 | >>> from routes.util import url_for |
|---|
| 99 | >>> map = Mapper(controller_scan=None) |
|---|
| 100 | >>> c = map.collection('entries', 'entry') |
|---|
| 101 | >>> c.member.link('ping', method='POST') |
|---|
| 102 | >>> url_for('entries') == '/entries' |
|---|
| 103 | True |
|---|
| 104 | >>> url_for('edit_entry', id=1) == '/entries/1/edit' |
|---|
| 105 | True |
|---|
| 106 | >>> url_for('ping_entry', id=1) == '/entries/1/ping' |
|---|
| 107 | True |
|---|
| 108 | |
|---|
| 109 | """ |
|---|
| 110 | if controller is None: |
|---|
| 111 | controller = resource_name or collection_name |
|---|
| 112 | |
|---|
| 113 | if path_prefix is None: |
|---|
| 114 | path_prefix = '/' + collection_name |
|---|
| 115 | |
|---|
| 116 | collection = SubMapper(self, collection_name=collection_name, |
|---|
| 117 | resource_name=resource_name, |
|---|
| 118 | path_prefix=path_prefix, controller=controller, |
|---|
| 119 | actions=collection_actions, **kwargs) |
|---|
| 120 | |
|---|
| 121 | collection.member = SubMapper(collection, path_prefix=member_prefix, |
|---|
| 122 | actions=member_actions, |
|---|
| 123 | **(member_options or {})) |
|---|
| 124 | |
|---|
| 125 | return collection |
|---|
| 126 | |
|---|
| 127 | |
|---|
| 128 | class SubMapper(SubMapperParent): |
|---|
| 129 | """Partial mapper for use with_options""" |
|---|
| 130 | def __init__(self, obj, resource_name=None, collection_name=None, |
|---|
| 131 | actions=None, formatted=None, **kwargs): |
|---|
| 132 | self.kwargs = kwargs |
|---|
| 133 | self.obj = obj |
|---|
| 134 | self.collection_name = collection_name |
|---|
| 135 | self.member = None |
|---|
| 136 | self.resource_name = resource_name \ |
|---|
| 137 | or getattr(obj, 'resource_name', None) \ |
|---|
| 138 | or kwargs.get('controller', None) \ |
|---|
| 139 | or getattr(obj, 'controller', None) |
|---|
| 140 | if formatted is not None: |
|---|
| 141 | self.formatted = formatted |
|---|
| 142 | else: |
|---|
| 143 | self.formatted = getattr(obj, 'formatted', None) |
|---|
| 144 | if self.formatted is None: |
|---|
| 145 | self.formatted = True |
|---|
| 146 | |
|---|
| 147 | self.add_actions(actions or []) |
|---|
| 148 | |
|---|
| 149 | def connect(self, *args, **kwargs): |
|---|
| 150 | newkargs = {} |
|---|
| 151 | newargs = args |
|---|
| 152 | for key, value in self.kwargs.items(): |
|---|
| 153 | if key == 'path_prefix': |
|---|
| 154 | if len(args) > 1: |
|---|
| 155 | newargs = (args[0], self.kwargs[key] + args[1]) |
|---|
| 156 | else: |
|---|
| 157 | newargs = (self.kwargs[key] + args[0],) |
|---|
| 158 | elif key in kwargs: |
|---|
| 159 | if isinstance(value, dict): |
|---|
| 160 | newkargs[key] = dict(value, **kwargs[key]) # merge dicts |
|---|
| 161 | else: |
|---|
| 162 | newkargs[key] = value + kwargs[key] |
|---|
| 163 | else: |
|---|
| 164 | newkargs[key] = self.kwargs[key] |
|---|
| 165 | for key in kwargs: |
|---|
| 166 | if key not in self.kwargs: |
|---|
| 167 | newkargs[key] = kwargs[key] |
|---|
| 168 | return self.obj.connect(*newargs, **newkargs) |
|---|
| 169 | |
|---|
| 170 | def link(self, rel=None, name=None, action=None, method='GET', |
|---|
| 171 | formatted=None, **kwargs): |
|---|
| 172 | """Generates a named route for a subresource. |
|---|
| 173 | |
|---|
| 174 | Example:: |
|---|
| 175 | |
|---|
| 176 | >>> from routes.util import url_for |
|---|
| 177 | >>> map = Mapper(controller_scan=None) |
|---|
| 178 | >>> c = map.collection('entries', 'entry') |
|---|
| 179 | >>> c.link('recent', name='recent_entries') |
|---|
| 180 | >>> c.member.link('ping', method='POST', formatted=True) |
|---|
| 181 | >>> url_for('entries') == '/entries' |
|---|
| 182 | True |
|---|
| 183 | >>> url_for('recent_entries') == '/entries/recent' |
|---|
| 184 | True |
|---|
| 185 | >>> url_for('ping_entry', id=1) == '/entries/1/ping' |
|---|
| 186 | True |
|---|
| 187 | >>> url_for('ping_entry', id=1, format='xml') == '/entries/1/ping.xml' |
|---|
| 188 | True |
|---|
| 189 | |
|---|
| 190 | """ |
|---|
| 191 | if formatted or (formatted is None and self.formatted): |
|---|
| 192 | suffix = '{.format}' |
|---|
| 193 | else: |
|---|
| 194 | suffix = '' |
|---|
| 195 | |
|---|
| 196 | return self.connect(name or (rel + '_' + self.resource_name), |
|---|
| 197 | '/' + (rel or name) + suffix, |
|---|
| 198 | action=action or rel or name, |
|---|
| 199 | **_kwargs_with_conditions(kwargs, method)) |
|---|
| 200 | |
|---|
| 201 | def new(self, **kwargs): |
|---|
| 202 | """Generates the "new" link for a collection submapper.""" |
|---|
| 203 | return self.link(rel='new', **kwargs) |
|---|
| 204 | |
|---|
| 205 | def edit(self, **kwargs): |
|---|
| 206 | """Generates the "edit" link for a collection member submapper.""" |
|---|
| 207 | return self.link(rel='edit', **kwargs) |
|---|
| 208 | |
|---|
| 209 | def action(self, name=None, action=None, method='GET', formatted=None, |
|---|
| 210 | **kwargs): |
|---|
| 211 | """Generates a named route at the base path of a submapper. |
|---|
| 212 | |
|---|
| 213 | Example:: |
|---|
| 214 | |
|---|
| 215 | >>> from routes import url_for |
|---|
| 216 | >>> map = Mapper(controller_scan=None) |
|---|
| 217 | >>> c = map.submapper(path_prefix='/entries', controller='entry') |
|---|
| 218 | >>> c.action(action='index', name='entries', formatted=True) |
|---|
| 219 | >>> c.action(action='create', method='POST') |
|---|
| 220 | >>> url_for(controller='entry', action='index', method='GET') == '/entries' |
|---|
| 221 | True |
|---|
| 222 | >>> url_for(controller='entry', action='index', method='GET', format='xml') == '/entries.xml' |
|---|
| 223 | True |
|---|
| 224 | >>> url_for(controller='entry', action='create', method='POST') == '/entries' |
|---|
| 225 | True |
|---|
| 226 | |
|---|
| 227 | """ |
|---|
| 228 | if formatted or (formatted is None and self.formatted): |
|---|
| 229 | suffix = '{.format}' |
|---|
| 230 | else: |
|---|
| 231 | suffix = '' |
|---|
| 232 | return self.connect(name or (action + '_' + self.resource_name), |
|---|
| 233 | suffix, |
|---|
| 234 | action=action or name, |
|---|
| 235 | **_kwargs_with_conditions(kwargs, method)) |
|---|
| 236 | |
|---|
| 237 | def index(self, name=None, **kwargs): |
|---|
| 238 | """Generates the "index" action for a collection submapper.""" |
|---|
| 239 | return self.action(name=name or self.collection_name, |
|---|
| 240 | action='index', method='GET', **kwargs) |
|---|
| 241 | |
|---|
| 242 | def show(self, name = None, **kwargs): |
|---|
| 243 | """Generates the "show" action for a collection member submapper.""" |
|---|
| 244 | return self.action(name=name or self.resource_name, |
|---|
| 245 | action='show', method='GET', **kwargs) |
|---|
| 246 | |
|---|
| 247 | def create(self, **kwargs): |
|---|
| 248 | """Generates the "create" action for a collection submapper.""" |
|---|
| 249 | return self.action(action='create', method='POST', **kwargs) |
|---|
| 250 | |
|---|
| 251 | def update(self, **kwargs): |
|---|
| 252 | """Generates the "update" action for a collection member submapper.""" |
|---|
| 253 | return self.action(action='update', method='PUT', **kwargs) |
|---|
| 254 | |
|---|
| 255 | def delete(self, **kwargs): |
|---|
| 256 | """Generates the "delete" action for a collection member submapper.""" |
|---|
| 257 | return self.action(action='delete', method='DELETE', **kwargs) |
|---|
| 258 | |
|---|
| 259 | def add_actions(self, actions): |
|---|
| 260 | [getattr(self, action)() for action in actions] |
|---|
| 261 | |
|---|
| 262 | # Provided for those who prefer using the 'with' syntax in Python 2.5+ |
|---|
| 263 | def __enter__(self): |
|---|
| 264 | return self |
|---|
| 265 | |
|---|
| 266 | def __exit__(self, type, value, tb): |
|---|
| 267 | pass |
|---|
| 268 | |
|---|
| 269 | # Create kwargs with a 'conditions' member generated for the given method |
|---|
| 270 | def _kwargs_with_conditions(kwargs, method): |
|---|
| 271 | if method and 'conditions' not in kwargs: |
|---|
| 272 | newkwargs = kwargs.copy() |
|---|
| 273 | newkwargs['conditions'] = {'method': method} |
|---|
| 274 | return newkwargs |
|---|
| 275 | else: |
|---|
| 276 | return kwargs |
|---|
| 277 | |
|---|
| 278 | |
|---|
| 279 | |
|---|
| 280 | class Mapper(SubMapperParent): |
|---|
| 281 | """Mapper handles URL generation and URL recognition in a web |
|---|
| 282 | application. |
|---|
| 283 | |
|---|
| 284 | Mapper is built handling dictionary's. It is assumed that the web |
|---|
| 285 | application will handle the dictionary returned by URL recognition |
|---|
| 286 | to dispatch appropriately. |
|---|
| 287 | |
|---|
| 288 | URL generation is done by passing keyword parameters into the |
|---|
| 289 | generate function, a URL is then returned. |
|---|
| 290 | |
|---|
| 291 | """ |
|---|
| 292 | def __init__(self, controller_scan=controller_scan, directory=None, |
|---|
| 293 | always_scan=False, register=True, explicit=True): |
|---|
| 294 | """Create a new Mapper instance |
|---|
| 295 | |
|---|
| 296 | All keyword arguments are optional. |
|---|
| 297 | |
|---|
| 298 | ``controller_scan`` |
|---|
| 299 | Function reference that will be used to return a list of |
|---|
| 300 | valid controllers used during URL matching. If |
|---|
| 301 | ``directory`` keyword arg is present, it will be passed |
|---|
| 302 | into the function during its call. This option defaults to |
|---|
| 303 | a function that will scan a directory for controllers. |
|---|
| 304 | |
|---|
| 305 | Alternatively, a list of controllers or None can be passed |
|---|
| 306 | in which are assumed to be the definitive list of |
|---|
| 307 | controller names valid when matching 'controller'. |
|---|
| 308 | |
|---|
| 309 | ``directory`` |
|---|
| 310 | Passed into controller_scan for the directory to scan. It |
|---|
| 311 | should be an absolute path if using the default |
|---|
| 312 | ``controller_scan`` function. |
|---|
| 313 | |
|---|
| 314 | ``always_scan`` |
|---|
| 315 | Whether or not the ``controller_scan`` function should be |
|---|
| 316 | run during every URL match. This is typically a good idea |
|---|
| 317 | during development so the server won't need to be restarted |
|---|
| 318 | anytime a controller is added. |
|---|
| 319 | |
|---|
| 320 | ``register`` |
|---|
| 321 | Boolean used to determine if the Mapper should use |
|---|
| 322 | ``request_config`` to register itself as the mapper. Since |
|---|
| 323 | it's done on a thread-local basis, this is typically best |
|---|
| 324 | used during testing though it won't hurt in other cases. |
|---|
| 325 | |
|---|
| 326 | ``explicit`` |
|---|
| 327 | Boolean used to determine if routes should be connected |
|---|
| 328 | with implicit defaults of:: |
|---|
| 329 | |
|---|
| 330 | {'controller':'content','action':'index','id':None} |
|---|
| 331 | |
|---|
| 332 | When set to True, these defaults will not be added to route |
|---|
| 333 | connections and ``url_for`` will not use Route memory. |
|---|
| 334 | |
|---|
| 335 | Additional attributes that may be set after mapper |
|---|
| 336 | initialization (ie, map.ATTRIBUTE = 'something'): |
|---|
| 337 | |
|---|
| 338 | ``encoding`` |
|---|
| 339 | Used to indicate alternative encoding/decoding systems to |
|---|
| 340 | use with both incoming URL's, and during Route generation |
|---|
| 341 | when passed a Unicode string. Defaults to 'utf-8'. |
|---|
| 342 | |
|---|
| 343 | ``decode_errors`` |
|---|
| 344 | How to handle errors in the encoding, generally ignoring |
|---|
| 345 | any chars that don't convert should be sufficient. Defaults |
|---|
| 346 | to 'ignore'. |
|---|
| 347 | |
|---|
| 348 | ``minimization`` |
|---|
| 349 | Boolean used to indicate whether or not Routes should |
|---|
| 350 | minimize URL's and the generated URL's, or require every |
|---|
| 351 | part where it appears in the path. Defaults to True. |
|---|
| 352 | |
|---|
| 353 | ``hardcode_names`` |
|---|
| 354 | Whether or not Named Routes result in the default options |
|---|
| 355 | for the route being used *or* if they actually force url |
|---|
| 356 | generation to use the route. Defaults to False. |
|---|
| 357 | |
|---|
| 358 | """ |
|---|
| 359 | self.matchlist = [] |
|---|
| 360 | self.maxkeys = {} |
|---|
| 361 | self.minkeys = {} |
|---|
| 362 | self.urlcache = LRUCache(1600) |
|---|
| 363 | self._created_regs = False |
|---|
| 364 | self._created_gens = False |
|---|
| 365 | self._master_regexp = None |
|---|
| 366 | self.prefix = None |
|---|
| 367 | self.req_data = threading.local() |
|---|
| 368 | self.directory = directory |
|---|
| 369 | self.always_scan = always_scan |
|---|
| 370 | self.controller_scan = controller_scan |
|---|
| 371 | self._regprefix = None |
|---|
| 372 | self._routenames = {} |
|---|
| 373 | self.debug = False |
|---|
| 374 | self.append_slash = False |
|---|
| 375 | self.sub_domains = False |
|---|
| 376 | self.sub_domains_ignore = [] |
|---|
| 377 | self.domain_match = '[^\.\/]+?\.[^\.\/]+' |
|---|
| 378 | self.explicit = explicit |
|---|
| 379 | self.encoding = 'utf-8' |
|---|
| 380 | self.decode_errors = 'ignore' |
|---|
| 381 | self.hardcode_names = True |
|---|
| 382 | self.minimization = False |
|---|
| 383 | self.create_regs_lock = threading.Lock() |
|---|
| 384 | if register: |
|---|
| 385 | config = request_config() |
|---|
| 386 | config.mapper = self |
|---|
| 387 | |
|---|
| 388 | def __str__(self): |
|---|
| 389 | """Generates a tabular string representation.""" |
|---|
| 390 | def format_methods(r): |
|---|
| 391 | if r.conditions: |
|---|
| 392 | method = r.conditions.get('method', '') |
|---|
| 393 | return type(method) is str and method or ', '.join(method) |
|---|
| 394 | else: |
|---|
| 395 | return '' |
|---|
| 396 | |
|---|
| 397 | table = [('Route name', 'Methods', 'Path')] + \ |
|---|
| 398 | [(r.name or '', format_methods(r), r.routepath or '') |
|---|
| 399 | for r in self.matchlist] |
|---|
| 400 | |
|---|
| 401 | widths = [max(len(row[col]) for row in table) |
|---|
| 402 | for col in range(len(table[0]))] |
|---|
| 403 | |
|---|
| 404 | return '\n'.join( |
|---|
| 405 | ' '.join(row[col].ljust(widths[col]) |
|---|
| 406 | for col in range(len(widths))) |
|---|
| 407 | for row in table) |
|---|
| 408 | |
|---|
| 409 | def _envget(self): |
|---|
| 410 | try: |
|---|
| 411 | return self.req_data.environ |
|---|
| 412 | except AttributeError: |
|---|
| 413 | return None |
|---|
| 414 | def _envset(self, env): |
|---|
| 415 | self.req_data.environ = env |
|---|
| 416 | def _envdel(self): |
|---|
| 417 | del self.req_data.environ |
|---|
| 418 | environ = property(_envget, _envset, _envdel) |
|---|
| 419 | |
|---|
| 420 | def extend(self, routes, path_prefix=''): |
|---|
| 421 | """Extends the mapper routes with a list of Route objects |
|---|
| 422 | |
|---|
| 423 | If a path_prefix is provided, all the routes will have their |
|---|
| 424 | path prepended with the path_prefix. |
|---|
| 425 | |
|---|
| 426 | Example:: |
|---|
| 427 | |
|---|
| 428 | >>> map = Mapper(controller_scan=None) |
|---|
| 429 | >>> map.connect('home', '/', controller='home', action='splash') |
|---|
| 430 | >>> map.matchlist[0].name == 'home' |
|---|
| 431 | True |
|---|
| 432 | >>> routes = [Route('index', '/index.htm', controller='home', |
|---|
| 433 | ... action='index')] |
|---|
| 434 | >>> map.extend(routes) |
|---|
| 435 | >>> len(map.matchlist) == 2 |
|---|
| 436 | True |
|---|
| 437 | >>> map.extend(routes, path_prefix='/subapp') |
|---|
| 438 | >>> len(map.matchlist) == 3 |
|---|
| 439 | True |
|---|
| 440 | >>> map.matchlist[2].routepath == '/subapp/index.htm' |
|---|
| 441 | True |
|---|
| 442 | |
|---|
| 443 | .. note:: |
|---|
| 444 | |
|---|
| 445 | This function does not merely extend the mapper with the |
|---|
| 446 | given list of routes, it actually creates new routes with |
|---|
| 447 | identical calling arguments. |
|---|
| 448 | |
|---|
| 449 | """ |
|---|
| 450 | for route in routes: |
|---|
| 451 | if path_prefix and route.minimization: |
|---|
| 452 | routepath = '/'.join([path_prefix, route.routepath]) |
|---|
| 453 | elif path_prefix: |
|---|
| 454 | routepath = path_prefix + route.routepath |
|---|
| 455 | else: |
|---|
| 456 | routepath = route.routepath |
|---|
| 457 | self.connect(route.name, routepath, **route._kargs) |
|---|
| 458 | |
|---|
| 459 | def connect(self, *args, **kargs): |
|---|
| 460 | """Create and connect a new Route to the Mapper. |
|---|
| 461 | |
|---|
| 462 | Usage: |
|---|
| 463 | |
|---|
| 464 | .. code-block:: python |
|---|
| 465 | |
|---|
| 466 | m = Mapper() |
|---|
| 467 | m.connect(':controller/:action/:id') |
|---|
| 468 | m.connect('date/:year/:month/:day', controller="blog", action="view") |
|---|
| 469 | m.connect('archives/:page', controller="blog", action="by_page", |
|---|
| 470 | requirements = { 'page':'\d{1,2}' }) |
|---|
| 471 | m.connect('category_list', 'archives/category/:section', controller='blog', action='category', |
|---|
| 472 | section='home', type='list') |
|---|
| 473 | m.connect('home', '', controller='blog', action='view', section='home') |
|---|
| 474 | |
|---|
| 475 | """ |
|---|
| 476 | routename = None |
|---|
| 477 | if len(args) > 1: |
|---|
| 478 | routename = args[0] |
|---|
| 479 | else: |
|---|
| 480 | args = (None,) + args |
|---|
| 481 | if '_explicit' not in kargs: |
|---|
| 482 | kargs['_explicit'] = self.explicit |
|---|
| 483 | if '_minimize' not in kargs: |
|---|
| 484 | kargs['_minimize'] = self.minimization |
|---|
| 485 | route = Route(*args, **kargs) |
|---|
| 486 | |
|---|
| 487 | # Apply encoding and errors if its not the defaults and the route |
|---|
| 488 | # didn't have one passed in. |
|---|
| 489 | if (self.encoding != 'utf-8' or self.decode_errors != 'ignore') and \ |
|---|
| 490 | '_encoding' not in kargs: |
|---|
| 491 | route.encoding = self.encoding |
|---|
| 492 | route.decode_errors = self.decode_errors |
|---|
| 493 | |
|---|
| 494 | if not route.static: |
|---|
| 495 | self.matchlist.append(route) |
|---|
| 496 | |
|---|
| 497 | if routename: |
|---|
| 498 | self._routenames[routename] = route |
|---|
| 499 | route.name = routename |
|---|
| 500 | if route.static: |
|---|
| 501 | return |
|---|
| 502 | exists = False |
|---|
| 503 | for key in self.maxkeys: |
|---|
| 504 | if key == route.maxkeys: |
|---|
| 505 | self.maxkeys[key].append(route) |
|---|
| 506 | exists = True |
|---|
| 507 | break |
|---|
| 508 | if not exists: |
|---|
| 509 | self.maxkeys[route.maxkeys] = [route] |
|---|
| 510 | self._created_gens = False |
|---|
| 511 | |
|---|
| 512 | def _create_gens(self): |
|---|
| 513 | """Create the generation hashes for route lookups""" |
|---|
| 514 | # Use keys temporailly to assemble the list to avoid excessive |
|---|
| 515 | # list iteration testing with "in" |
|---|
| 516 | controllerlist = {} |
|---|
| 517 | actionlist = {} |
|---|
| 518 | |
|---|
| 519 | # Assemble all the hardcoded/defaulted actions/controllers used |
|---|
| 520 | for route in self.matchlist: |
|---|
| 521 | if route.static: |
|---|
| 522 | continue |
|---|
| 523 | if route.defaults.has_key('controller'): |
|---|
| 524 | controllerlist[route.defaults['controller']] = True |
|---|
| 525 | if route.defaults.has_key('action'): |
|---|
| 526 | actionlist[route.defaults['action']] = True |
|---|
| 527 | |
|---|
| 528 | # Setup the lists of all controllers/actions we'll add each route |
|---|
| 529 | # to. We include the '*' in the case that a generate contains a |
|---|
| 530 | # controller/action that has no hardcodes |
|---|
| 531 | controllerlist = controllerlist.keys() + ['*'] |
|---|
| 532 | actionlist = actionlist.keys() + ['*'] |
|---|
| 533 | |
|---|
| 534 | # Go through our list again, assemble the controllers/actions we'll |
|---|
| 535 | # add each route to. If its hardcoded, we only add it to that dict key. |
|---|
| 536 | # Otherwise we add it to every hardcode since it can be changed. |
|---|
| 537 | gendict = {} # Our generated two-deep hash |
|---|
| 538 | for route in self.matchlist: |
|---|
| 539 | if route.static: |
|---|
| 540 | continue |
|---|
| 541 | clist = controllerlist |
|---|
| 542 | alist = actionlist |
|---|
| 543 | if 'controller' in route.hardcoded: |
|---|
| 544 | clist = [route.defaults['controller']] |
|---|
| 545 | if 'action' in route.hardcoded: |
|---|
| 546 | alist = [unicode(route.defaults['action'])] |
|---|
| 547 | for controller in clist: |
|---|
| 548 | for action in alist: |
|---|
| 549 | actiondict = gendict.setdefault(controller, {}) |
|---|
| 550 | actiondict.setdefault(action, ([], {}))[0].append(route) |
|---|
| 551 | self._gendict = gendict |
|---|
| 552 | self._created_gens = True |
|---|
| 553 | |
|---|
| 554 | def create_regs(self, *args, **kwargs): |
|---|
| 555 | """Atomically creates regular expressions for all connected |
|---|
| 556 | routes |
|---|
| 557 | """ |
|---|
| 558 | self.create_regs_lock.acquire() |
|---|
| 559 | try: |
|---|
| 560 | self._create_regs(*args, **kwargs) |
|---|
| 561 | finally: |
|---|
| 562 | self.create_regs_lock.release() |
|---|
| 563 | |
|---|
| 564 | def _create_regs(self, clist=None): |
|---|
| 565 | """Creates regular expressions for all connected routes""" |
|---|
| 566 | if clist is None: |
|---|
| 567 | if self.directory: |
|---|
| 568 | clist = self.controller_scan(self.directory) |
|---|
| 569 | elif callable(self.controller_scan): |
|---|
| 570 | clist = self.controller_scan() |
|---|
| 571 | elif not self.controller_scan: |
|---|
| 572 | clist = [] |
|---|
| 573 | else: |
|---|
| 574 | clist = self.controller_scan |
|---|
| 575 | |
|---|
| 576 | for key, val in self.maxkeys.iteritems(): |
|---|
| 577 | for route in val: |
|---|
| 578 | route.makeregexp(clist) |
|---|
| 579 | |
|---|
| 580 | regexps = [] |
|---|
| 581 | routematches = [] |
|---|
| 582 | for route in self.matchlist: |
|---|
| 583 | if not route.static: |
|---|
| 584 | routematches.append(route) |
|---|
| 585 | regexps.append(route.makeregexp(clist, include_names=False)) |
|---|
| 586 | self._routematches = routematches |
|---|
| 587 | |
|---|
| 588 | # Create our regexp to strip the prefix |
|---|
| 589 | if self.prefix: |
|---|
| 590 | self._regprefix = re.compile(self.prefix + '(.*)') |
|---|
| 591 | |
|---|
| 592 | # Save the master regexp |
|---|
| 593 | regexp = '|'.join(['(?:%s)' % x for x in regexps]) |
|---|
| 594 | self._master_reg = regexp |
|---|
| 595 | self._master_regexp = re.compile(regexp) |
|---|
| 596 | self._created_regs = True |
|---|
| 597 | |
|---|
| 598 | def _match(self, url, environ): |
|---|
| 599 | """Internal Route matcher |
|---|
| 600 | |
|---|
| 601 | Matches a URL against a route, and returns a tuple of the match |
|---|
| 602 | dict and the route object if a match is successfull, otherwise |
|---|
| 603 | it returns empty. |
|---|
| 604 | |
|---|
| 605 | For internal use only. |
|---|
| 606 | |
|---|
| 607 | """ |
|---|
| 608 | if not self._created_regs and self.controller_scan: |
|---|
| 609 | self.create_regs() |
|---|
| 610 | elif not self._created_regs: |
|---|
| 611 | raise RoutesException("You must generate the regular expressions" |
|---|
| 612 | " before matching.") |
|---|
| 613 | |
|---|
| 614 | if self.always_scan: |
|---|
| 615 | self.create_regs() |
|---|
| 616 | |
|---|
| 617 | matchlog = [] |
|---|
| 618 | if self.prefix: |
|---|
| 619 | if re.match(self._regprefix, url): |
|---|
| 620 | url = re.sub(self._regprefix, r'\1', url) |
|---|
| 621 | if not url: |
|---|
| 622 | url = '/' |
|---|
| 623 | else: |
|---|
| 624 | return (None, None, matchlog) |
|---|
| 625 | |
|---|
| 626 | environ = environ or self.environ |
|---|
| 627 | sub_domains = self.sub_domains |
|---|
| 628 | sub_domains_ignore = self.sub_domains_ignore |
|---|
| 629 | domain_match = self.domain_match |
|---|
| 630 | debug = self.debug |
|---|
| 631 | |
|---|
| 632 | # Check to see if its a valid url against the main regexp |
|---|
| 633 | # Done for faster invalid URL elimination |
|---|
| 634 | valid_url = re.match(self._master_regexp, url) |
|---|
| 635 | if not valid_url: |
|---|
| 636 | return (None, None, matchlog) |
|---|
| 637 | |
|---|
| 638 | for route in self.matchlist: |
|---|
| 639 | if route.static: |
|---|
| 640 | if debug: |
|---|
| 641 | matchlog.append(dict(route=route, static=True)) |
|---|
| 642 | continue |
|---|
| 643 | match = route.match(url, environ, sub_domains, sub_domains_ignore, |
|---|
| 644 | domain_match) |
|---|
| 645 | if debug: |
|---|
| 646 | matchlog.append(dict(route=route, regexp=bool(match))) |
|---|
| 647 | if isinstance(match, dict) or match: |
|---|
| 648 | return (match, route, matchlog) |
|---|
| 649 | return (None, None, matchlog) |
|---|
| 650 | |
|---|
| 651 | def match(self, url=None, environ=None): |
|---|
| 652 | """Match a URL against against one of the routes contained. |
|---|
| 653 | |
|---|
| 654 | Will return None if no valid match is found. |
|---|
| 655 | |
|---|
| 656 | .. code-block:: python |
|---|
| 657 | |
|---|
| 658 | resultdict = m.match('/joe/sixpack') |
|---|
| 659 | |
|---|
| 660 | """ |
|---|
| 661 | if not url and not environ: |
|---|
| 662 | raise RoutesException('URL or environ must be provided') |
|---|
| 663 | |
|---|
| 664 | if not url: |
|---|
| 665 | url = environ['PATH_INFO'] |
|---|
| 666 | |
|---|
| 667 | result = self._match(url, environ) |
|---|
| 668 | if self.debug: |
|---|
| 669 | return result[0], result[1], result[2] |
|---|
| 670 | if isinstance(result[0], dict) or result[0]: |
|---|
| 671 | return result[0] |
|---|
| 672 | return None |
|---|
| 673 | |
|---|
| 674 | def routematch(self, url=None, environ=None): |
|---|
| 675 | """Match a URL against against one of the routes contained. |
|---|
| 676 | |
|---|
| 677 | Will return None if no valid match is found, otherwise a |
|---|
| 678 | result dict and a route object is returned. |
|---|
| 679 | |
|---|
| 680 | .. code-block:: python |
|---|
| 681 | |
|---|
| 682 | resultdict, route_obj = m.match('/joe/sixpack') |
|---|
| 683 | |
|---|
| 684 | """ |
|---|
| 685 | if not url and not environ: |
|---|
| 686 | raise RoutesException('URL or environ must be provided') |
|---|
| 687 | |
|---|
| 688 | if not url: |
|---|
| 689 | url = environ['PATH_INFO'] |
|---|
| 690 | result = self._match(url, environ) |
|---|
| 691 | if self.debug: |
|---|
| 692 | return result[0], result[1], result[2] |
|---|
| 693 | if isinstance(result[0], dict) or result[0]: |
|---|
| 694 | return result[0], result[1] |
|---|
| 695 | return None |
|---|
| 696 | |
|---|
| 697 | def generate(self, *args, **kargs): |
|---|
| 698 | """Generate a route from a set of keywords |
|---|
| 699 | |
|---|
| 700 | Returns the url text, or None if no URL could be generated. |
|---|
| 701 | |
|---|
| 702 | .. code-block:: python |
|---|
| 703 | |
|---|
| 704 | m.generate(controller='content',action='view',id=10) |
|---|
| 705 | |
|---|
| 706 | """ |
|---|
| 707 | # Generate ourself if we haven't already |
|---|
| 708 | if not self._created_gens: |
|---|
| 709 | self._create_gens() |
|---|
| 710 | |
|---|
| 711 | if self.append_slash: |
|---|
| 712 | kargs['_append_slash'] = True |
|---|
| 713 | |
|---|
| 714 | if not self.explicit: |
|---|
| 715 | if 'controller' not in kargs: |
|---|
| 716 | kargs['controller'] = 'content' |
|---|
| 717 | if 'action' not in kargs: |
|---|
| 718 | kargs['action'] = 'index' |
|---|
| 719 | |
|---|
| 720 | environ = kargs.pop('_environ', self.environ) |
|---|
| 721 | controller = kargs.get('controller', None) |
|---|
| 722 | action = kargs.get('action', None) |
|---|
| 723 | |
|---|
| 724 | # If the URL didn't depend on the SCRIPT_NAME, we'll cache it |
|---|
| 725 | # keyed by just by kargs; otherwise we need to cache it with |
|---|
| 726 | # both SCRIPT_NAME and kargs: |
|---|
| 727 | cache_key = unicode(args).encode('utf8') + \ |
|---|
| 728 | unicode(kargs).encode('utf8') |
|---|
| 729 | |
|---|
| 730 | if self.urlcache is not None: |
|---|
| 731 | if self.environ: |
|---|
| 732 | cache_key_script_name = '%s:%s' % ( |
|---|
| 733 | environ.get('SCRIPT_NAME', ''), cache_key) |
|---|
| 734 | else: |
|---|
| 735 | cache_key_script_name = cache_key |
|---|
| 736 | |
|---|
| 737 | # Check the url cache to see if it exists, use it if it does |
|---|
| 738 | for key in [cache_key, cache_key_script_name]: |
|---|
| 739 | if key in self.urlcache: |
|---|
| 740 | return self.urlcache[key] |
|---|
| 741 | |
|---|
| 742 | actionlist = self._gendict.get(controller) or self._gendict.get('*', {}) |
|---|
| 743 | if not actionlist and not args: |
|---|
| 744 | return None |
|---|
| 745 | (keylist, sortcache) = actionlist.get(action) or \ |
|---|
| 746 | actionlist.get('*', (None, {})) |
|---|
| 747 | if not keylist and not args: |
|---|
| 748 | return None |
|---|
| 749 | |
|---|
| 750 | keys = frozenset(kargs.keys()) |
|---|
| 751 | cacheset = False |
|---|
| 752 | cachekey = unicode(keys) |
|---|
| 753 | cachelist = sortcache.get(cachekey) |
|---|
| 754 | if args: |
|---|
| 755 | keylist = args |
|---|
| 756 | elif cachelist: |
|---|
| 757 | keylist = cachelist |
|---|
| 758 | else: |
|---|
| 759 | cacheset = True |
|---|
| 760 | newlist = [] |
|---|
| 761 | for route in keylist: |
|---|
| 762 | if len(route.minkeys - route.dotkeys - keys) == 0: |
|---|
| 763 | newlist.append(route) |
|---|
| 764 | keylist = newlist |
|---|
| 765 | |
|---|
| 766 | def keysort(a, b): |
|---|
| 767 | """Sorts two sets of sets, to order them ideally for |
|---|
| 768 | matching.""" |
|---|
| 769 | am = a.minkeys |
|---|
| 770 | a = a.maxkeys |
|---|
| 771 | b = b.maxkeys |
|---|
| 772 | |
|---|
| 773 | lendiffa = len(keys^a) |
|---|
| 774 | lendiffb = len(keys^b) |
|---|
| 775 | # If they both match, don't switch them |
|---|
| 776 | if lendiffa == 0 and lendiffb == 0: |
|---|
| 777 | return 0 |
|---|
| 778 | |
|---|
| 779 | # First, if a matches exactly, use it |
|---|
| 780 | if lendiffa == 0: |
|---|
| 781 | return -1 |
|---|
| 782 | |
|---|
| 783 | # Or b matches exactly, use it |
|---|
| 784 | if lendiffb == 0: |
|---|
| 785 | return 1 |
|---|
| 786 | |
|---|
| 787 | # Neither matches exactly, return the one with the most in |
|---|
| 788 | # common |
|---|
| 789 | if cmp(lendiffa, lendiffb) != 0: |
|---|
| 790 | return cmp(lendiffa, lendiffb) |
|---|
| 791 | |
|---|
| 792 | # Neither matches exactly, but if they both have just as much |
|---|
| 793 | # in common |
|---|
| 794 | if len(keys&b) == len(keys&a): |
|---|
| 795 | # Then we return the shortest of the two |
|---|
| 796 | return cmp(len(a), len(b)) |
|---|
| 797 | |
|---|
| 798 | # Otherwise, we return the one that has the most in common |
|---|
| 799 | else: |
|---|
| 800 | return cmp(len(keys&b), len(keys&a)) |
|---|
| 801 | |
|---|
| 802 | keylist.sort(keysort) |
|---|
| 803 | if cacheset: |
|---|
| 804 | sortcache[cachekey] = keylist |
|---|
| 805 | |
|---|
| 806 | # Iterate through the keylist of sorted routes (or a single route if |
|---|
| 807 | # it was passed in explicitly for hardcoded named routes) |
|---|
| 808 | for route in keylist: |
|---|
| 809 | fail = False |
|---|
| 810 | for key in route.hardcoded: |
|---|
| 811 | kval = kargs.get(key) |
|---|
| 812 | if not kval: |
|---|
| 813 | continue |
|---|
| 814 | if isinstance(kval, str): |
|---|
| 815 | kval = kval.decode(self.encoding) |
|---|
| 816 | else: |
|---|
| 817 | kval = unicode(kval) |
|---|
| 818 | if kval != route.defaults[key] and not callable(route.defaults[key]): |
|---|
| 819 | fail = True |
|---|
| 820 | break |
|---|
| 821 | if fail: |
|---|
| 822 | continue |
|---|
| 823 | path = route.generate(**kargs) |
|---|
| 824 | if path: |
|---|
| 825 | if self.prefix: |
|---|
| 826 | path = self.prefix + path |
|---|
| 827 | external_static = route.static and route.external |
|---|
| 828 | if environ and environ.get('SCRIPT_NAME', '') != ''\ |
|---|
| 829 | and not route.absolute and not external_static: |
|---|
| 830 | path = environ['SCRIPT_NAME'] + path |
|---|
| 831 | key = cache_key_script_name |
|---|
| 832 | else: |
|---|
| 833 | key = cache_key |
|---|
| 834 | if self.urlcache is not None: |
|---|
| 835 | self.urlcache[key] = str(path) |
|---|
| 836 | return str(path) |
|---|
| 837 | else: |
|---|
| 838 | continue |
|---|
| 839 | return None |
|---|
| 840 | |
|---|
| 841 | def resource(self, member_name, collection_name, **kwargs): |
|---|
| 842 | """Generate routes for a controller resource |
|---|
| 843 | |
|---|
| 844 | The member_name name should be the appropriate singular version |
|---|
| 845 | of the resource given your locale and used with members of the |
|---|
| 846 | collection. The collection_name name will be used to refer to |
|---|
| 847 | the resource collection methods and should be a plural version |
|---|
| 848 | of the member_name argument. By default, the member_name name |
|---|
| 849 | will also be assumed to map to a controller you create. |
|---|
| 850 | |
|---|
| 851 | The concept of a web resource maps somewhat directly to 'CRUD' |
|---|
| 852 | operations. The overlying things to keep in mind is that |
|---|
| 853 | mapping a resource is about handling creating, viewing, and |
|---|
| 854 | editing that resource. |
|---|
| 855 | |
|---|
| 856 | All keyword arguments are optional. |
|---|
| 857 | |
|---|
| 858 | ``controller`` |
|---|
| 859 | If specified in the keyword args, the controller will be |
|---|
| 860 | the actual controller used, but the rest of the naming |
|---|
| 861 | conventions used for the route names and URL paths are |
|---|
| 862 | unchanged. |
|---|
| 863 | |
|---|
| 864 | ``collection`` |
|---|
| 865 | Additional action mappings used to manipulate/view the |
|---|
| 866 | entire set of resources provided by the controller. |
|---|
| 867 | |
|---|
| 868 | Example:: |
|---|
| 869 | |
|---|
| 870 | map.resource('message', 'messages', collection={'rss':'GET'}) |
|---|
| 871 | # GET /message/rss (maps to the rss action) |
|---|
| 872 | # also adds named route "rss_message" |
|---|
| 873 | |
|---|
| 874 | ``member`` |
|---|
| 875 | Additional action mappings used to access an individual |
|---|
| 876 | 'member' of this controllers resources. |
|---|
| 877 | |
|---|
| 878 | Example:: |
|---|
| 879 | |
|---|
| 880 | map.resource('message', 'messages', member={'mark':'POST'}) |
|---|
| 881 | # POST /message/1/mark (maps to the mark action) |
|---|
| 882 | # also adds named route "mark_message" |
|---|
| 883 | |
|---|
| 884 | ``new`` |
|---|
| 885 | Action mappings that involve dealing with a new member in |
|---|
| 886 | the controller resources. |
|---|
| 887 | |
|---|
| 888 | Example:: |
|---|
| 889 | |
|---|
| 890 | map.resource('message', 'messages', new={'preview':'POST'}) |
|---|
| 891 | # POST /message/new/preview (maps to the preview action) |
|---|
| 892 | # also adds a url named "preview_new_message" |
|---|
| 893 | |
|---|
| 894 | ``path_prefix`` |
|---|
| 895 | Prepends the URL path for the Route with the path_prefix |
|---|
| 896 | given. This is most useful for cases where you want to mix |
|---|
| 897 | resources or relations between resources. |
|---|
| 898 | |
|---|
| 899 | ``name_prefix`` |
|---|
| 900 | Perpends the route names that are generated with the |
|---|
| 901 | name_prefix given. Combined with the path_prefix option, |
|---|
| 902 | it's easy to generate route names and paths that represent |
|---|
| 903 | resources that are in relations. |
|---|
| 904 | |
|---|
| 905 | Example:: |
|---|
| 906 | |
|---|
| 907 | map.resource('message', 'messages', controller='categories', |
|---|
| 908 | path_prefix='/category/:category_id', |
|---|
| 909 | name_prefix="category_") |
|---|
| 910 | # GET /category/7/message/1 |
|---|
| 911 | # has named route "category_message" |
|---|
| 912 | |
|---|
| 913 | ``parent_resource`` |
|---|
| 914 | A ``dict`` containing information about the parent |
|---|
| 915 | resource, for creating a nested resource. It should contain |
|---|
| 916 | the ``member_name`` and ``collection_name`` of the parent |
|---|
| 917 | resource. This ``dict`` will |
|---|
| 918 | be available via the associated ``Route`` object which can |
|---|
| 919 | be accessed during a request via |
|---|
| 920 | ``request.environ['routes.route']`` |
|---|
| 921 | |
|---|
| 922 | If ``parent_resource`` is supplied and ``path_prefix`` |
|---|
| 923 | isn't, ``path_prefix`` will be generated from |
|---|
| 924 | ``parent_resource`` as |
|---|
| 925 | "<parent collection name>/:<parent member name>_id". |
|---|
| 926 | |
|---|
| 927 | If ``parent_resource`` is supplied and ``name_prefix`` |
|---|
| 928 | isn't, ``name_prefix`` will be generated from |
|---|
| 929 | ``parent_resource`` as "<parent member name>_". |
|---|
| 930 | |
|---|
| 931 | Example:: |
|---|
| 932 | |
|---|
| 933 | >>> from routes.util import url_for |
|---|
| 934 | >>> m = Mapper() |
|---|
| 935 | >>> m.resource('location', 'locations', |
|---|
| 936 | ... parent_resource=dict(member_name='region', |
|---|
| 937 | ... collection_name='regions')) |
|---|
| 938 | >>> # path_prefix is "regions/:region_id" |
|---|
| 939 | >>> # name prefix is "region_" |
|---|
| 940 | >>> url_for('region_locations', region_id=13) |
|---|
| 941 | '/regions/13/locations' |
|---|
| 942 | >>> url_for('region_new_location', region_id=13) |
|---|
| 943 | '/regions/13/locations/new' |
|---|
| 944 | >>> url_for('region_location', region_id=13, id=60) |
|---|
| 945 | '/regions/13/locations/60' |
|---|
| 946 | >>> url_for('region_edit_location', region_id=13, id=60) |
|---|
| 947 | '/regions/13/locations/60/edit' |
|---|
| 948 | |
|---|
| 949 | Overriding generated ``path_prefix``:: |
|---|
| 950 | |
|---|
| 951 | >>> m = Mapper() |
|---|
| 952 | >>> m.resource('location', 'locations', |
|---|
| 953 | ... parent_resource=dict(member_name='region', |
|---|
| 954 | ... collection_name='regions'), |
|---|
| 955 | ... path_prefix='areas/:area_id') |
|---|
| 956 | >>> # name prefix is "region_" |
|---|
| 957 | >>> url_for('region_locations', area_id=51) |
|---|
| 958 | '/areas/51/locations' |
|---|
| 959 | |
|---|
| 960 | Overriding generated ``name_prefix``:: |
|---|
| 961 | |
|---|
| 962 | >>> m = Mapper() |
|---|
| 963 | >>> m.resource('location', 'locations', |
|---|
| 964 | ... parent_resource=dict(member_name='region', |
|---|
| 965 | ... collection_name='regions'), |
|---|
| 966 | ... name_prefix='') |
|---|
| 967 | >>> # path_prefix is "regions/:region_id" |
|---|
| 968 | >>> url_for('locations', region_id=51) |
|---|
| 969 | '/regions/51/locations' |
|---|
| 970 | |
|---|
| 971 | """ |
|---|
| 972 | collection = kwargs.pop('collection', {}) |
|---|
| 973 | member = kwargs.pop('member', {}) |
|---|
| 974 | new = kwargs.pop('new', {}) |
|---|
| 975 | path_prefix = kwargs.pop('path_prefix', None) |
|---|
| 976 | name_prefix = kwargs.pop('name_prefix', None) |
|---|
| 977 | parent_resource = kwargs.pop('parent_resource', None) |
|---|
| 978 | |
|---|
| 979 | # Generate ``path_prefix`` if ``path_prefix`` wasn't specified and |
|---|
| 980 | # ``parent_resource`` was. Likewise for ``name_prefix``. Make sure |
|---|
| 981 | # that ``path_prefix`` and ``name_prefix`` *always* take precedence if |
|---|
| 982 | # they are specified--in particular, we need to be careful when they |
|---|
| 983 | # are explicitly set to "". |
|---|
| 984 | if parent_resource is not None: |
|---|
| 985 | if path_prefix is None: |
|---|
| 986 | path_prefix = '%s/:%s_id' % (parent_resource['collection_name'], |
|---|
| 987 | parent_resource['member_name']) |
|---|
| 988 | if name_prefix is None: |
|---|
| 989 | name_prefix = '%s_' % parent_resource['member_name'] |
|---|
| 990 | else: |
|---|
| 991 | if path_prefix is None: path_prefix = '' |
|---|
| 992 | if name_prefix is None: name_prefix = '' |
|---|
| 993 | |
|---|
| 994 | # Ensure the edit and new actions are in and GET |
|---|
| 995 | member['edit'] = 'GET' |
|---|
| 996 | new.update({'new': 'GET'}) |
|---|
| 997 | |
|---|
| 998 | # Make new dict's based off the old, except the old values become keys, |
|---|
| 999 | # and the old keys become items in a list as the value |
|---|
| 1000 | def swap(dct, newdct): |
|---|
| 1001 | """Swap the keys and values in the dict, and uppercase the values |
|---|
| 1002 | from the dict during the swap.""" |
|---|
| 1003 | for key, val in dct.iteritems(): |
|---|
| 1004 | newdct.setdefault(val.upper(), []).append(key) |
|---|
| 1005 | return newdct |
|---|
| 1006 | collection_methods = swap(collection, {}) |
|---|
| 1007 | member_methods = swap(member, {}) |
|---|
| 1008 | new_methods = swap(new, {}) |
|---|
| 1009 | |
|---|
| 1010 | # Insert create, update, and destroy methods |
|---|
| 1011 | collection_methods.setdefault('POST', []).insert(0, 'create') |
|---|
| 1012 | member_methods.setdefault('PUT', []).insert(0, 'update') |
|---|
| 1013 | member_methods.setdefault('DELETE', []).insert(0, 'delete') |
|---|
| 1014 | |
|---|
| 1015 | # If there's a path prefix option, use it with the controller |
|---|
| 1016 | controller = strip_slashes(collection_name) |
|---|
| 1017 | path_prefix = strip_slashes(path_prefix) |
|---|
| 1018 | path_prefix = '/' + path_prefix |
|---|
| 1019 | if path_prefix and path_prefix != '/': |
|---|
| 1020 | path = path_prefix + '/' + controller |
|---|
| 1021 | else: |
|---|
| 1022 | path = '/' + controller |
|---|
| 1023 | collection_path = path |
|---|
| 1024 | new_path = path + "/new" |
|---|
| 1025 | member_path = path + "/:(id)" |
|---|
| 1026 | |
|---|
| 1027 | options = { |
|---|
| 1028 | 'controller': kwargs.get('controller', controller), |
|---|
| 1029 | '_member_name': member_name, |
|---|
| 1030 | '_collection_name': collection_name, |
|---|
| 1031 | '_parent_resource': parent_resource, |
|---|
| 1032 | '_filter': kwargs.get('_filter') |
|---|
| 1033 | } |
|---|
| 1034 | |
|---|
| 1035 | def requirements_for(meth): |
|---|
| 1036 | """Returns a new dict to be used for all route creation as the |
|---|
| 1037 | route options""" |
|---|
| 1038 | opts = options.copy() |
|---|
| 1039 | if method != 'any': |
|---|
| 1040 | opts['conditions'] = {'method':[meth.upper()]} |
|---|
| 1041 | return opts |
|---|
| 1042 | |
|---|
| 1043 | # Add the routes for handling collection methods |
|---|
| 1044 | for method, lst in collection_methods.iteritems(): |
|---|
| 1045 | primary = (method != 'GET' and lst.pop(0)) or None |
|---|
| 1046 | route_options = requirements_for(method) |
|---|
| 1047 | for action in lst: |
|---|
| 1048 | route_options['action'] = action |
|---|
| 1049 | route_name = "%s%s_%s" % (name_prefix, action, collection_name) |
|---|
| 1050 | self.connect("formatted_" + route_name, "%s/%s.:(format)" % \ |
|---|
| 1051 | (collection_path, action), **route_options) |
|---|
| 1052 | self.connect(route_name, "%s/%s" % (collection_path, action), |
|---|
| 1053 | **route_options) |
|---|
| 1054 | if primary: |
|---|
| 1055 | route_options['action'] = primary |
|---|
| 1056 | self.connect("%s.:(format)" % collection_path, **route_options) |
|---|
| 1057 | self.connect(collection_path, **route_options) |
|---|
| 1058 | |
|---|
| 1059 | # Specifically add in the built-in 'index' collection method and its |
|---|
| 1060 | # formatted version |
|---|
| 1061 | self.connect("formatted_" + name_prefix + collection_name, |
|---|
| 1062 | collection_path + ".:(format)", action='index', |
|---|
| 1063 | conditions={'method':['GET']}, **options) |
|---|
| 1064 | self.connect(name_prefix + collection_name, collection_path, |
|---|
| 1065 | action='index', conditions={'method':['GET']}, **options) |
|---|
| 1066 | |
|---|
| 1067 | # Add the routes that deal with new resource methods |
|---|
| 1068 | for method, lst in new_methods.iteritems(): |
|---|
| 1069 | route_options = requirements_for(method) |
|---|
| 1070 | for action in lst: |
|---|
| 1071 | path = (action == 'new' and new_path) or "%s/%s" % (new_path, |
|---|
| 1072 | action) |
|---|
| 1073 | name = "new_" + member_name |
|---|
| 1074 | if action != 'new': |
|---|
| 1075 | name = action + "_" + name |
|---|
| 1076 | route_options['action'] = action |
|---|
| 1077 | formatted_path = (action == 'new' and new_path + '.:(format)') or \ |
|---|
| 1078 | "%s/%s.:(format)" % (new_path, action) |
|---|
| 1079 | self.connect("formatted_" + name_prefix + name, formatted_path, |
|---|
| 1080 | **route_options) |
|---|
| 1081 | self.connect(name_prefix + name, path, **route_options) |
|---|
| 1082 | |
|---|
| 1083 | requirements_regexp = '[^\/]+' |
|---|
| 1084 | |
|---|
| 1085 | # Add the routes that deal with member methods of a resource |
|---|
| 1086 | for method, lst in member_methods.iteritems(): |
|---|
| 1087 | route_options = requirements_for(method) |
|---|
| 1088 | route_options['requirements'] = {'id':requirements_regexp} |
|---|
| 1089 | if method not in ['POST', 'GET', 'any']: |
|---|
| 1090 | primary = lst.pop(0) |
|---|
| 1091 | else: |
|---|
| 1092 | primary = None |
|---|
| 1093 | for action in lst: |
|---|
| 1094 | route_options['action'] = action |
|---|
| 1095 | self.connect("formatted_%s%s_%s" % (name_prefix, action, |
|---|
| 1096 | member_name), |
|---|
| 1097 | "%s/%s.:(format)" % (member_path, action), **route_options) |
|---|
| 1098 | self.connect("%s%s_%s" % (name_prefix, action, member_name), |
|---|
| 1099 | "%s/%s" % (member_path, action), **route_options) |
|---|
| 1100 | if primary: |
|---|
| 1101 | route_options['action'] = primary |
|---|
| 1102 | self.connect("%s.:(format)" % member_path, **route_options) |
|---|
| 1103 | self.connect(member_path, **route_options) |
|---|
| 1104 | |
|---|
| 1105 | # Specifically add the member 'show' method |
|---|
| 1106 | route_options = requirements_for('GET') |
|---|
| 1107 | route_options['action'] = 'show' |
|---|
| 1108 | route_options['requirements'] = {'id':requirements_regexp} |
|---|
| 1109 | self.connect("formatted_" + name_prefix + member_name, |
|---|
| 1110 | member_path + ".:(format)", **route_options) |
|---|
| 1111 | self.connect(name_prefix + member_name, member_path, **route_options) |
|---|
| 1112 | |
|---|
| 1113 | def redirect(self, match_path, destination_path, *args, **kwargs): |
|---|
| 1114 | """Add a redirect route to the mapper |
|---|
| 1115 | |
|---|
| 1116 | Redirect routes bypass the wrapped WSGI application and instead |
|---|
| 1117 | result in a redirect being issued by the RoutesMiddleware. As |
|---|
| 1118 | such, this method is only meaningful when using |
|---|
| 1119 | RoutesMiddleware. |
|---|
| 1120 | |
|---|
| 1121 | By default, a 302 Found status code is used, this can be |
|---|
| 1122 | changed by providing a ``_redirect_code`` keyword argument |
|---|
| 1123 | which will then be used instead. Note that the entire status |
|---|
| 1124 | code string needs to be present. |
|---|
| 1125 | |
|---|
| 1126 | When using keyword arguments, all arguments that apply to |
|---|
| 1127 | matching will be used for the match, while generation specific |
|---|
| 1128 | options will be used during generation. Thus all options |
|---|
| 1129 | normally available to connected Routes may be used with |
|---|
| 1130 | redirect routes as well. |
|---|
| 1131 | |
|---|
| 1132 | Example:: |
|---|
| 1133 | |
|---|
| 1134 | map = Mapper() |
|---|
| 1135 | map.redirect('/legacyapp/archives/{url:.*}, '/archives/{url}) |
|---|
| 1136 | map.redirect('/home/index', '/', _redirect_code='301 Moved Permanently') |
|---|
| 1137 | |
|---|
| 1138 | """ |
|---|
| 1139 | both_args = ['_encoding', '_explicit', '_minimize'] |
|---|
| 1140 | gen_args = ['_filter'] |
|---|
| 1141 | |
|---|
| 1142 | status_code = kwargs.pop('_redirect_code', '302 Found') |
|---|
| 1143 | gen_dict, match_dict = {}, {} |
|---|
| 1144 | |
|---|
| 1145 | # Create the dict of args for the generation route |
|---|
| 1146 | for key in both_args + gen_args: |
|---|
| 1147 | if key in kwargs: |
|---|
| 1148 | gen_dict[key] = kwargs[key] |
|---|
| 1149 | gen_dict['_static'] = True |
|---|
| 1150 | |
|---|
| 1151 | # Create the dict of args for the matching route |
|---|
| 1152 | for key in kwargs: |
|---|
| 1153 | if key not in gen_args: |
|---|
| 1154 | match_dict[key] = kwargs[key] |
|---|
| 1155 | |
|---|
| 1156 | self.connect(match_path, **match_dict) |
|---|
| 1157 | match_route = self.matchlist[-1] |
|---|
| 1158 | |
|---|
| 1159 | self.connect('_redirect_%s' % id(match_route), destination_path, |
|---|
| 1160 | **gen_dict) |
|---|
| 1161 | match_route.redirect = True |
|---|
| 1162 | match_route.redirect_status = status_code |
|---|