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/')
|
|
1795
by William Grant
Trailing slashes in URLs can now be detected by views accepting subpaths. |
50 |
['path', 'to', 'something', '']
|
1294.2.1
by William Grant
Add an object-traversal-based router. |
51 |
>>> _segment_path('/')
|
1795
by William Grant
Trailing slashes in URLs can now be detected by views accepting subpaths. |
52 |
['']
|
1294.2.1
by William Grant
Add an object-traversal-based router. |
53 |
"""
|
54 |
||
1795
by William Grant
Trailing slashes in URLs can now be detected by views accepting subpaths. |
55 |
segments = os.path.normpath(path).split(os.sep) |
1294.2.1
by William Grant
Add an object-traversal-based router. |
56 |
|
1795
by William Grant
Trailing slashes in URLs can now be detected by views accepting subpaths. |
57 |
# Remove empty segment caused by a leading seperator.
|
1294.2.1
by William Grant
Add an object-traversal-based router. |
58 |
if segments[0] == '': |
59 |
segments.pop(0) |
|
1795
by William Grant
Trailing slashes in URLs can now be detected by views accepting subpaths. |
60 |
|
61 |
# normpath removes trailing separators. But trailing seperators are
|
|
62 |
# meaningful to browsers, so we add an empty segment.
|
|
63 |
if path.endswith(os.sep) and len(segments) > 0 and segments[-1] != '': |
|
64 |
segments.append('') |
|
65 |
||
1294.2.1
by William Grant
Add an object-traversal-based router. |
66 |
return segments |
67 |
||
1294.3.2
by William Grant
Router->Publisher |
68 |
class Publisher(object): |
69 |
'''Publisher to resolve and generate paths.
|
|
1294.2.1
by William Grant
Add an object-traversal-based router. |
70 |
|
71 |
Maintains a registry of forward and reverse routes, dealing with paths
|
|
72 |
to objects and views published in the URL space.
|
|
73 |
'''
|
|
74 |
||
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
75 |
def __init__(self, root, default='+index', viewset=None): |
1294.2.1
by William Grant
Add an object-traversal-based router. |
76 |
self.fmap = {} # Forward map. |
77 |
self.rmap = {} # Reverse map. |
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
78 |
self.smap = {} |
1294.2.4
by William Grant
Support generation of view URLs. |
79 |
self.srmap = {} |
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
80 |
self.vmap = {} |
1294.2.4
by William Grant
Support generation of view URLs. |
81 |
self.vrmap = {} |
1294.2.1
by William Grant
Add an object-traversal-based router. |
82 |
self.root = root |
83 |
self.default = default |
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
84 |
self.viewset = viewset |
1294.2.1
by William Grant
Add an object-traversal-based router. |
85 |
|
86 |
def add_forward(self, src, segment, func, argc): |
|
87 |
"""Register a forward (path resolution) route."""
|
|
88 |
||
89 |
if src not in self.fmap: |
|
90 |
self.fmap[src] = {} |
|
91 |
||
92 |
# If a route already exists with the same source and name, we have a
|
|
93 |
# conflict. We don't like conflicts.
|
|
94 |
if segment in self.fmap[src]: |
|
95 |
raise RouteConflict((src, segment, func), |
|
96 |
(src, segment, self.fmap[src][segment][0])) |
|
97 |
||
98 |
self.fmap[src][segment] = (func, argc) |
|
99 |
||
1294.2.67
by William Grant
Add i.w.r support for directly adding annotated routing functions. |
100 |
def add_forward_func(self, func): |
101 |
frm = func._forward_route_meta |
|
102 |
self.add_forward(frm['src'], frm['segment'], func, frm['argc']) |
|
103 |
||
1294.2.1
by William Grant
Add an object-traversal-based router. |
104 |
def add_reverse(self, src, func): |
105 |
"""Register a reverse (path generation) route."""
|
|
106 |
||
107 |
if src in self.rmap: |
|
108 |
raise RouteConflict((src, func), (src, self.rmap[src])) |
|
109 |
self.rmap[src] = func |
|
110 |
||
1294.2.67
by William Grant
Add i.w.r support for directly adding annotated routing functions. |
111 |
def add_reverse_func(self, func): |
112 |
self.add_reverse(func._reverse_route_src, func) |
|
113 |
||
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
114 |
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. |
115 |
"""Add a named view for a class, in the specified view set.
|
116 |
||
117 |
If the name is None, the view will live immediately under the source
|
|
118 |
object. This should be used only if you need the view to have a
|
|
119 |
subpath -- otherwise just using a view with the default name is
|
|
120 |
better.
|
|
121 |
"""
|
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
122 |
|
123 |
if src not in self.vmap: |
|
124 |
self.vmap[src] = {} |
|
125 |
||
126 |
if viewset not in self.vmap[src]: |
|
127 |
self.vmap[src][viewset] = {} |
|
128 |
||
1294.2.4
by William Grant
Support generation of view URLs. |
129 |
if src not in self.vrmap: |
130 |
self.vrmap[src] = {} |
|
131 |
||
132 |
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. |
133 |
raise RouteConflict((src, name, cls, viewset), |
134 |
(src, name, self.vmap[src][viewset][name], viewset)) |
|
135 |
||
136 |
self.vmap[src][viewset][name] = cls |
|
1294.2.4
by William Grant
Support generation of view URLs. |
137 |
self.vrmap[src][cls] = (name, viewset) |
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
138 |
|
139 |
def add_set_switch(self, segment, viewset): |
|
140 |
"""Register a leading path segment to switch to a view set."""
|
|
141 |
||
1294.2.7
by William Grant
Fix conflict checking in Router.add_set_switch. |
142 |
if segment in self.smap: |
143 |
raise RouteConflict((segment, viewset), |
|
144 |
(segment, self.smap[segment])) |
|
145 |
||
146 |
if viewset in self.srmap: |
|
147 |
raise RouteConflict((segment, viewset), |
|
148 |
(self.srmap[viewset], viewset)) |
|
1294.2.4
by William Grant
Support generation of view URLs. |
149 |
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
150 |
self.smap[segment] = viewset |
1294.2.4
by William Grant
Support generation of view URLs. |
151 |
self.srmap[viewset] = segment |
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
152 |
|
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. |
153 |
def traversed_to_object(self, obj): |
154 |
"""Called when the path resolver encounters an object.
|
|
155 |
||
156 |
Can be overridden to perform checks on an object before
|
|
157 |
continuing resolution. This is handy for verifying permissions.
|
|
158 |
"""
|
|
159 |
# We do nothing by default.
|
|
160 |
pass
|
|
161 |
||
1294.2.1
by William Grant
Add an object-traversal-based router. |
162 |
def resolve(self, path): |
163 |
"""Resolve a path into an object.
|
|
164 |
||
165 |
Traverses the tree of routes using the given path.
|
|
166 |
"""
|
|
167 |
||
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
168 |
viewset = self.viewset |
169 |
todo = _segment_path(path) |
|
170 |
||
171 |
# Override the viewset if the first segment matches.
|
|
1294.2.16
by William Grant
Don't crash when resolving a path with no segments. |
172 |
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. |
173 |
viewset = self.smap[todo[0]] |
174 |
del todo[0] |
|
175 |
||
176 |
(obj, view, subpath) = self._traverse(todo, self.root, viewset) |
|
177 |
||
1294.2.12
by William Grant
Implement subpaths in resolution. |
178 |
return obj, view, subpath |
1294.2.1
by William Grant
Add an object-traversal-based router. |
179 |
|
1294.2.14
by William Grant
Implement subpaths in generation. |
180 |
def generate(self, obj, view=None, subpath=None): |
1294.2.1
by William Grant
Add an object-traversal-based router. |
181 |
"""Resolve an object into a path.
|
182 |
||
183 |
Traverse up the tree of reverse routes, generating a path which
|
|
184 |
resolves to the object.
|
|
185 |
"""
|
|
186 |
||
187 |
# Attempt to get all the way to the top. Each reverse route should
|
|
188 |
# return a (parent, pathsegments) tuple.
|
|
189 |
curobj = obj |
|
190 |
names = [] |
|
191 |
||
192 |
# None represents the root.
|
|
1294.3.1
by William Grant
Allow reverse routes to return the real root too. |
193 |
while curobj not in (ROOT, self.root): |
1294.2.1
by William Grant
Add an object-traversal-based router. |
194 |
route = self.rmap.get(type(curobj)) |
195 |
if route is None: |
|
196 |
raise NoPath(obj, curobj) |
|
197 |
(curobj, newnames) = route(curobj) |
|
198 |
||
199 |
# The reverse route can return either a string of one segment,
|
|
200 |
# or a tuple of many.
|
|
201 |
if isinstance(newnames, basestring): |
|
202 |
names.insert(0, newnames) |
|
203 |
else: |
|
204 |
names = list(newnames) + list(names) |
|
205 |
||
1294.2.4
by William Grant
Support generation of view URLs. |
206 |
if view is not None: |
207 |
# If we don't have the view registered for this type, we can do
|
|
208 |
# nothing.
|
|
209 |
if type(obj) not in self.vrmap or \ |
|
210 |
view not in self.vrmap[type(obj)]: |
|
211 |
raise NoPath(obj, view) |
|
212 |
||
213 |
(viewname, viewset) = self.vrmap[type(obj)][view] |
|
214 |
||
215 |
# If the view's set isn't the default one, we need to have it in
|
|
216 |
# the map.
|
|
217 |
if viewset != self.viewset: |
|
218 |
if viewset not in self.srmap: |
|
219 |
raise NoPath(obj, view) |
|
220 |
else: |
|
221 |
names = [self.srmap[viewset]] + names |
|
222 |
||
223 |
# Generate nice URLs for the default route, if it is the last.
|
|
224 |
if viewname != self.default: |
|
1294.2.44
by William Grant
Implement deep view generation. |
225 |
# Deep views may have multiple segments in their name.
|
226 |
if isinstance(viewname, basestring): |
|
227 |
names += [viewname] |
|
1294.2.47
by William Grant
Implement default deep views. |
228 |
elif viewname[-1] == '+index' and not subpath: |
229 |
# If the last segment of the path is the default view, we
|
|
230 |
# can omit it.
|
|
231 |
names += viewname[:-1] |
|
1294.2.44
by William Grant
Implement deep view generation. |
232 |
else: |
233 |
names += viewname |
|
1294.2.1
by William Grant
Add an object-traversal-based router. |
234 |
|
1294.2.14
by William Grant
Implement subpaths in generation. |
235 |
if subpath is not None: |
236 |
if isinstance(subpath, basestring): |
|
237 |
return os.path.join(os.path.join('/', *names), subpath) |
|
238 |
else: |
|
239 |
names += subpath |
|
1294.2.1
by William Grant
Add an object-traversal-based router. |
240 |
return os.path.join('/', *names) |
241 |
||
1294.2.72
by William Grant
Add Router.get_ancestors. |
242 |
def get_ancestors(self, obj): |
243 |
"""Get a sequence of an object's ancestors.
|
|
244 |
||
245 |
Traverse up the tree of reverse routes, taking note of all ancestors.
|
|
246 |
"""
|
|
247 |
||
248 |
# Attempt to get all the way to the top. Each reverse route should
|
|
249 |
# return a (parent, pathsegments) tuple. We don't care about
|
|
250 |
# pathsegments in this case.
|
|
251 |
objs = [obj] |
|
252 |
||
253 |
# None represents the root.
|
|
1294.3.1
by William Grant
Allow reverse routes to return the real root too. |
254 |
while objs[0] not in (ROOT, self.root): |
1294.2.72
by William Grant
Add Router.get_ancestors. |
255 |
route = self.rmap.get(type(objs[0])) |
256 |
if route is None: |
|
257 |
raise NoPath(obj, objs[0]) |
|
258 |
objs.insert(0, route(objs[0])[0]) |
|
259 |
||
260 |
return objs[1:] |
|
1294.2.14
by William Grant
Implement subpaths in generation. |
261 |
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
262 |
def _traverse(self, todo, obj, viewset): |
1294.2.1
by William Grant
Add an object-traversal-based router. |
263 |
"""Populate the object stack given a list of path segments.
|
264 |
||
265 |
Traverses the forward route tree, using the given path segments.
|
|
266 |
||
267 |
Intended to be used by route(), and nobody else.
|
|
268 |
"""
|
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
269 |
while True: |
270 |
# Attempt views first, then routes.
|
|
271 |
if type(obj) in self.vmap and \ |
|
272 |
viewset in self.vmap[type(obj)]: |
|
1294.2.42
by William Grant
Implement deep views. |
273 |
vnames = self.vmap[type(obj)][viewset] |
1795
by William Grant
Trailing slashes in URLs can now be detected by views accepting subpaths. |
274 |
# If there is a view with no name, it overrides
|
275 |
# everything. Regardless of what comes after, we take it
|
|
276 |
# and use all remaining segments as the subpath.
|
|
1294.2.136
by William Grant
Add publisher support for nameless views, directly under an object. |
277 |
if None in vnames: |
278 |
view = vnames[None] |
|
279 |
remove = 0 |
|
1795
by William Grant
Trailing slashes in URLs can now be detected by views accepting subpaths. |
280 |
# If there are no segments or only the empty segment left,
|
281 |
# attempt the default view. Otherwise, look for a view with
|
|
282 |
# the name in the first remaining path segment.
|
|
1294.2.136
by William Grant
Add publisher support for nameless views, directly under an object. |
283 |
else: |
1795
by William Grant
Trailing slashes in URLs can now be detected by views accepting subpaths. |
284 |
if todo in ([], ['']): |
285 |
view = vnames.get(self.default) |
|
286 |
else: |
|
287 |
view = vnames.get(todo[0]) |
|
1294.2.136
by William Grant
Add publisher support for nameless views, directly under an object. |
288 |
remove = 1 |
1294.2.42
by William Grant
Implement deep views. |
289 |
|
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
290 |
if view is not None: |
1294.2.136
by William Grant
Add publisher support for nameless views, directly under an object. |
291 |
return (obj, view, tuple(todo[remove:])) |
1694
by William Grant
Shuffle deep view code to make a bit more sense. |
292 |
# We have just one segment, but no view was found. Try
|
293 |
# appending the default view name.
|
|
294 |
elif len(todo) == 1: |
|
295 |
# Augment it with the default view name, and look it up.
|
|
296 |
view = vnames.get((todo[0], self.default)) |
|
297 |
if view is not None: |
|
298 |
return (obj, view, tuple(todo[2:])) |
|
1294.2.47
by William Grant
Implement default deep views. |
299 |
# Now we must check for deep views.
|
300 |
# A deep view is one that has a name consisting of
|
|
301 |
# multiple segments. It's messier than it could be, because
|
|
302 |
# we also allow omission of the final segment if it is the
|
|
303 |
# default view name.
|
|
1294.2.42
by William Grant
Implement deep views. |
304 |
elif len(todo) >= 2: |
1693
by William Grant
Add support for "really deep" (more than two segment) views. |
305 |
for x in range(2, len(todo) + 1): |
306 |
view = vnames.get(tuple(todo[:x])) |
|
307 |
if view is not None: |
|
308 |
return (obj, view, tuple(todo[x:])) |
|
1795
by William Grant
Trailing slashes in URLs can now be detected by views accepting subpaths. |
309 |
# If we have an empty final segment (indicating a
|
310 |
# trailing slash), replace it with the default view.
|
|
311 |
# Otherwise try just adding the default view name.
|
|
312 |
prefix = list(todo) |
|
313 |
if prefix[-1] == '': |
|
314 |
prefix.pop() |
|
315 |
||
316 |
view = vnames.get(tuple(prefix + [self.default])) |
|
1294.2.42
by William Grant
Implement deep views. |
317 |
if view is not None: |
1693
by William Grant
Add support for "really deep" (more than two segment) views. |
318 |
return (obj, view, tuple()) |
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
319 |
|
1294.2.26
by William Grant
Refactor view finding and subsequent NotFound checks. |
320 |
# If there are no segments left to use, or there are no routes, we
|
321 |
# get out.
|
|
1294.2.45
by William Grant
Fix the NotFound args when a class has no forward routes. |
322 |
if len(todo) == 0: |
1294.2.24
by William Grant
Always raise a NotFound if the path doesn't resolve to a view. |
323 |
raise NotFound(obj, '+index', ()) |
1294.2.2
by William Grant
Split out views from normal routes, and add a viewset concept. |
324 |
|
1294.2.45
by William Grant
Fix the NotFound args when a class has no forward routes. |
325 |
if type(obj) not in self.fmap: |
326 |
raise NotFound(obj, todo[0], todo[1:]) |
|
327 |
||
1294.2.30
by William Grant
Rename 'names' to 'routenames', so I stop confusing myself. |
328 |
routenames = self.fmap[type(obj)] |
1294.2.1
by William Grant
Add an object-traversal-based router. |
329 |
|
1294.2.31
by William Grant
Collapse route existence check. |
330 |
if todo[0] in routenames: |
1294.2.32
by William Grant
De-duplicate route retrieval. |
331 |
routename = todo[0] |
1294.2.1
by William Grant
Add an object-traversal-based router. |
332 |
# The first path segment is the route identifier, so we skip
|
333 |
# it when identifying arguments.
|
|
1294.2.29
by William Grant
Refactor forward route argument calculation. |
334 |
argoffset = 1 |
1294.2.30
by William Grant
Rename 'names' to 'routenames', so I stop confusing myself. |
335 |
elif None in routenames: |
1294.2.1
by William Grant
Add an object-traversal-based router. |
336 |
# Attempt traversal directly (with no intermediate segment)
|
337 |
# as a last resort.
|
|
1294.2.32
by William Grant
De-duplicate route retrieval. |
338 |
routename = None |
1294.2.29
by William Grant
Refactor forward route argument calculation. |
339 |
argoffset = 0 |
1294.2.27
by William Grant
Replace an if,else(if,else) with an if,elif,else. |
340 |
else: |
1294.2.39
by William Grant
Make tests pass. |
341 |
raise NotFound(obj, todo[0], tuple(todo[1:])) |
1294.2.1
by William Grant
Add an object-traversal-based router. |
342 |
|
1294.2.32
by William Grant
De-duplicate route retrieval. |
343 |
route, argc = routenames[routename] |
344 |
||
1294.2.29
by William Grant
Refactor forward route argument calculation. |
345 |
if argc is INF: |
346 |
args = todo[argoffset:] |
|
347 |
todo = [] |
|
348 |
else: |
|
349 |
args = todo[argoffset:argc + argoffset] |
|
350 |
todo = todo[argc + argoffset:] |
|
351 |
||
1294.2.15
by William Grant
Allow routes that take infinitely many arguments. |
352 |
if argc is not INF and len(args) != argc: |
1294.2.1
by William Grant
Add an object-traversal-based router. |
353 |
# There were too few path segments left. Die.
|
1294.2.25
by William Grant
Raise more sensible NotFounds where multiple arguments are involved. |
354 |
raise InsufficientPathSegments( |
355 |
obj, |
|
356 |
tuple(args) if len(args) != 1 else args[0], |
|
357 |
tuple(todo) |
|
358 |
)
|
|
359 |
||
360 |
newobj = route(obj, *args) |
|
361 |
||
362 |
if newobj is None: |
|
363 |
raise NotFound(obj, tuple(args) if len(args) != 1 else args[0], |
|
364 |
tuple(todo)) |
|
365 |
||
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. |
366 |
self.traversed_to_object(newobj) |
367 |
||
1294.2.25
by William Grant
Raise more sensible NotFounds where multiple arguments are involved. |
368 |
obj = newobj |
1294.2.1
by William Grant
Add an object-traversal-based router. |
369 |