Edgewall Software

TracDev/ContextRefactoring: trac_context.py

File trac_context.py, 11.7 KB (added by cboos, 14 months ago)

Work in progress snapshot - this is a modified source:trunk/trac/context.py, which will later be renamed to trac/resource.py; it contains the definition of the Resource class, the IResourceManager interface and the ResourceSystem component.

Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2007 Edgewall Software
4# Copyright (C) 2006-2007 Alec Thomas <alec@swapoff.org>
5# Copyright (C) 2007 Christian Boos <cboos@neuf.fr>
6# All rights reserved.
7#
8# This software is licensed as described in the file COPYING, which
9# you should have received as part of this distribution. The terms
10# are also available at http://trac.edgewall.org/wiki/TracLicense.
11#
12# This software consists of voluntary contributions made by many
13# individuals. For the exact contribution history, see the revision
14# history and logs, available at http://trac.edgewall.org/log/.
15#
16# Author: Christian Boos <cboos@neuf.fr>
17#         Alec Thomas <alec@swapoff.org>
18
19from trac.core import *
20from trac.util.compat import reversed
21from trac.util.translation import _
22
23# TODO: rename this file to trac/resource.py
24
25class ResourceNotFound(TracError):
26    """Thrown when a non-existent resource is requested"""
27
28
29class IResourceManager(Interface):
30
31    def get_resource_realms():
32        """Generate realm strings identifying the realm of resources handled
33        by this component."""
34
35    def get_model(resource):
36        """Return a data model object corresponding to the given `resource`.
37
38        NOTE: this method could also be used to clarify the model.exists
39              semantic, e.g.
40               - when the resource.id is None, return a new model
41               - when the resource.id is not None, either return the
42                 corresponding model if it exists, None otherwise.
43              (currently achieving the above requires catching exceptions,
44              see #4130)
45        """
46
47    def get_resource_url(resource, href, **kwargs):
48        """Return the URL for the requested resource."""
49
50    def get_resource_description(resource, format=None):
51        """Return a representation of the resource, according to the `format`.
52
53        For example, the ticket with the ID 123 is represented like `'#123'`
54        for the `'compact'` format, `'Ticket #123'` for the default `None`
55        format.
56        With the `'summary'`format, more details about the resource will be
57        given, at the expense of a lookup to the resource's model.
58        """
59
60class Resource(object):
61    """Resource identifier.
62
63    This specifies as precisely as possible *which* resource in a Trac
64    system is manipulated.
65
66    A resource is identified by:
67     - a `realm` (a string like `'wiki'` or `'ticket'`)
68     - an `id`, which uniquely identifies a resource within its realm.
69       If the `id` information is not set, then the resource identifier
70       could be used to refer to the realm as a whole.
71     - an optional `version` information.
72       If `version` is `None`, this refers by convention to the latest
73       version of the resource.
74    (- a `project` identifier) 0.12 ?
75    (- a `field` identifier) 0.12 ?
76
77    This light-weight object can be used to access:
78     - the more heavy-weight `model` object encapsulating the complete data
79       and application logic associated to the "resource".
80     - a light-weight descriptor that can be used to have different ways to
81       describe the resource in a way specific to each resource realm
82   
83    """
84
85    __slots__ = ('_perm', 'realm', 'id', 'version', '_model', 'parent',
86                 '_manager')
87
88    def __init__(self, perm, realm, id, version, model, parent):
89        """Create a resource identifier."""
90        self._perm = perm
91        self.realm = realm
92        self.id = id
93        self.version = version
94        self._model = model
95        self.parent = parent
96        self._manager = None
97
98    def __repr__(self):
99        path = []
100        r = self
101        while r:
102            name = r.realm
103            if r.id:
104                name += ':' + unicode(r.id) # id can be numerical
105            if r.version:
106                name += '@' + unicode(r.version)
107            path.append(name)
108            r = r.parent
109        return '<Resource %r>' % (', '.join(reversed(path)))
110
111    def __eq__(self, other):
112        return self.realm == other.realm and \
113               self.id == other.id and \
114               self.version == other.version and \
115               self.parent == other.parent
116
117    def __contains__(self, action):
118        """Convenience method for doing a permission check on the resource.
119
120        Thus the calls:
121            'WIKI_VIEW' in perm(resource)
122           or
123            'WIKI_VIEW' in perm(realm, id, version)
124
125        are working as expected.
126        """
127        return self.perm
128
129    def __hash__(self):
130        """Hash this resource descriptor, including its hierarchy."""
131        path = []
132        current = self
133        while current:
134            path.extend((self.realm, self.id, self.version))
135            current = current.parent
136        return hash(tuple(path))
137
138    env = property(lambda self: self._perm.env)
139    perm = property(lambda self: self._perm.copy(self))
140
141    def _get_model(self):
142        if not self._model:
143            self._model = self.manager.get_model(self)
144            if not self._model:
145                raise ResourceNotFound(_("Can't retrieve model for %s:%s") %
146                                       (self.realm, self.id))
147        return self._model
148
149    model = property(_get_model)
150
151    def _get_manager(self):
152        if not self._manager:
153            self._manager = ResourceSystem(self.env) \
154                            .get_resource_manager(self.realm)
155            if not self._manager:
156                raise TracError(_("Can't retrieve manager for %s:%s") %
157                                (self.realm, self.id))
158        return self._manager
159
160    manager = property(_get_manager)
161
162    def url(self, href, path=None, **kwargs):
163        return self.manager.get_resource_url(self, href, path, **kwargs)
164
165    name = property(
166        lambda self: self.manager.get_resource_description(self))
167
168    shortname = property(
169        lambda self: self.manager.get_resource_description(self, 'compact'))
170    summary = property(
171        lambda self: self.manager.get_resource_description(self, 'summary'))
172
173    # -- methods for retrieving other Resource identifiers
174
175    def assert_copy(self, realm=False, id=False, version=False, model=None,
176                    parent=None):
177        """Create a new Resource using the current resource as a template.
178
179        Optional keyword arguments can be given to override `realm`, `id`,
180        `version` and set the `model`.
181        If `realm` is changed, then the original `id` and `version` values
182        will not be reused.
183        If `id` is changed, then the original `version` value will not be
184        reused.
185        In any case, the `model` value will not reused.
186
187        This method will raise a `PermissionError` exception if there's no
188        read permission for the specified resource.
189        """
190        return self._perm.assert_resource(*self._copy(realm, id, version,
191                                                      model, parent))
192
193    def __call__(self, realm=False, id=False, version=False, model=None,
194                 parent=None):
195        """Create another resource using the current resource as a template.
196
197        Unlike `copy()`, this will return `None` instead of a `Resource` object
198        if there's no read permission for the specified resource.
199        """
200        return self._perm(*self._copy(realm, id, version, model, parent))
201
202    def _copy(self, realm, id, version, model, parent):
203        if realm is False:  # i.e. not set
204            realm = self.realm
205        if realm != self.realm:
206            return (realm or '', id or None, version or None, model, parent)
207        else:
208            if id is False:
209                id = self.id or None
210            if version is False:
211                version = id == self.id and self.version or None
212            return (realm, id, version, model, parent)
213
214    # -- methods for retrieving children Resource identifiers
215   
216    def assert_child(self, realm, id=False, version=False, model=None):
217        """Retrieve a child resource for a secondary `realm`.
218
219        Same as `copy()`, except that it sets the parent to `self`.
220        """
221        return self._perm.assert_resource(*self._copy(realm, id, version,
222                                                      model, self))
223
224    def child(self, realm, id=False, version=False, model=None):
225        """Retrieve a child resource for a secondary `realm`.
226
227        Same as `__call__`, except that it sets the parent to `self`.
228        """
229        return self._perm(*self._copy(realm, id, version, model, self))
230   
231
232
233class ResourceSystem(Component):
234    """Resource identification and description.
235
236    This component makes the link between Resource identifiers and their
237    corresponding manager component.
238
239    It acts itself as a resource manager for resources in realms that are not
240    explicitely managed.
241    """
242
243    implements = (IResourceManager,)
244
245    resource_managers = ExtensionPoint(IResourceManager)
246
247    def __init__(self):
248        self._resource_managers_map = None
249
250    # IResourceManager methods
251
252    def get_resource_realms(self):
253        yield ''
254
255    def get_model(self, resource):
256        return None
257
258    def get_resource_url(self, resource, href, path=None, **kwargs):
259        """Produce an URL to the `resource`, using the `href` as a base.
260       
261        In addition, a relative `path` can be given to refer to another
262        resource within the same realm, and relative to the current resource.
263        """
264        if path and path[0] == '/': # absolute reference, start at project base
265            return href(path.lstrip('/'), **kwargs)
266        base = unicode(resource.id or '').split('/')
267        for comp in (path or '').split('/'):
268            if comp in ('.', ''):
269                continue
270            elif comp == '..':
271                if base:
272                    base.pop()
273            else:
274                base.append(comp)
275        if path in (None, '', '.') and resource.version is not None \
276               and 'version' not in kwargs:
277            kwargs['version'] = resource.version
278        return href(resource.realm, *base, **kwargs)
279
280    def get_resource_description(self, resource, format=None, **kwargs):
281        """Return a representation of the resource.
282
283        Typical formats are: `None` (default), `'compact'` or `'summary'`.
284
285        For example, the ticket with the ID 123 is represented like `'#123'`
286        in compact mode, `'Ticket #123'` in default mode and adds much more
287        ticket state information to this in summary mode.
288        """
289        name = '%s:%s' % (resource.realm, resource.id)
290        if format == 'summary':
291            name += ' at version %s' % resource.version
292        return name
293
294    # Public methods
295
296    def get_resource_manager(self, realm):
297        """Return the `ResourceManager` responsible for the given `realm`.
298
299        If there's no corresponding realm, this will be the `ResourceSystem`
300        itself.
301        """
302        # build a dict of realm keys to IResourceManager implementations
303        if not self._resource_managers_map:
304            map = {}
305            for manager in self.resource_managers:
306                for manager_realm in manager.get_resource_realms():
307                    map[manager_realm] = manager
308            self._resource_managers_map = map
309        return self._resource_managers_map.get(realm, self)
310
311    def get_known_realms(self):
312        """Return a list of all the realm names of resource managers."""
313        realms = []
314        for manager in self.resource_managers:
315            for realm in manager.get_resource_realms():
316                realms.append(realm)
317        return realms