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