~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/')
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