| 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 | |
|---|
| 19 | from trac.core import * |
|---|
| 20 | from trac.util.compat import reversed |
|---|
| 21 | from trac.util.translation import _ |
|---|
| 22 | |
|---|
| 23 | # TODO: rename this file to trac/resource.py |
|---|
| 24 | |
|---|
| 25 | class ResourceNotFound(TracError): |
|---|
| 26 | """Thrown when a non-existent resource is requested""" |
|---|
| 27 | |
|---|
| 28 | |
|---|
| 29 | class 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 | |
|---|
| 60 | class 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 | |
|---|
| 233 | class 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 |
|---|