~azzar1/unity/add-show-desktop-key

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:]))
1694 by William Grant
Shuffle deep view code to make a bit more sense.
283
                # We have just one segment, but no view was found. Try
284
                # appending the default view name.
285
                elif len(todo) == 1:
286
                    # Augment it with the default view name, and look it up.
287
                    view = vnames.get((todo[0], self.default))
288
                    if view is not None:
289
                        return (obj, view, tuple(todo[2:]))
1294.2.47 by William Grant
Implement default deep views.
290
                # Now we must check for deep views.
291
                # A deep view is one that has a name consisting of
292
                # multiple segments. It's messier than it could be, because
293
                # we also allow omission of the final segment if it is the
294
                # default view name.
1294.2.42 by William Grant
Implement deep views.
295
                elif len(todo) >= 2:
1693 by William Grant
Add support for "really deep" (more than two segment) views.
296
                    for x in range(2, len(todo) + 1):
297
                        view = vnames.get(tuple(todo[:x]))
298
                        if view is not None:
299
                            return (obj, view, tuple(todo[x:]))
300
                    view = vnames.get(tuple(todo + [self.default]))
1294.2.42 by William Grant
Implement deep views.
301
                    if view is not None:
1693 by William Grant
Add support for "really deep" (more than two segment) views.
302
                        return (obj, view, tuple())
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
303
1294.2.26 by William Grant
Refactor view finding and subsequent NotFound checks.
304
            # If there are no segments left to use, or there are no routes, we
305
            # get out.
1294.2.45 by William Grant
Fix the NotFound args when a class has no forward routes.
306
            if len(todo) == 0:
1294.2.24 by William Grant
Always raise a NotFound if the path doesn't resolve to a view.
307
                raise NotFound(obj, '+index', ())
1294.2.2 by William Grant
Split out views from normal routes, and add a viewset concept.
308
1294.2.45 by William Grant
Fix the NotFound args when a class has no forward routes.
309
            if type(obj) not in self.fmap:
310
                raise NotFound(obj, todo[0], todo[1:])
311
1294.2.30 by William Grant
Rename 'names' to 'routenames', so I stop confusing myself.
312
            routenames = self.fmap[type(obj)]
1294.2.1 by William Grant
Add an object-traversal-based router.
313
1294.2.31 by William Grant
Collapse route existence check.
314
            if todo[0] in routenames:
1294.2.32 by William Grant
De-duplicate route retrieval.
315
                routename = todo[0]
1294.2.1 by William Grant
Add an object-traversal-based router.
316
                # The first path segment is the route identifier, so we skip
317
                # it when identifying arguments.
1294.2.29 by William Grant
Refactor forward route argument calculation.
318
                argoffset = 1
1294.2.30 by William Grant
Rename 'names' to 'routenames', so I stop confusing myself.
319
            elif None in routenames:
1294.2.1 by William Grant
Add an object-traversal-based router.
320
                # Attempt traversal directly (with no intermediate segment)
321
                # as a last resort.
1294.2.32 by William Grant
De-duplicate route retrieval.
322
                routename = None
1294.2.29 by William Grant
Refactor forward route argument calculation.
323
                argoffset = 0
1294.2.27 by William Grant
Replace an if,else(if,else) with an if,elif,else.
324
            else:
1294.2.39 by William Grant
Make tests pass.
325
                raise NotFound(obj, todo[0], tuple(todo[1:]))
1294.2.1 by William Grant
Add an object-traversal-based router.
326
1294.2.32 by William Grant
De-duplicate route retrieval.
327
            route, argc = routenames[routename]
328
1294.2.29 by William Grant
Refactor forward route argument calculation.
329
            if argc is INF:
330
                args = todo[argoffset:]
331
                todo = []
332
            else:
333
                args = todo[argoffset:argc + argoffset]
334
                todo = todo[argc + argoffset:]
335
1294.2.15 by William Grant
Allow routes that take infinitely many arguments.
336
            if argc is not INF and len(args) != argc:
1294.2.1 by William Grant
Add an object-traversal-based router.
337
                # There were too few path segments left. Die.
1294.2.25 by William Grant
Raise more sensible NotFounds where multiple arguments are involved.
338
                raise InsufficientPathSegments(
339
                                obj,
340
                                tuple(args) if len(args) != 1 else args[0],
341
                                tuple(todo)
342
                                )
343
344
            newobj = route(obj, *args)
345
346
            if newobj is None:
347
                raise NotFound(obj, tuple(args) if len(args) != 1 else args[0],
348
                               tuple(todo))
349
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.
350
            self.traversed_to_object(newobj)
351
1294.2.25 by William Grant
Raise more sensible NotFounds where multiple arguments are involved.
352
            obj = newobj
1294.2.1 by William Grant
Add an object-traversal-based router.
353