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

« back to all changes in this revision

Viewing changes to ivle/webapp/publisher/__init__.py

  • Committer: William Grant
  • Date: 2009-12-08 03:50:24 UTC
  • mfrom: (1294.2.143 ui-the-third)
  • Revision ID: grantw@unimelb.edu.au-20091208035024-wjx8zp54gth15ph8
Merge ui-the-third. This is another UI revamp.

The header is now thin! Thin! The yellow bar is gone. The tabs are gone.
Breadcrumbs are here. Routes is replaced (with an object publishing
mechanism). Views are less repetitive. etc.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
 
 
18
"""Object publishing URL utilities."""
 
19
 
 
20
import os.path
 
21
 
 
22
ROOT = object() # Marker object for the root.
 
23
INF = object()
 
24
 
 
25
class PublishingError(Exception):
 
26
    pass
 
27
 
 
28
class NotFound(PublishingError):
 
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
 
 
36
class NoPath(PublishingError):
 
37
    """There is no path from the given object to the root."""
 
38
    pass
 
39
 
 
40
class RouteConflict(PublishingError):
 
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
 
 
64
class Publisher(object):
 
65
    '''Publisher to resolve and generate paths.
 
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
 
 
71
    def __init__(self, root, default='+index', viewset=None):
 
72
        self.fmap = {} # Forward map.
 
73
        self.rmap = {} # Reverse map.
 
74
        self.smap = {}
 
75
        self.srmap = {}
 
76
        self.vmap = {}
 
77
        self.vrmap = {}
 
78
        self.root = root
 
79
        self.default = default
 
80
        self.viewset = viewset
 
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
 
 
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
 
 
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
 
 
107
    def add_reverse_func(self, func):
 
108
        self.add_reverse(func._reverse_route_src, func)
 
109
 
 
110
    def add_view(self, src, name, cls, viewset=None):
 
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
        """
 
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
 
 
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]:
 
129
            raise RouteConflict((src, name, cls, viewset),
 
130
                         (src, name, self.vmap[src][viewset][name], viewset))
 
131
 
 
132
        self.vmap[src][viewset][name] = cls
 
133
        self.vrmap[src][cls] = (name, viewset)
 
134
 
 
135
    def add_set_switch(self, segment, viewset):
 
136
        """Register a leading path segment to switch to a view set."""
 
137
 
 
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))
 
145
 
 
146
        self.smap[segment] = viewset
 
147
        self.srmap[viewset] = segment
 
148
 
 
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
 
 
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
 
 
164
        viewset = self.viewset
 
165
        todo = _segment_path(path)
 
166
 
 
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]]
 
170
            del todo[0]
 
171
 
 
172
        (obj, view, subpath) = self._traverse(todo, self.root, viewset)
 
173
 
 
174
        return obj, view, subpath
 
175
 
 
176
    def generate(self, obj, view=None, subpath=None):
 
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.
 
189
        while curobj not in (ROOT, self.root):
 
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
 
 
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:
 
221
                # Deep views may have multiple segments in their name.
 
222
                if isinstance(viewname, basestring):
 
223
                    names += [viewname]
 
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]
 
228
                else:
 
229
                    names += viewname
 
230
 
 
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
 
236
        return os.path.join('/', *names)
 
237
 
 
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.
 
250
        while objs[0] not in (ROOT, self.root):
 
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:]
 
257
 
 
258
    def _traverse(self, todo, obj, viewset):
 
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
        """
 
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.
 
270
                # Otherwise, look for a view with the name in the first
 
271
                # remaining path segment.
 
272
                vnames = self.vmap[type(obj)][viewset]
 
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
 
280
 
 
281
                if view is not None:
 
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
 
287
                # default view name.
 
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:]))
 
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:]))
 
297
 
 
298
            # If there are no segments left to use, or there are no routes, we
 
299
            # get out.
 
300
            if len(todo) == 0:
 
301
                raise NotFound(obj, '+index', ())
 
302
 
 
303
            if type(obj) not in self.fmap:
 
304
                raise NotFound(obj, todo[0], todo[1:])
 
305
 
 
306
            routenames = self.fmap[type(obj)]
 
307
 
 
308
            if todo[0] in routenames:
 
309
                routename = todo[0]
 
310
                # The first path segment is the route identifier, so we skip
 
311
                # it when identifying arguments.
 
312
                argoffset = 1
 
313
            elif None in routenames:
 
314
                # Attempt traversal directly (with no intermediate segment)
 
315
                # as a last resort.
 
316
                routename = None
 
317
                argoffset = 0
 
318
            else:
 
319
                raise NotFound(obj, todo[0], tuple(todo[1:]))
 
320
 
 
321
            route, argc = routenames[routename]
 
322
 
 
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
 
 
330
            if argc is not INF and len(args) != argc:
 
331
                # There were too few path segments left. Die.
 
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
 
 
344
            self.traversed_to_object(newobj)
 
345
 
 
346
            obj = newobj
 
347