1 | """Utility functions for use in templates / controllers |
---|
2 | |
---|
3 | *PLEASE NOTE*: Many of these functions expect an initialized RequestConfig |
---|
4 | object. This is expected to have been initialized for EACH REQUEST by the web |
---|
5 | framework. |
---|
6 | |
---|
7 | """ |
---|
8 | import os |
---|
9 | import re |
---|
10 | import urllib |
---|
11 | from routes import request_config |
---|
12 | |
---|
13 | |
---|
14 | class RoutesException(Exception): |
---|
15 | """Tossed during Route exceptions""" |
---|
16 | |
---|
17 | |
---|
18 | class MatchException(RoutesException): |
---|
19 | """Tossed during URL matching exceptions""" |
---|
20 | |
---|
21 | |
---|
22 | class GenerationException(RoutesException): |
---|
23 | """Tossed during URL generation exceptions""" |
---|
24 | |
---|
25 | |
---|
26 | def _screenargs(kargs, mapper, environ, force_explicit=False): |
---|
27 | """ |
---|
28 | Private function that takes a dict, and screens it against the current |
---|
29 | request dict to determine what the dict should look like that is used. |
---|
30 | This is responsible for the requests "memory" of the current. |
---|
31 | """ |
---|
32 | # Coerce any unicode args with the encoding |
---|
33 | encoding = mapper.encoding |
---|
34 | for key, val in kargs.iteritems(): |
---|
35 | if isinstance(val, unicode): |
---|
36 | kargs[key] = val.encode(encoding) |
---|
37 | |
---|
38 | if mapper.explicit and mapper.sub_domains and not force_explicit: |
---|
39 | return _subdomain_check(kargs, mapper, environ) |
---|
40 | elif mapper.explicit and not force_explicit: |
---|
41 | return kargs |
---|
42 | |
---|
43 | controller_name = kargs.get('controller') |
---|
44 | |
---|
45 | if controller_name and controller_name.startswith('/'): |
---|
46 | # If the controller name starts with '/', ignore route memory |
---|
47 | kargs['controller'] = kargs['controller'][1:] |
---|
48 | return kargs |
---|
49 | elif controller_name and not kargs.has_key('action'): |
---|
50 | # Fill in an action if we don't have one, but have a controller |
---|
51 | kargs['action'] = 'index' |
---|
52 | |
---|
53 | route_args = environ.get('wsgiorg.routing_args') |
---|
54 | if route_args: |
---|
55 | memory_kargs = route_args[1].copy() |
---|
56 | else: |
---|
57 | memory_kargs = {} |
---|
58 | |
---|
59 | # Remove keys from memory and kargs if kargs has them as None |
---|
60 | for key in [key for key in kargs.keys() if kargs[key] is None]: |
---|
61 | del kargs[key] |
---|
62 | if memory_kargs.has_key(key): |
---|
63 | del memory_kargs[key] |
---|
64 | |
---|
65 | # Merge the new args on top of the memory args |
---|
66 | memory_kargs.update(kargs) |
---|
67 | |
---|
68 | # Setup a sub-domain if applicable |
---|
69 | if mapper.sub_domains: |
---|
70 | memory_kargs = _subdomain_check(memory_kargs, mapper, environ) |
---|
71 | return memory_kargs |
---|
72 | |
---|
73 | |
---|
74 | def _subdomain_check(kargs, mapper, environ): |
---|
75 | """Screen the kargs for a subdomain and alter it appropriately depending |
---|
76 | on the current subdomain or lack therof.""" |
---|
77 | if mapper.sub_domains: |
---|
78 | subdomain = kargs.pop('sub_domain', None) |
---|
79 | if isinstance(subdomain, unicode): |
---|
80 | subdomain = str(subdomain) |
---|
81 | |
---|
82 | fullhost = environ.get('HTTP_HOST') or environ.get('SERVER_NAME') |
---|
83 | |
---|
84 | # In case environ defaulted to {} |
---|
85 | if not fullhost: |
---|
86 | return kargs |
---|
87 | |
---|
88 | hostmatch = fullhost.split(':') |
---|
89 | host = hostmatch[0] |
---|
90 | port = '' |
---|
91 | if len(hostmatch) > 1: |
---|
92 | port += ':' + hostmatch[1] |
---|
93 | sub_match = re.compile('^.+?\.(%s)$' % mapper.domain_match) |
---|
94 | domain = re.sub(sub_match, r'\1', host) |
---|
95 | if subdomain and not host.startswith(subdomain) and \ |
---|
96 | subdomain not in mapper.sub_domains_ignore: |
---|
97 | kargs['_host'] = subdomain + '.' + domain + port |
---|
98 | elif (subdomain in mapper.sub_domains_ignore or \ |
---|
99 | subdomain is None) and domain != host: |
---|
100 | kargs['_host'] = domain + port |
---|
101 | return kargs |
---|
102 | else: |
---|
103 | return kargs |
---|
104 | |
---|
105 | |
---|
106 | def _url_quote(string, encoding): |
---|
107 | """A Unicode handling version of urllib.quote.""" |
---|
108 | if encoding: |
---|
109 | if isinstance(string, unicode): |
---|
110 | s = string.encode(encoding) |
---|
111 | elif isinstance(string, str): |
---|
112 | # assume the encoding is already correct |
---|
113 | s = string |
---|
114 | else: |
---|
115 | s = unicode(string).encode(encoding) |
---|
116 | else: |
---|
117 | s = str(string) |
---|
118 | return urllib.quote(s, '/') |
---|
119 | |
---|
120 | |
---|
121 | def _str_encode(string, encoding): |
---|
122 | if encoding: |
---|
123 | if isinstance(string, unicode): |
---|
124 | s = string.encode(encoding) |
---|
125 | elif isinstance(string, str): |
---|
126 | # assume the encoding is already correct |
---|
127 | s = string |
---|
128 | else: |
---|
129 | s = unicode(string).encode(encoding) |
---|
130 | return s |
---|
131 | |
---|
132 | |
---|
133 | def url_for(*args, **kargs): |
---|
134 | """Generates a URL |
---|
135 | |
---|
136 | All keys given to url_for are sent to the Routes Mapper instance for |
---|
137 | generation except for:: |
---|
138 | |
---|
139 | anchor specified the anchor name to be appened to the path |
---|
140 | host overrides the default (current) host if provided |
---|
141 | protocol overrides the default (current) protocol if provided |
---|
142 | qualified creates the URL with the host/port information as |
---|
143 | needed |
---|
144 | |
---|
145 | The URL is generated based on the rest of the keys. When generating a new |
---|
146 | URL, values will be used from the current request's parameters (if |
---|
147 | present). The following rules are used to determine when and how to keep |
---|
148 | the current requests parameters: |
---|
149 | |
---|
150 | * If the controller is present and begins with '/', no defaults are used |
---|
151 | * If the controller is changed, action is set to 'index' unless otherwise |
---|
152 | specified |
---|
153 | |
---|
154 | For example, if the current request yielded a dict of |
---|
155 | {'controller': 'blog', 'action': 'view', 'id': 2}, with the standard |
---|
156 | ':controller/:action/:id' route, you'd get the following results:: |
---|
157 | |
---|
158 | url_for(id=4) => '/blog/view/4', |
---|
159 | url_for(controller='/admin') => '/admin', |
---|
160 | url_for(controller='admin') => '/admin/view/2' |
---|
161 | url_for(action='edit') => '/blog/edit/2', |
---|
162 | url_for(action='list', id=None) => '/blog/list' |
---|
163 | |
---|
164 | **Static and Named Routes** |
---|
165 | |
---|
166 | If there is a string present as the first argument, a lookup is done |
---|
167 | against the named routes table to see if there's any matching routes. The |
---|
168 | keyword defaults used with static routes will be sent in as GET query |
---|
169 | arg's if a route matches. |
---|
170 | |
---|
171 | If no route by that name is found, the string is assumed to be a raw URL. |
---|
172 | Should the raw URL begin with ``/`` then appropriate SCRIPT_NAME data will |
---|
173 | be added if present, otherwise the string will be used as the url with |
---|
174 | keyword args becoming GET query args. |
---|
175 | |
---|
176 | """ |
---|
177 | anchor = kargs.get('anchor') |
---|
178 | host = kargs.get('host') |
---|
179 | protocol = kargs.get('protocol') |
---|
180 | qualified = kargs.pop('qualified', None) |
---|
181 | |
---|
182 | # Remove special words from kargs, convert placeholders |
---|
183 | for key in ['anchor', 'host', 'protocol']: |
---|
184 | if kargs.get(key): |
---|
185 | del kargs[key] |
---|
186 | config = request_config() |
---|
187 | route = None |
---|
188 | static = False |
---|
189 | encoding = config.mapper.encoding |
---|
190 | url = '' |
---|
191 | if len(args) > 0: |
---|
192 | route = config.mapper._routenames.get(args[0]) |
---|
193 | |
---|
194 | # No named route found, assume the argument is a relative path |
---|
195 | if not route: |
---|
196 | static = True |
---|
197 | url = args[0] |
---|
198 | |
---|
199 | if url.startswith('/') and hasattr(config, 'environ') \ |
---|
200 | and config.environ.get('SCRIPT_NAME'): |
---|
201 | url = config.environ.get('SCRIPT_NAME') + url |
---|
202 | |
---|
203 | if static: |
---|
204 | if kargs: |
---|
205 | url += '?' |
---|
206 | query_args = [] |
---|
207 | for key, val in kargs.iteritems(): |
---|
208 | if isinstance(val, (list, tuple)): |
---|
209 | for value in val: |
---|
210 | query_args.append("%s=%s" % ( |
---|
211 | urllib.quote(unicode(key).encode(encoding)), |
---|
212 | urllib.quote(unicode(value).encode(encoding)))) |
---|
213 | else: |
---|
214 | query_args.append("%s=%s" % ( |
---|
215 | urllib.quote(unicode(key).encode(encoding)), |
---|
216 | urllib.quote(unicode(val).encode(encoding)))) |
---|
217 | url += '&'.join(query_args) |
---|
218 | environ = getattr(config, 'environ', {}) |
---|
219 | if 'wsgiorg.routing_args' not in environ: |
---|
220 | environ = environ.copy() |
---|
221 | mapper_dict = getattr(config, 'mapper_dict', None) |
---|
222 | if mapper_dict is not None: |
---|
223 | match_dict = mapper_dict.copy() |
---|
224 | else: |
---|
225 | match_dict = {} |
---|
226 | environ['wsgiorg.routing_args'] = ((), match_dict) |
---|
227 | |
---|
228 | if not static: |
---|
229 | route_args = [] |
---|
230 | if route: |
---|
231 | if config.mapper.hardcode_names: |
---|
232 | route_args.append(route) |
---|
233 | newargs = route.defaults.copy() |
---|
234 | newargs.update(kargs) |
---|
235 | |
---|
236 | # If this route has a filter, apply it |
---|
237 | if route.filter: |
---|
238 | newargs = route.filter(newargs) |
---|
239 | |
---|
240 | if not route.static: |
---|
241 | # Handle sub-domains |
---|
242 | newargs = _subdomain_check(newargs, config.mapper, environ) |
---|
243 | else: |
---|
244 | newargs = _screenargs(kargs, config.mapper, environ) |
---|
245 | anchor = newargs.pop('_anchor', None) or anchor |
---|
246 | host = newargs.pop('_host', None) or host |
---|
247 | protocol = newargs.pop('_protocol', None) or protocol |
---|
248 | url = config.mapper.generate(*route_args, **newargs) |
---|
249 | if anchor is not None: |
---|
250 | url += '#' + _url_quote(anchor, encoding) |
---|
251 | if host or protocol or qualified: |
---|
252 | if not host and not qualified: |
---|
253 | # Ensure we don't use a specific port, as changing the protocol |
---|
254 | # means that we most likely need a new port |
---|
255 | host = config.host.split(':')[0] |
---|
256 | elif not host: |
---|
257 | host = config.host |
---|
258 | if not protocol: |
---|
259 | protocol = config.protocol |
---|
260 | if url is not None: |
---|
261 | url = protocol + '://' + host + url |
---|
262 | |
---|
263 | if not isinstance(url, str) and url is not None: |
---|
264 | raise GenerationException("url_for can only return a string, got " |
---|
265 | "unicode instead: %s" % url) |
---|
266 | if url is None: |
---|
267 | raise GenerationException( |
---|
268 | "url_for could not generate URL. Called with args: %s %s" % \ |
---|
269 | (args, kargs)) |
---|
270 | return url |
---|
271 | |
---|
272 | |
---|
273 | class URLGenerator(object): |
---|
274 | """The URL Generator generates URL's |
---|
275 | |
---|
276 | It is automatically instantiated by the RoutesMiddleware and put |
---|
277 | into the ``wsgiorg.routing_args`` tuple accessible as:: |
---|
278 | |
---|
279 | url = environ['wsgiorg.routing_args'][0][0] |
---|
280 | |
---|
281 | Or via the ``routes.url`` key:: |
---|
282 | |
---|
283 | url = environ['routes.url'] |
---|
284 | |
---|
285 | The url object may be instantiated outside of a web context for use |
---|
286 | in testing, however sub_domain support and fully qualified URL's |
---|
287 | cannot be generated without supplying a dict that must contain the |
---|
288 | key ``HTTP_HOST``. |
---|
289 | |
---|
290 | """ |
---|
291 | def __init__(self, mapper, environ): |
---|
292 | """Instantiate the URLGenerator |
---|
293 | |
---|
294 | ``mapper`` |
---|
295 | The mapper object to use when generating routes. |
---|
296 | ``environ`` |
---|
297 | The environment dict used in WSGI, alternately, any dict |
---|
298 | that contains at least an ``HTTP_HOST`` value. |
---|
299 | |
---|
300 | """ |
---|
301 | self.mapper = mapper |
---|
302 | if 'SCRIPT_NAME' not in environ: |
---|
303 | environ['SCRIPT_NAME'] = '' |
---|
304 | self.environ = environ |
---|
305 | |
---|
306 | def __call__(self, *args, **kargs): |
---|
307 | """Generates a URL |
---|
308 | |
---|
309 | All keys given to url_for are sent to the Routes Mapper instance for |
---|
310 | generation except for:: |
---|
311 | |
---|
312 | anchor specified the anchor name to be appened to the path |
---|
313 | host overrides the default (current) host if provided |
---|
314 | protocol overrides the default (current) protocol if provided |
---|
315 | qualified creates the URL with the host/port information as |
---|
316 | needed |
---|
317 | |
---|
318 | """ |
---|
319 | anchor = kargs.get('anchor') |
---|
320 | host = kargs.get('host') |
---|
321 | protocol = kargs.get('protocol') |
---|
322 | qualified = kargs.pop('qualified', None) |
---|
323 | |
---|
324 | # Remove special words from kargs, convert placeholders |
---|
325 | for key in ['anchor', 'host', 'protocol']: |
---|
326 | if kargs.get(key): |
---|
327 | del kargs[key] |
---|
328 | |
---|
329 | route = None |
---|
330 | use_current = '_use_current' in kargs and kargs.pop('_use_current') |
---|
331 | |
---|
332 | static = False |
---|
333 | encoding = self.mapper.encoding |
---|
334 | url = '' |
---|
335 | |
---|
336 | more_args = len(args) > 0 |
---|
337 | if more_args: |
---|
338 | route = self.mapper._routenames.get(args[0]) |
---|
339 | |
---|
340 | if not route and more_args: |
---|
341 | static = True |
---|
342 | url = args[0] |
---|
343 | if url.startswith('/') and self.environ.get('SCRIPT_NAME'): |
---|
344 | url = self.environ.get('SCRIPT_NAME') + url |
---|
345 | |
---|
346 | if static: |
---|
347 | if kargs: |
---|
348 | url += '?' |
---|
349 | query_args = [] |
---|
350 | for key, val in kargs.iteritems(): |
---|
351 | if isinstance(val, (list, tuple)): |
---|
352 | for value in val: |
---|
353 | query_args.append("%s=%s" % ( |
---|
354 | urllib.quote(unicode(key).encode(encoding)), |
---|
355 | urllib.quote(unicode(value).encode(encoding)))) |
---|
356 | else: |
---|
357 | query_args.append("%s=%s" % ( |
---|
358 | urllib.quote(unicode(key).encode(encoding)), |
---|
359 | urllib.quote(unicode(val).encode(encoding)))) |
---|
360 | url += '&'.join(query_args) |
---|
361 | if not static: |
---|
362 | route_args = [] |
---|
363 | if route: |
---|
364 | if self.mapper.hardcode_names: |
---|
365 | route_args.append(route) |
---|
366 | newargs = route.defaults.copy() |
---|
367 | newargs.update(kargs) |
---|
368 | |
---|
369 | # If this route has a filter, apply it |
---|
370 | if route.filter: |
---|
371 | newargs = route.filter(newargs) |
---|
372 | if not route.static or (route.static and not route.external): |
---|
373 | # Handle sub-domains, retain sub_domain if there is one |
---|
374 | sub = newargs.get('sub_domain', None) |
---|
375 | newargs = _subdomain_check(newargs, self.mapper, |
---|
376 | self.environ) |
---|
377 | # If the route requires a sub-domain, and we have it, restore |
---|
378 | # it |
---|
379 | if 'sub_domain' in route.defaults: |
---|
380 | newargs['sub_domain'] = sub |
---|
381 | |
---|
382 | elif use_current: |
---|
383 | newargs = _screenargs(kargs, self.mapper, self.environ, force_explicit=True) |
---|
384 | elif 'sub_domain' in kargs: |
---|
385 | newargs = _subdomain_check(kargs, self.mapper, self.environ) |
---|
386 | else: |
---|
387 | newargs = kargs |
---|
388 | |
---|
389 | anchor = anchor or newargs.pop('_anchor', None) |
---|
390 | host = host or newargs.pop('_host', None) |
---|
391 | protocol = protocol or newargs.pop('_protocol', None) |
---|
392 | newargs['_environ'] = self.environ |
---|
393 | url = self.mapper.generate(*route_args, **newargs) |
---|
394 | if anchor is not None: |
---|
395 | url += '#' + _url_quote(anchor, encoding) |
---|
396 | if host or protocol or qualified: |
---|
397 | if 'routes.cached_hostinfo' not in self.environ: |
---|
398 | cache_hostinfo(self.environ) |
---|
399 | hostinfo = self.environ['routes.cached_hostinfo'] |
---|
400 | |
---|
401 | if not host and not qualified: |
---|
402 | # Ensure we don't use a specific port, as changing the protocol |
---|
403 | # means that we most likely need a new port |
---|
404 | host = hostinfo['host'].split(':')[0] |
---|
405 | elif not host: |
---|
406 | host = hostinfo['host'] |
---|
407 | if not protocol: |
---|
408 | protocol = hostinfo['protocol'] |
---|
409 | if url is not None: |
---|
410 | if host[-1] != '/': |
---|
411 | host += '/' |
---|
412 | url = protocol + '://' + host + url.lstrip('/') |
---|
413 | |
---|
414 | if not isinstance(url, str) and url is not None: |
---|
415 | raise GenerationException("Can only return a string, got " |
---|
416 | "unicode instead: %s" % url) |
---|
417 | if url is None: |
---|
418 | raise GenerationException( |
---|
419 | "Could not generate URL. Called with args: %s %s" % \ |
---|
420 | (args, kargs)) |
---|
421 | return url |
---|
422 | |
---|
423 | def current(self, *args, **kwargs): |
---|
424 | """Generate a route that includes params used on the current |
---|
425 | request |
---|
426 | |
---|
427 | The arguments for this method are identical to ``__call__`` |
---|
428 | except that arguments set to None will remove existing route |
---|
429 | matches of the same name from the set of arguments used to |
---|
430 | construct a URL. |
---|
431 | """ |
---|
432 | return self(_use_current=True, *args, **kwargs) |
---|
433 | |
---|
434 | |
---|
435 | def redirect_to(*args, **kargs): |
---|
436 | """Issues a redirect based on the arguments. |
---|
437 | |
---|
438 | Redirect's *should* occur as a "302 Moved" header, however the web |
---|
439 | framework may utilize a different method. |
---|
440 | |
---|
441 | All arguments are passed to url_for to retrieve the appropriate URL, then |
---|
442 | the resulting URL it sent to the redirect function as the URL. |
---|
443 | """ |
---|
444 | target = url_for(*args, **kargs) |
---|
445 | config = request_config() |
---|
446 | return config.redirect(target) |
---|
447 | |
---|
448 | |
---|
449 | def cache_hostinfo(environ): |
---|
450 | """Processes the host information and stores a copy |
---|
451 | |
---|
452 | This work was previously done but wasn't stored in environ, nor is |
---|
453 | it guaranteed to be setup in the future (Routes 2 and beyond). |
---|
454 | |
---|
455 | cache_hostinfo processes environ keys that may be present to |
---|
456 | determine the proper host, protocol, and port information to use |
---|
457 | when generating routes. |
---|
458 | |
---|
459 | """ |
---|
460 | hostinfo = {} |
---|
461 | if environ.get('HTTPS') or environ.get('wsgi.url_scheme') == 'https' \ |
---|
462 | or environ.get('HTTP_X_FORWARDED_PROTO') == 'https': |
---|
463 | hostinfo['protocol'] = 'https' |
---|
464 | else: |
---|
465 | hostinfo['protocol'] = 'http' |
---|
466 | if environ.get('HTTP_X_FORWARDED_HOST'): |
---|
467 | hostinfo['host'] = environ['HTTP_X_FORWARDED_HOST'] |
---|
468 | elif environ.get('HTTP_HOST'): |
---|
469 | hostinfo['host'] = environ['HTTP_HOST'] |
---|
470 | else: |
---|
471 | hostinfo['host'] = environ['SERVER_NAME'] |
---|
472 | if environ.get('wsgi.url_scheme') == 'https': |
---|
473 | if environ['SERVER_PORT'] != '443': |
---|
474 | hostinfo['host'] += ':' + environ['SERVER_PORT'] |
---|
475 | else: |
---|
476 | if environ['SERVER_PORT'] != '80': |
---|
477 | hostinfo['host'] += ':' + environ['SERVER_PORT'] |
---|
478 | environ['routes.cached_hostinfo'] = hostinfo |
---|
479 | return hostinfo |
---|
480 | |
---|
481 | |
---|
482 | def controller_scan(directory=None): |
---|
483 | """Scan a directory for python files and use them as controllers""" |
---|
484 | if directory is None: |
---|
485 | return [] |
---|
486 | |
---|
487 | def find_controllers(dirname, prefix=''): |
---|
488 | """Locate controllers in a directory""" |
---|
489 | controllers = [] |
---|
490 | for fname in os.listdir(dirname): |
---|
491 | filename = os.path.join(dirname, fname) |
---|
492 | if os.path.isfile(filename) and \ |
---|
493 | re.match('^[^_]{1,1}.*\.py$', fname): |
---|
494 | controllers.append(prefix + fname[:-3]) |
---|
495 | elif os.path.isdir(filename): |
---|
496 | controllers.extend(find_controllers(filename, |
---|
497 | prefix=prefix+fname+'/')) |
---|
498 | return controllers |
---|
499 | def longest_first(fst, lst): |
---|
500 | """Compare the length of one string to another, shortest goes first""" |
---|
501 | return cmp(len(lst), len(fst)) |
---|
502 | controllers = find_controllers(directory) |
---|
503 | controllers.sort(longest_first) |
---|
504 | return controllers |
---|