|
1 # |
|
2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net> |
|
3 # All Rights Reserved. |
|
4 # |
|
5 # This software is subject to the provisions of the Zope Public License, |
|
6 # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. |
|
7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED |
|
8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
|
9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS |
|
10 # FOR A PARTICULAR PURPOSE. |
|
11 # |
|
12 |
|
13 __docformat__ = 'restructuredtext' |
|
14 |
|
15 |
|
16 # import standard library |
|
17 |
|
18 # import interfaces |
|
19 from pyams_utils.interfaces.traversing import IPathElements |
|
20 from pyramid.interfaces import VH_ROOT_KEY |
|
21 from zope.intid.interfaces import IIntIds |
|
22 from zope.location.interfaces import IContained |
|
23 from zope.traversing.interfaces import ITraversable, BeforeTraverseEvent |
|
24 |
|
25 # import packages |
|
26 from pyams_utils.adapter import adapter_config, ContextAdapter |
|
27 from pyams_utils.registry import query_utility |
|
28 from pyramid.compat import decode_path_info, is_nonstr_iter |
|
29 from pyramid.exceptions import URLDecodeError, NotFound |
|
30 from pyramid.location import lineage |
|
31 from pyramid.threadlocal import get_current_registry |
|
32 from pyramid.traversal import ResourceTreeTraverser, slash, split_path_info, empty |
|
33 from zope.interface import Interface |
|
34 |
|
35 |
|
36 class NamespaceTraverser(ResourceTreeTraverser): |
|
37 """Custom traverser handling views and namespaces |
|
38 |
|
39 This is an upgraded version of native Pyramid traverser. |
|
40 It adds: |
|
41 - a new BeforeTraverseEvent before traversing each object in the path |
|
42 - support for namespaces with "++" notation |
|
43 """ |
|
44 |
|
45 NAMESPACE_SELECTOR = '++' |
|
46 |
|
47 def __call__(self, request): |
|
48 |
|
49 environ = request.environ |
|
50 matchdict = request.matchdict |
|
51 |
|
52 if matchdict is not None: |
|
53 path = matchdict.get('traverse', slash) or slash |
|
54 if is_nonstr_iter(path): |
|
55 # this is a *traverse stararg (not a {traverse}) |
|
56 # routing has already decoded these elements, so we just |
|
57 # need to join them |
|
58 path = '/' + slash.join(path) or slash |
|
59 |
|
60 subpath = matchdict.get('subpath', ()) |
|
61 if not is_nonstr_iter(subpath): |
|
62 # this is not a *subpath stararg (just a {subpath}) |
|
63 # routing has already decoded this string, so we just need |
|
64 # to split it |
|
65 subpath = split_path_info(subpath) |
|
66 |
|
67 else: |
|
68 subpath = () |
|
69 try: |
|
70 # empty if mounted under a path in mod_wsgi, for example |
|
71 path = request.path_info or slash |
|
72 except KeyError: |
|
73 # if environ['PATH_INFO'] is just not there |
|
74 path = slash |
|
75 except UnicodeDecodeError as e: |
|
76 raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason) |
|
77 |
|
78 if VH_ROOT_KEY in environ: |
|
79 # HTTP_X_VHM_ROOT |
|
80 vroot_path = decode_path_info(environ[VH_ROOT_KEY]) |
|
81 vroot_tuple = split_path_info(vroot_path) |
|
82 vpath = vroot_path + path |
|
83 vroot_idx = len(vroot_tuple) - 1 |
|
84 else: |
|
85 vroot_tuple = () |
|
86 vpath = path |
|
87 vroot_idx = -1 |
|
88 |
|
89 root = self.root |
|
90 ob = vroot = root |
|
91 |
|
92 request.registry.notify(BeforeTraverseEvent(root, request)) |
|
93 |
|
94 if vpath == slash: |
|
95 # invariant: vpath must not be empty |
|
96 # prevent a call to traversal_path if we know it's going |
|
97 # to return the empty tuple |
|
98 vpath_tuple = () |
|
99 |
|
100 else: |
|
101 # we do dead reckoning here via tuple slicing instead of |
|
102 # pushing and popping temporary lists for speed purposes |
|
103 # and this hurts readability; apologies |
|
104 i = 0 |
|
105 view_selector = self.VIEW_SELECTOR |
|
106 ns_selector = self.NAMESPACE_SELECTOR |
|
107 vpath_tuple = split_path_info(vpath) |
|
108 |
|
109 for segment in vpath_tuple: |
|
110 if ob is not root: |
|
111 request.registry.notify(BeforeTraverseEvent(ob, request)) |
|
112 |
|
113 if segment[:2] == view_selector: |
|
114 # check for view name prefixed by '@@' |
|
115 return {'context': ob, |
|
116 'view_name': segment[2:], |
|
117 'subpath': vpath_tuple[i + 1:], |
|
118 'traversed': vpath_tuple[:vroot_idx + i + 1], |
|
119 'virtual_root': vroot, |
|
120 'virtual_root_path': vroot_tuple, |
|
121 'root': root} |
|
122 |
|
123 elif segment[:2] == ns_selector: |
|
124 # check for namespace prefixed by '++' |
|
125 # when a namespace is detected, named "ITraversable" multi-adapters are searched for |
|
126 # context and request, for context and for request, sequentially; a NotFound exception is |
|
127 # raised if traverser can't be found, otherwise it's "traverse" method is called to get new |
|
128 # context |
|
129 ns, name = segment[2:].split(ns_selector, 1) |
|
130 registry = get_current_registry() |
|
131 traverser = registry.queryMultiAdapter((ob, request), ITraversable, ns) |
|
132 if traverser is None: |
|
133 traverser = registry.queryAdapter(ob, ITraversable, ns) |
|
134 if traverser is None: |
|
135 traverser = registry.queryAdapter(request, ITraversable, ns) |
|
136 if traverser is None: |
|
137 raise NotFound() |
|
138 ob = traverser.traverse(name, vpath_tuple[vroot_idx + i + 1:]) |
|
139 i += 1 |
|
140 continue |
|
141 |
|
142 try: |
|
143 getitem = ob.__getitem__ |
|
144 except AttributeError: |
|
145 return {'context': ob, |
|
146 'view_name': segment, |
|
147 'subpath': vpath_tuple[i + 1:], |
|
148 'traversed': vpath_tuple[:vroot_idx + i + 1], |
|
149 'virtual_root': vroot, |
|
150 'virtual_root_path': vroot_tuple, |
|
151 'root': root} |
|
152 |
|
153 try: |
|
154 next = getitem(segment) |
|
155 except KeyError: |
|
156 return {'context': ob, |
|
157 'view_name': segment, |
|
158 'subpath': vpath_tuple[i + 1:], |
|
159 'traversed': vpath_tuple[:vroot_idx + i + 1], |
|
160 'virtual_root': vroot, |
|
161 'virtual_root_path': vroot_tuple, |
|
162 'root': root} |
|
163 if i == vroot_idx: |
|
164 vroot = next |
|
165 ob = next |
|
166 i += 1 |
|
167 |
|
168 if ob is not root: |
|
169 request.registry.notify(BeforeTraverseEvent(ob, request)) |
|
170 |
|
171 return {'context': ob, |
|
172 'view_name': empty, |
|
173 'subpath': subpath, |
|
174 'traversed': vpath_tuple, |
|
175 'virtual_root': vroot, |
|
176 'virtual_root_path': vroot_tuple, |
|
177 'root': root} |
|
178 |
|
179 |
|
180 def get_parent(context, interface=Interface, allow_context=True, condition=None): |
|
181 """Get first parent of the context that implements given interface |
|
182 |
|
183 :param object context: base element |
|
184 :param Interface interface: the interface that parend should implement |
|
185 :param boolean allow_context: if 'True' (the default), traversing is done starting with context; otherwise, |
|
186 traversing is done starting from context's parent |
|
187 :param callable condition: an optional function that should return a 'True' result when called with parent |
|
188 as first argument |
|
189 """ |
|
190 if allow_context: |
|
191 parent = context |
|
192 else: |
|
193 parent = getattr(context, '__parent__', None) |
|
194 while parent is not None: |
|
195 if interface.providedBy(parent): |
|
196 target = interface(parent) |
|
197 if (not condition) or condition(target): |
|
198 return target |
|
199 parent = getattr(parent, '__parent__', None) |
|
200 return None |
|
201 |
|
202 |
|
203 @adapter_config(context=IContained, provides=IPathElements) |
|
204 class PathElementsAdapter(ContextAdapter): |
|
205 """Contained object path elements adapter |
|
206 |
|
207 This interface is intended to be used inside a keyword index to |
|
208 be able to search object based on a given parent |
|
209 """ |
|
210 |
|
211 @property |
|
212 def parents(self): |
|
213 intids = query_utility(IIntIds) |
|
214 if intids is None: |
|
215 return [] |
|
216 return [intids.register(parent) for parent in lineage(self.context)] |