1294.2.1
by William Grant
Add an object-traversal-based router. |
1 |
# IVLE - Informatics Virtual Learning Environment
|
2 |
# Copyright (C) 2007-2009 The University of Melbourne
|
|
3 |
#
|
|
4 |
# This program is free software; you can redistribute it and/or modify
|
|
5 |
# it under the terms of the GNU General Public License as published by
|
|
6 |
# the Free Software Foundation; either version 2 of the License, or
|
|
7 |
# (at your option) any later version.
|
|
8 |
#
|
|
9 |
# This program is distributed in the hope that it will be useful,
|
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12 |
# GNU General Public License for more details.
|
|
13 |
#
|
|
14 |
# You should have received a copy of the GNU General Public License
|
|
15 |
# along with this program; if not, write to the Free Software
|
|
16 |
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17 |
||
1294.3.2
by William Grant
Router->Publisher |
18 |
"""Object publishing URL utilities."""
|
1294.2.1
by William Grant
Add an object-traversal-based router. |
19 |
|
20 |
import os.path |
|
21 |
||
22 |
ROOT = object() # Marker object for the root. |
|
1294.2.15
by William Grant
Allow routes that take infinitely many arguments. |
23 |
INF = object() |
1294.2.1
by William Grant
Add an object-traversal-based router. |
24 |
|
1294.3.2
by William Grant
Router->Publisher |
25 |
class PublishingError(Exception): |
1294.2.1
by William Grant
Add an object-traversal-based router. |
26 |
pass
|
27 |
||
1294.3.2
by William Grant
Router->Publisher |
28 |
class NotFound(PublishingError): |
1294.2.1
by William Grant
Add an object-traversal-based router. |
29 |
"""The path did not resolve to an object."""
|
30 |
pass
|
|
31 |
||
32 |
class InsufficientPathSegments(NotFound): |
|
33 |
"""The path led to a route that expected more arguments."""
|
|
34 |
pass
|
|
35 |
||
1294.3.2
by William Grant
Router->Publisher |
36 |
class NoPath(PublishingError): |
1294.2.1
by William Grant
Add an object-traversal-based router. |
37 |
"""There is no path from the given object to the root."""
|
38 |
pass
|
|
39 |
||
1294.3.2
by William Grant
Router->Publisher |
40 |
class RouteConflict(PublishingError): |
1294.2.1
by William Grant
Add an object-traversal-based router. |
41 |
"""A route with the same discriminator is already registered."""
|
42 |
pass
|
|
43 |
||
44 |
def _segment_path(path): |
|
45 |
"""Split a path into its segments, after normalisation.
|
|
46 |
||
47 |
>>> _segment_path('/path/to/something')
|
|
48 |
['path', 'to', 'something']
|
|
49 |
>>> _segment_path('/path/to/something/')
|
|
50 |
['path', 'to', 'something']
|
|
51 |
>>> _segment_path('/')
|
|
52 |
[]
|
|
53 |
"""
|
|
54 |
||
55 |
segments = os.path.normpath(path).split('/') |
|
56 |
||
57 |
# Remove empty segments caused by leading and trailing seperators.
|
|
58 |
if segments[0] == '': |
|
59 |
segments.pop(0) |
|
60 |
if segments[-1] == '': |
|
61 |
segments.pop() |
|
62 |
return segments |
|
63 |
||
1294.3.2
by William Grant
Router->Publisher |
64 |
class Publisher(object): |
65 |
'''Publisher to resolve and generate paths.
|
|
1294.2.1
by William Grant
Add an object-traversal-based router. |
66 |
|
67 |
Maintains a registry of forward and reverse routes, dealing with paths
|
|
68 |
to objects and views published in the URL space.
|
|
69 |
'''
|
|
70 |
||
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
71 |
def __init__(self, root, default='+index', viewset=None): |
1294.2.1
by William Grant
Add an object-traversal-based router. |
72 |
self.fmap = {} # Forward map. |
73 |
self.rmap = {} # Reverse map. |
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
74 |
self.smap = {} |
1294.2.4
by William Grant
Support generation of view URLs. |
75 |
self.srmap = {} |
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
76 |
self.vmap = {} |
1294.2.4
by William Grant
Support generation of view URLs. |
77 |
self.vrmap = {} |
1294.2.1
by William Grant
Add an object-traversal-based router. |
78 |
self.root = root |
79 |
self.default = default |
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
80 |
self.viewset = viewset |
1294.2.1
by William Grant
Add an object-traversal-based router. |
81 |
|
82 |
def add_forward(self, src, segment, func, argc): |
|
83 |
"""Register a forward (path resolution) route."""
|
|
84 |
||
85 |
if src not in self.fmap: |
|
86 |
self.fmap[src] = {} |
|
87 |
||
88 |
# If a route already exists with the same source and name, we have a
|
|
89 |
# conflict. We don't like conflicts.
|
|
90 |
if segment in self.fmap[src]: |
|
91 |
raise RouteConflict((src, segment, func), |
|
92 |
(src, segment, self.fmap[src][segment][0])) |
|
93 |
||
94 |
self.fmap[src][segment] = (func, argc) |
|
95 |
||
1294.2.67
by William Grant
Add i.w.r support for directly adding annotated routing functions. |
96 |
def add_forward_func(self, func): |
97 |
frm = func._forward_route_meta |
|
98 |
self.add_forward(frm['src'], frm['segment'], func, frm['argc']) |
|
99 |
||
1294.2.1
by William Grant
Add an object-traversal-based router. |
100 |
def add_reverse(self, src, func): |
101 |
"""Register a reverse (path generation) route."""
|
|
102 |
||
103 |
if src in self.rmap: |
|
104 |
raise RouteConflict((src, func), (src, self.rmap[src])) |
|
105 |
self.rmap[src] = func |
|
106 |
||
1294.2.67
by William Grant
Add i.w.r support for directly adding annotated routing functions. |
107 |
def add_reverse_func(self, func): |
108 |
self.add_reverse(func._reverse_route_src, func) |
|
109 |
||
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
110 |
def add_view(self, src, name, cls, viewset=None): |
1294.2.136
by William Grant
Add publisher support for nameless views, directly under an object. |
111 |
"""Add a named view for a class, in the specified view set.
|
112 |
||
113 |
If the name is None, the view will live immediately under the source
|
|
114 |
object. This should be used only if you need the view to have a
|
|
115 |
subpath -- otherwise just using a view with the default name is
|
|
116 |
better.
|
|
117 |
"""
|
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
118 |
|
119 |
if src not in self.vmap: |
|
120 |
self.vmap[src] = {} |
|
121 |
||
122 |
if viewset not in self.vmap[src]: |
|
123 |
self.vmap[src][viewset] = {} |
|
124 |
||
1294.2.4
by William Grant
Support generation of view URLs. |
125 |
if src not in self.vrmap: |
126 |
self.vrmap[src] = {} |
|
127 |
||
128 |
if name in self.vmap[src][viewset] or cls in self.vrmap[src]: |
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
129 |
raise RouteConflict((src, name, cls, viewset), |
130 |
(src, name, self.vmap[src][viewset][name], viewset)) |
|
131 |
||
132 |
self.vmap[src][viewset][name] = cls |
|
1294.2.4
by William Grant
Support generation of view URLs. |
133 |
self.vrmap[src][cls] = (name, viewset) |
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
134 |
|
135 |
def add_set_switch(self, segment, viewset): |
|
136 |
"""Register a leading path segment to switch to a view set."""
|
|
137 |
||
1294.2.7
by William Grant
Fix conflict checking in Router.add_set_switch. |
138 |
if segment in self.smap: |
139 |
raise RouteConflict((segment, viewset), |
|
140 |
(segment, self.smap[segment])) |
|
141 |
||
142 |
if viewset in self.srmap: |
|
143 |
raise RouteConflict((segment, viewset), |
|
144 |
(self.srmap[viewset], viewset)) |
|
1294.2.4
by William Grant
Support generation of view URLs. |
145 |
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
146 |
self.smap[segment] = viewset |
1294.2.4
by William Grant
Support generation of view URLs. |
147 |
self.srmap[viewset] = segment |
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
148 |
|
1294.2.129
by William Grant
Refuse to traverse through an object to which the user has no permissions. This stops information leakage in breadcrumbs. |
149 |
def traversed_to_object(self, obj): |
150 |
"""Called when the path resolver encounters an object.
|
|
151 |
||
152 |
Can be overridden to perform checks on an object before
|
|
153 |
continuing resolution. This is handy for verifying permissions.
|
|
154 |
"""
|
|
155 |
# We do nothing by default.
|
|
156 |
pass
|
|
157 |
||
1294.2.1
by William Grant
Add an object-traversal-based router. |
158 |
def resolve(self, path): |
159 |
"""Resolve a path into an object.
|
|
160 |
||
161 |
Traverses the tree of routes using the given path.
|
|
162 |
"""
|
|
163 |
||
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
164 |
viewset = self.viewset |
165 |
todo = _segment_path(path) |
|
166 |
||
167 |
# Override the viewset if the first segment matches.
|
|
1294.2.16
by William Grant
Don't crash when resolving a path with no segments. |
168 |
if len(todo) > 0 and todo[0] in self.smap: |
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
169 |
viewset = self.smap[todo[0]] |
170 |
del todo[0] |
|
171 |
||
172 |
(obj, view, subpath) = self._traverse(todo, self.root, viewset) |
|
173 |
||
1294.2.12
by William Grant
Implement subpaths in resolution. |
174 |
return obj, view, subpath |
1294.2.1
by William Grant
Add an object-traversal-based router. |
175 |
|
1294.2.14
by William Grant
Implement subpaths in generation. |
176 |
def generate(self, obj, view=None, subpath=None): |
1294.2.1
by William Grant
Add an object-traversal-based router. |
177 |
"""Resolve an object into a path.
|
178 |
||
179 |
Traverse up the tree of reverse routes, generating a path which
|
|
180 |
resolves to the object.
|
|
181 |
"""
|
|
182 |
||
183 |
# Attempt to get all the way to the top. Each reverse route should
|
|
184 |
# return a (parent, pathsegments) tuple.
|
|
185 |
curobj = obj |
|
186 |
names = [] |
|
187 |
||
188 |
# None represents the root.
|
|
1294.3.1
by William Grant
Allow reverse routes to return the real root too. |
189 |
while curobj not in (ROOT, self.root): |
1294.2.1
by William Grant
Add an object-traversal-based router. |
190 |
route = self.rmap.get(type(curobj)) |
191 |
if route is None: |
|
192 |
raise NoPath(obj, curobj) |
|
193 |
(curobj, newnames) = route(curobj) |
|
194 |
||
195 |
# The reverse route can return either a string of one segment,
|
|
196 |
# or a tuple of many.
|
|
197 |
if isinstance(newnames, basestring): |
|
198 |
names.insert(0, newnames) |
|
199 |
else: |
|
200 |
names = list(newnames) + list(names) |
|
201 |
||
1294.2.4
by William Grant
Support generation of view URLs. |
202 |
if view is not None: |
203 |
# If we don't have the view registered for this type, we can do
|
|
204 |
# nothing.
|
|
205 |
if type(obj) not in self.vrmap or \ |
|
206 |
view not in self.vrmap[type(obj)]: |
|
207 |
raise NoPath(obj, view) |
|
208 |
||
209 |
(viewname, viewset) = self.vrmap[type(obj)][view] |
|
210 |
||
211 |
# If the view's set isn't the default one, we need to have it in
|
|
212 |
# the map.
|
|
213 |
if viewset != self.viewset: |
|
214 |
if viewset not in self.srmap: |
|
215 |
raise NoPath(obj, view) |
|
216 |
else: |
|
217 |
names = [self.srmap[viewset]] + names |
|
218 |
||
219 |
# Generate nice URLs for the default route, if it is the last.
|
|
220 |
if viewname != self.default: |
|
1294.2.44
by William Grant
Implement deep view generation. |
221 |
# Deep views may have multiple segments in their name.
|
222 |
if isinstance(viewname, basestring): |
|
223 |
names += [viewname] |
|
1294.2.47
by William Grant
Implement default deep views. |
224 |
elif viewname[-1] == '+index' and not subpath: |
225 |
# If the last segment of the path is the default view, we
|
|
226 |
# can omit it.
|
|
227 |
names += viewname[:-1] |
|
1294.2.44
by William Grant
Implement deep view generation. |
228 |
else: |
229 |
names += viewname |
|
1294.2.1
by William Grant
Add an object-traversal-based router. |
230 |
|
1294.2.14
by William Grant
Implement subpaths in generation. |
231 |
if subpath is not None: |
232 |
if isinstance(subpath, basestring): |
|
233 |
return os.path.join(os.path.join('/', *names), subpath) |
|
234 |
else: |
|
235 |
names += subpath |
|
1294.2.1
by William Grant
Add an object-traversal-based router. |
236 |
return os.path.join('/', *names) |
237 |
||
1294.2.72
by William Grant
Add Router.get_ancestors. |
238 |
def get_ancestors(self, obj): |
239 |
"""Get a sequence of an object's ancestors.
|
|
240 |
||
241 |
Traverse up the tree of reverse routes, taking note of all ancestors.
|
|
242 |
"""
|
|
243 |
||
244 |
# Attempt to get all the way to the top. Each reverse route should
|
|
245 |
# return a (parent, pathsegments) tuple. We don't care about
|
|
246 |
# pathsegments in this case.
|
|
247 |
objs = [obj] |
|
248 |
||
249 |
# None represents the root.
|
|
1294.3.1
by William Grant
Allow reverse routes to return the real root too. |
250 |
while objs[0] not in (ROOT, self.root): |
1294.2.72
by William Grant
Add Router.get_ancestors. |
251 |
route = self.rmap.get(type(objs[0])) |
252 |
if route is None: |
|
253 |
raise NoPath(obj, objs[0]) |
|
254 |
objs.insert(0, route(objs[0])[0]) |
|
255 |
||
256 |
return objs[1:] |
|
1294.2.14
by William Grant
Implement subpaths in generation. |
257 |
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
258 |
def _traverse(self, todo, obj, viewset): |
1294.2.1
by William Grant
Add an object-traversal-based router. |
259 |
"""Populate the object stack given a list of path segments.
|
260 |
||
261 |
Traverses the forward route tree, using the given path segments.
|
|
262 |
||
263 |
Intended to be used by route(), and nobody else.
|
|
264 |
"""
|
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
265 |
while True: |
266 |
# Attempt views first, then routes.
|
|
267 |
if type(obj) in self.vmap and \ |
|
268 |
viewset in self.vmap[type(obj)]: |
|
269 |
# If there are no segments left, attempt the default view.
|
|
1294.2.26
by William Grant
Refactor view finding and subsequent NotFound checks. |
270 |
# Otherwise, look for a view with the name in the first
|
271 |
# remaining path segment.
|
|
1294.2.42
by William Grant
Implement deep views. |
272 |
vnames = self.vmap[type(obj)][viewset] |
1294.2.136
by William Grant
Add publisher support for nameless views, directly under an object. |
273 |
if None in vnames: |
274 |
view = vnames[None] |
|
275 |
remove = 0 |
|
276 |
else: |
|
277 |
view = vnames.get( |
|
278 |
self.default if len(todo) == 0 else todo[0]) |
|
279 |
remove = 1 |
|
1294.2.42
by William Grant
Implement deep views. |
280 |
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
281 |
if view is not None: |
1294.2.136
by William Grant
Add publisher support for nameless views, directly under an object. |
282 |
return (obj, view, tuple(todo[remove:])) |
1294.2.47
by William Grant
Implement default deep views. |
283 |
# Now we must check for deep views.
|
284 |
# A deep view is one that has a name consisting of
|
|
285 |
# multiple segments. It's messier than it could be, because
|
|
286 |
# we also allow omission of the final segment if it is the
|
|
287 |
# default view name.
|
|
1294.2.42
by William Grant
Implement deep views. |
288 |
elif len(todo) >= 2: |
289 |
view = vnames.get(tuple(todo[:2])) |
|
290 |
if view is not None: |
|
291 |
return (obj, view, tuple(todo[2:])) |
|
1294.2.47
by William Grant
Implement default deep views. |
292 |
elif len(todo) == 1: |
293 |
# Augment it with the default view name, and look it up.
|
|
294 |
view = vnames.get((todo[0], self.default)) |
|
295 |
if view is not None: |
|
296 |
return (obj, view, tuple(todo[2:])) |
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
297 |
|
1294.2.26
by William Grant
Refactor view finding and subsequent NotFound checks. |
298 |
# If there are no segments left to use, or there are no routes, we
|
299 |
# get out.
|
|
1294.2.45
by William Grant
Fix the NotFound args when a class has no forward routes. |
300 |
if len(todo) == 0: |
1294.2.24
by William Grant
Always raise a NotFound if the path doesn't resolve to a view. |
301 |
raise NotFound(obj, '+index', ()) |
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
302 |
|
1294.2.45
by William Grant
Fix the NotFound args when a class has no forward routes. |
303 |
if type(obj) not in self.fmap: |
304 |
raise NotFound(obj, todo[0], todo[1:]) |
|
305 |
||
1294.2.30
by William Grant
Rename 'names' to 'routenames', so I stop confusing myself. |
306 |
routenames = self.fmap[type(obj)] |
1294.2.1
by William Grant
Add an object-traversal-based router. |
307 |
|
1294.2.31
by William Grant
Collapse route existence check. |
308 |
if todo[0] in routenames: |
1294.2.32
by William Grant
De-duplicate route retrieval. |
309 |
routename = todo[0] |
1294.2.1
by William Grant
Add an object-traversal-based router. |
310 |
# The first path segment is the route identifier, so we skip
|
311 |
# it when identifying arguments.
|
|
1294.2.29
by William Grant
Refactor forward route argument calculation. |
312 |
argoffset = 1 |
1294.2.30
by William Grant
Rename 'names' to 'routenames', so I stop confusing myself. |
313 |
elif None in routenames: |
1294.2.1
by William Grant
Add an object-traversal-based router. |
314 |
# Attempt traversal directly (with no intermediate segment)
|
315 |
# as a last resort.
|
|
1294.2.32
by William Grant
De-duplicate route retrieval. |
316 |
routename = None |
1294.2.29
by William Grant
Refactor forward route argument calculation. |
317 |
argoffset = 0 |
1294.2.27
by William Grant
Replace an if,else(if,else) with an if,elif,else. |
318 |
else: |
1294.2.39
by William Grant
Make tests pass. |
319 |
raise NotFound(obj, todo[0], tuple(todo[1:])) |
1294.2.1
by William Grant
Add an object-traversal-based router. |
320 |
|
1294.2.32
by William Grant
De-duplicate route retrieval. |
321 |
route, argc = routenames[routename] |
322 |
||
1294.2.29
by William Grant
Refactor forward route argument calculation. |
323 |
if argc is INF: |
324 |
args = todo[argoffset:] |
|
325 |
todo = [] |
|
326 |
else: |
|
327 |
args = todo[argoffset:argc + argoffset] |
|
328 |
todo = todo[argc + argoffset:] |
|
329 |
||
1294.2.15
by William Grant
Allow routes that take infinitely many arguments. |
330 |
if argc is not INF and len(args) != argc: |
1294.2.1
by William Grant
Add an object-traversal-based router. |
331 |
# There were too few path segments left. Die.
|
1294.2.25
by William Grant
Raise more sensible NotFounds where multiple arguments are involved. |
332 |
raise InsufficientPathSegments( |
333 |
obj, |
|
334 |
tuple(args) if len(args) != 1 else args[0], |
|
335 |
tuple(todo) |
|
336 |
)
|
|
337 |
||
338 |
newobj = route(obj, *args) |
|
339 |
||
340 |
if newobj is None: |
|
341 |
raise NotFound(obj, tuple(args) if len(args) != 1 else args[0], |
|
342 |
tuple(todo)) |
|
343 |
||
1294.2.129
by William Grant
Refuse to traverse through an object to which the user has no permissions. This stops information leakage in breadcrumbs. |
344 |
self.traversed_to_object(newobj) |
345 |
||
1294.2.25
by William Grant
Raise more sensible NotFounds where multiple arguments are involved. |
346 |
obj = newobj |
1294.2.1
by William Grant
Add an object-traversal-based router. |
347 |