1
# IVLE - Informatics Virtual Learning Environment
2
# Copyright (C) 2007-2009 The University of Melbourne
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.
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.
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
18
"""Object publishing URL utilities."""
22
ROOT = object() # Marker object for the root.
25
class PublishingError(Exception):
28
class NotFound(PublishingError):
29
"""The path did not resolve to an object."""
32
class InsufficientPathSegments(NotFound):
33
"""The path led to a route that expected more arguments."""
36
class NoPath(PublishingError):
37
"""There is no path from the given object to the root."""
40
class RouteConflict(PublishingError):
41
"""A route with the same discriminator is already registered."""
44
def _segment_path(path):
45
"""Split a path into its segments, after normalisation.
47
>>> _segment_path('/path/to/something')
48
['path', 'to', 'something']
49
>>> _segment_path('/path/to/something/')
50
['path', 'to', 'something']
51
>>> _segment_path('/')
55
segments = os.path.normpath(path).split('/')
57
# Remove empty segments caused by leading and trailing seperators.
60
if segments[-1] == '':
64
class Publisher(object):
65
'''Publisher to resolve and generate paths.
67
Maintains a registry of forward and reverse routes, dealing with paths
68
to objects and views published in the URL space.
71
def __init__(self, root, default='+index', viewset=None):
72
self.fmap = {} # Forward map.
73
self.rmap = {} # Reverse map.
79
self.default = default
80
self.viewset = viewset
82
def add_forward(self, src, segment, func, argc):
83
"""Register a forward (path resolution) route."""
85
if src not in self.fmap:
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]))
94
self.fmap[src][segment] = (func, argc)
96
def add_forward_func(self, func):
97
frm = func._forward_route_meta
98
self.add_forward(frm['src'], frm['segment'], func, frm['argc'])
100
def add_reverse(self, src, func):
101
"""Register a reverse (path generation) route."""
104
raise RouteConflict((src, func), (src, self.rmap[src]))
105
self.rmap[src] = func
107
def add_reverse_func(self, func):
108
self.add_reverse(func._reverse_route_src, func)
110
def add_view(self, src, name, cls, viewset=None):
111
"""Add a named view for a class, in the specified view set.
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
119
if src not in self.vmap:
122
if viewset not in self.vmap[src]:
123
self.vmap[src][viewset] = {}
125
if src not in self.vrmap:
128
if name in self.vmap[src][viewset] or cls in self.vrmap[src]:
129
raise RouteConflict((src, name, cls, viewset),
130
(src, name, self.vmap[src][viewset][name], viewset))
132
self.vmap[src][viewset][name] = cls
133
self.vrmap[src][cls] = (name, viewset)
135
def add_set_switch(self, segment, viewset):
136
"""Register a leading path segment to switch to a view set."""
138
if segment in self.smap:
139
raise RouteConflict((segment, viewset),
140
(segment, self.smap[segment]))
142
if viewset in self.srmap:
143
raise RouteConflict((segment, viewset),
144
(self.srmap[viewset], viewset))
146
self.smap[segment] = viewset
147
self.srmap[viewset] = segment
149
def traversed_to_object(self, obj):
150
"""Called when the path resolver encounters an object.
152
Can be overridden to perform checks on an object before
153
continuing resolution. This is handy for verifying permissions.
155
# We do nothing by default.
158
def resolve(self, path):
159
"""Resolve a path into an object.
161
Traverses the tree of routes using the given path.
164
viewset = self.viewset
165
todo = _segment_path(path)
167
# Override the viewset if the first segment matches.
168
if len(todo) > 0 and todo[0] in self.smap:
169
viewset = self.smap[todo[0]]
172
(obj, view, subpath) = self._traverse(todo, self.root, viewset)
174
return obj, view, subpath
176
def generate(self, obj, view=None, subpath=None):
177
"""Resolve an object into a path.
179
Traverse up the tree of reverse routes, generating a path which
180
resolves to the object.
183
# Attempt to get all the way to the top. Each reverse route should
184
# return a (parent, pathsegments) tuple.
188
# None represents the root.
189
while curobj not in (ROOT, self.root):
190
route = self.rmap.get(type(curobj))
192
raise NoPath(obj, curobj)
193
(curobj, newnames) = route(curobj)
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)
200
names = list(newnames) + list(names)
203
# If we don't have the view registered for this type, we can do
205
if type(obj) not in self.vrmap or \
206
view not in self.vrmap[type(obj)]:
207
raise NoPath(obj, view)
209
(viewname, viewset) = self.vrmap[type(obj)][view]
211
# If the view's set isn't the default one, we need to have it in
213
if viewset != self.viewset:
214
if viewset not in self.srmap:
215
raise NoPath(obj, view)
217
names = [self.srmap[viewset]] + names
219
# Generate nice URLs for the default route, if it is the last.
220
if viewname != self.default:
221
# Deep views may have multiple segments in their name.
222
if isinstance(viewname, basestring):
224
elif viewname[-1] == '+index' and not subpath:
225
# If the last segment of the path is the default view, we
227
names += viewname[:-1]
231
if subpath is not None:
232
if isinstance(subpath, basestring):
233
return os.path.join(os.path.join('/', *names), subpath)
236
return os.path.join('/', *names)
238
def get_ancestors(self, obj):
239
"""Get a sequence of an object's ancestors.
241
Traverse up the tree of reverse routes, taking note of all ancestors.
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.
249
# None represents the root.
250
while objs[0] not in (ROOT, self.root):
251
route = self.rmap.get(type(objs[0]))
253
raise NoPath(obj, objs[0])
254
objs.insert(0, route(objs[0])[0])
258
def _traverse(self, todo, obj, viewset):
259
"""Populate the object stack given a list of path segments.
261
Traverses the forward route tree, using the given path segments.
263
Intended to be used by route(), and nobody else.
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.
270
# Otherwise, look for a view with the name in the first
271
# remaining path segment.
272
vnames = self.vmap[type(obj)][viewset]
278
self.default if len(todo) == 0 else todo[0])
282
return (obj, view, tuple(todo[remove:]))
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
289
view = vnames.get(tuple(todo[:2]))
291
return (obj, view, tuple(todo[2:]))
293
# Augment it with the default view name, and look it up.
294
view = vnames.get((todo[0], self.default))
296
return (obj, view, tuple(todo[2:]))
298
# If there are no segments left to use, or there are no routes, we
301
raise NotFound(obj, '+index', ())
303
if type(obj) not in self.fmap:
304
raise NotFound(obj, todo[0], todo[1:])
306
routenames = self.fmap[type(obj)]
308
if todo[0] in routenames:
310
# The first path segment is the route identifier, so we skip
311
# it when identifying arguments.
313
elif None in routenames:
314
# Attempt traversal directly (with no intermediate segment)
319
raise NotFound(obj, todo[0], tuple(todo[1:]))
321
route, argc = routenames[routename]
324
args = todo[argoffset:]
327
args = todo[argoffset:argc + argoffset]
328
todo = todo[argc + argoffset:]
330
if argc is not INF and len(args) != argc:
331
# There were too few path segments left. Die.
332
raise InsufficientPathSegments(
334
tuple(args) if len(args) != 1 else args[0],
338
newobj = route(obj, *args)
341
raise NotFound(obj, tuple(args) if len(args) != 1 else args[0],
344
self.traversed_to_object(newobj)