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 |
---|