source: tagsplugin/trunk/tractags/api.py

Last change on this file was 18143, checked in by Cinc-th, 2 years ago

TagsPlugin: replace unicode and dict.iteritems() statements which are no longer available in Python 3. Fix exception handling now requiring Exception as e. Fixed imports.

Refs #13994

File size: 18.4 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006 Alec Thomas <alec@swapoff.org>
4# Copyright (C) 2011-2014 Steffen Hoffmann <hoff.st@web.de>
5# Copyright (C) 2014 Jun Omae <jun66j5@gmail.com>
6# Copyright (C) 2014 Ryan J Ollos <ryan.j.ollos@gmail.com>
7#
8# This software is licensed as described in the file COPYING, which
9# you should have received as part of this distribution.
10#
11
12import collections
13import pkg_resources
14import re
15import threading
16
17from trac.config import BoolOption, ListOption, Option
18from trac.core import Component, ExtensionPoint, Interface, TracError
19from trac.core import implements
20from trac.perm import IPermissionPolicy, IPermissionRequestor
21from trac.perm import PermissionError, PermissionSystem
22from trac.resource import IResourceManager, get_resource_url
23from trac.resource import get_resource_description
24from trac.util import get_reporter_id
25from trac.util.text import to_unicode
26from trac.util.translation import domain_functions
27from trac.wiki.model import WikiPage
28
29# Import translation functions.
30add_domain, _, N_, gettext, ngettext, tag_, tagn_ = \
31    domain_functions('tractags', ('add_domain', '_', 'N_', 'gettext',
32                                  'ngettext', 'tag_', 'tagn_'))
33dgettext = None
34
35from tractags.model import resource_tags, tag_frequency, tag_resource
36from tractags.model import tagged_resources
37# Now call module importing i18n methods from here.
38from tractags.query import *
39
40REALM_RE = re.compile('realm:(\w+)', re.U | re.I)
41
42
43class InvalidTagRealm(TracError):
44    pass
45
46
47class ITagProvider(Interface):
48    """The interface for Components providing per-realm tag storage and
49    manipulation methods.
50
51    Change comments and reparenting are supported since tags-0.7.
52    """
53    def get_taggable_realm():
54        """Return the realm this provider supports tags on."""
55
56    def get_tagged_resources(req, tags=None, filter=None):
57        """Return a sequence of resources and *all* their tags.
58
59        :param tags: If provided, return only those resources with the given
60                     tags.
61        :param filter: If provided, skip matching resources.
62
63        :rtype: Sequence of (resource, tags) tuples.
64        """
65
66    def get_all_tags(req, filter=None):
67        """Return all tags with numbers of occurance.
68
69        :param filter: If provided, skip matching resources.
70
71        :rtype: Counter object (dict sub-class: {tag_name: tag_frequency} ).
72
73        """
74
75    def get_resource_tags(req, resource, when=None):
76        """Get tags for a Resource object."""
77
78    def resource_tags(resource):
79        """Get tags for a Resource object skipping permission checks."""
80
81    def set_resource_tags(req, resource, tags, comment=u'', when=None):
82        """Set tags for a resource."""
83
84    def reparent_resource_tags(req, resource, old_id, comment=u''):
85        """Move tags, typically when renaming an existing resource."""
86
87    def remove_resource_tags(req, resource, comment=u''):
88        """Remove all tags from a resource."""
89
90    def describe_tagged_resource(req, resource):
91        """Return a one line description of the tagged resource."""
92
93
94class DefaultTagProvider(Component):
95    """An abstract base tag provider that stores tags in the database.
96
97    Use this if you need storage for your tags. Simply set the class variable
98    `realm` and optionally `check_permission()`.
99
100    See tractags.wiki.WikiTagProvider for an example.
101    """
102
103    implements(ITagProvider)
104
105    abstract = True
106
107    # Resource realm this provider manages tags for. Set this.
108    realm = None
109
110    revisable = False
111
112    def __init__(self):
113        # Do this once, because configuration lookups are costly.
114        cfg = self.env.config
115        self.revisable = self.realm in cfg.getlist('tags', 'revisable_realms')
116
117    # Public methods
118
119    def check_permission(self, perm, action):
120        """Delegate function for checking permissions.
121
122        Override to implement custom permissions. Defaults to TAGS_VIEW and
123        TAGS_MODIFY.
124        """
125        map = {'view': 'TAGS_VIEW', 'modify': 'TAGS_MODIFY'}
126        return map[action] in perm('tag')
127
128    # ITagProvider methods
129
130    def get_taggable_realm(self):
131        return self.realm
132
133    def get_tagged_resources(self, req, tags=None, filter=None):
134        if not self.check_permission(req.perm, 'view'):
135            return
136        return tagged_resources(self.env, self.check_permission, req.perm,
137                                self.realm, tags, filter)
138
139    def get_all_tags(self, req, filter=None):
140        all_tags = collections.Counter()
141        for tag, count in tag_frequency(self.env, self.realm, filter):
142            all_tags[tag] = count
143        return all_tags
144
145    def get_resource_tags(self, req, resource, when=None):
146        assert resource.realm == self.realm
147        if not self.check_permission(req.perm(resource), 'view'):
148            return
149        return resource_tags(self.env, resource, when=when)
150
151    def resource_tags(self, resource):
152        assert resource.realm == self.realm
153        return resource_tags(self.env, resource)
154
155    def set_resource_tags(self, req, resource, tags, comment=u'', when=None):
156        assert resource.realm == self.realm
157        if not self.check_permission(req.perm(resource), 'modify'):
158            raise PermissionError(resource=resource, env=self.env)
159        tag_resource(self.env, resource, author=self._get_author(req),
160                     tags=tags, log=self.revisable, when=when)
161
162    def reparent_resource_tags(self, req, resource, old_id, comment=u''):
163        assert resource.realm == self.realm
164        if not self.check_permission(req.perm(resource), 'modify'):
165            raise PermissionError(resource=resource, env=self.env)
166        tag_resource(self.env, resource, old_id, self._get_author(req),
167                     log=self.revisable)
168
169    def remove_resource_tags(self, req, resource, comment=u''):
170        assert resource.realm == self.realm
171        if not self.check_permission(req.perm(resource), 'modify'):
172            raise PermissionError(resource=resource, env=self.env)
173        tag_resource(self.env, resource, author=self._get_author(req),
174                     log=self.revisable)
175
176    def describe_tagged_resource(self, req, resource):
177        raise NotImplementedError
178
179    def _get_author(self, req):
180        return get_reporter_id(req, 'author')
181
182
183class TagPolicy(Component):
184    """[extra] Security policy based on tags."""
185
186    implements(IPermissionPolicy)
187
188    def check_permission(self, action, username, resource, perm):
189        if resource is None or action.split('_')[0] != resource.realm.upper():
190            return None
191
192        from tractags.api import TagSystem
193
194        class FakeRequest(object):
195            def __init__(self, perm):
196                self.perm = perm
197
198        permission = action.lower().split('_')[1]
199        req = FakeRequest(perm)
200        tags = TagSystem(self.env).get_tags(None, resource)
201
202        # Explicitly denied?
203        if ':-'.join((username, permission)) in tags:
204            return False
205
206        # Find all granted permissions for the requesting user from
207        # tagged permissions by expanding any meta action as well.
208        if action in set(PermissionSystem(self.env).expand_actions(
209                         ['_'.join([resource.realm, t.split(':')[1]]).upper()
210                          for t in tags if t.split(':')[0] == username])):
211            return True
212
213
214class TagSystem(Component):
215    """[main] Tagging system for Trac.
216
217    Associating resources with tags is easy, faceted content classification.
218
219    Available components are marked according to their relevance as follows:
220
221     `[main]`:: provide core with a generic tagging engine as well as support
222     for common realms 'ticket' (TracTickets) and 'wiki' (TracWiki).
223     `[opt]`:: add more features to improve user experience and maintenance
224     `[extra]`:: enable advanced features for specific use cases
225
226    Make sure to understand their purpose before deactivating `[main]` or
227    activating `[extra]` components.
228    """
229
230    implements(IPermissionRequestor, IResourceManager)
231
232    tag_providers = ExtensionPoint(ITagProvider)
233
234    revisable = ListOption('tags', 'revisable_realms', 'wiki',
235        doc="Comma-separated list of realms requiring tag change history.")
236    wiki_page_link = BoolOption('tags', 'wiki_page_link', True,
237        doc="Link a tag to the wiki page with same name, if it exists.")
238    wiki_page_prefix = Option('tags', 'wiki_page_prefix', '',
239        doc="Prefix for tag wiki page names.")
240
241    # Internal variables
242    _realm_provider_map = None
243
244    def __init__(self):
245        # Bind the 'tractags' catalog to the specified locale directory.
246        locale_dir = pkg_resources.resource_filename(__name__, 'locale')
247        add_domain(self.env.path, locale_dir)
248
249        self._populate_provider_map()
250
251    # Public methods
252
253    def query(self, req, query='', attribute_handlers=None):
254        """Returns a sequence of (resource, tags) tuples matching a query.
255
256        Query syntax is described in tractags.query.
257
258        :param attribute_handlers: Register additional query attribute
259                                   handlers. See Query documentation for more
260                                   information.
261        """
262        def realm_handler(_, node, context):
263            return query.match(node, [context.realm])
264
265        all_attribute_handlers = {
266            'realm': realm_handler,
267        }
268        all_attribute_handlers.update(attribute_handlers or {})
269        query = Query(query, attribute_handlers=all_attribute_handlers)
270        providers = set()
271        for m in REALM_RE.finditer(query.as_string()):
272            realm = m.group(1)
273            providers.add(self._get_provider(realm))
274        if not providers:
275            providers = self.tag_providers
276
277        query_tags = set(query.terms())
278        for provider in providers:
279            self.env.log.debug('Querying ' + repr(provider))
280            for resource, tags in provider.get_tagged_resources(req,
281                                                          query_tags) or []:
282                if query(tags, context=resource):
283                    yield resource, tags
284
285    def get_taggable_realms(self, perm=None):
286        """Returns the names of available taggable realms as set.
287
288        If a `PermissionCache` object is passed as optional `perm` argument,
289        permission checks will be done for tag providers that have a
290        `check_permission` method.
291        """
292        return set(p.get_taggable_realm()
293                   for p in self.tag_providers
294                   if perm is None or not hasattr(p, 'check_permission') or
295                       p.check_permission(perm, 'view'))
296
297    def get_all_tags(self, req, realms=[]):
298        """Get all tags for all supported realms or only for specified ones.
299
300        Returns a Counter object (special dict) with tag name as key and tag
301        frequency as value.
302        """
303        all_tags = collections.Counter()
304        all_realms = self.get_taggable_realms(req.perm)
305        if not realms or set(realms) == all_realms:
306            realms = all_realms
307        for provider in self.tag_providers:
308            if provider.get_taggable_realm() in realms:
309                try:
310                    all_tags += provider.get_all_tags(req)
311                except AttributeError:
312                    # Fallback for older providers.
313                    try:
314                        for resource, tags in \
315                            provider.get_tagged_resources(req):
316                                all_tags.update(tags)
317                    except TypeError:
318                        # Defense against loose ITagProvider implementations,
319                        # that might become obsolete in the future.
320                        self.env.log.warning('ITagProvider %r has outdated'
321                                             'get_tagged_resources() method' %
322                                             provider)
323        return all_tags
324
325    def get_tags(self, req, resource, when=None):
326        """Get tags for resource."""
327        if not req:
328            # Bypass permission checks as required i. e. for TagsPolicy,
329            # an IPermissionProvider.
330            return set(self._get_provider(resource.realm) \
331                       .resource_tags(resource))
332        return set(self._get_provider(resource.realm) \
333                   .get_resource_tags(req, resource, when=when))
334
335    def set_tags(self, req, resource, tags, comment=u'', when=None):
336        """Set tags on a resource.
337
338        Existing tags are replaced.
339        """
340        try:
341            return self._get_provider(resource.realm) \
342                   .set_resource_tags(req, resource, set(tags), comment, when)
343        except TypeError:
344            # Handle old style tag providers gracefully.
345            return self._get_provider(resource.realm) \
346                   .set_resource_tags(req, resource, set(tags))
347
348    def add_tags(self, req, resource, tags, comment=u''):
349        """Add to existing tags on a resource."""
350        tags = set(tags)
351        tags.update(self.get_tags(req, resource))
352        try:
353            self.set_tags(req, resource, tags, comment)
354        except TypeError:
355            # Handle old style tag providers gracefully.
356            self.set_tags(req, resource, tags)
357
358    def reparent_tags(self, req, resource, old_name, comment=u''):
359        """Move tags, typically when renaming an existing resource.
360
361        Tags can't be moved between different tag realms with intention.
362        """
363        provider = self._get_provider(resource.realm)
364        provider.reparent_resource_tags(req, resource, old_name, comment)
365
366    def replace_tag(self, req, old_tags, new_tag=None, comment=u'',
367                    allow_delete=False, filter=[]):
368        """Replace one or more tags in all resources it exists/they exist in.
369
370        Tagged resources may be filtered by realm and tag deletion is
371        optionally allowed for convenience as well.
372        """
373        # Provide list regardless of attribute type.
374        for provider in [p for p in self.tag_providers
375                         if not filter or p.get_taggable_realm() in filter]:
376            for resource, tags in \
377                    provider.get_tagged_resources(req, old_tags):
378                old_tags = set(old_tags)
379                if old_tags.issuperset(tags) and not new_tag:
380                    if allow_delete:
381                        self.delete_tags(req, resource, None, comment)
382                else:
383                    s_tags = set(tags)
384                    eff_tags = s_tags - old_tags
385                    if new_tag:
386                        eff_tags.add(new_tag)
387                    # Prevent to touch resources without effective change.
388                    if eff_tags != s_tags and (allow_delete or new_tag):
389                        self.set_tags(req, resource, eff_tags, comment)
390
391    def delete_tags(self, req, resource, tags=None, comment=u''):
392        """Delete tags on a resource.
393
394        If tags is None, remove all tags on the resource.
395        """
396        provider = self._get_provider(resource.realm)
397        if tags is None:
398            try:
399                provider.remove_resource_tags(req, resource, comment)
400            except TypeError:
401                 # Handle old style tag providers gracefully.
402                provider.remove_resource_tags(req, resource)
403        else:
404            current_tags = set(provider.get_resource_tags(req, resource))
405            current_tags.difference_update(tags)
406            try:
407                provider.set_resource_tags(req, resource, current_tags,
408                                           comment)
409            except TypeError:
410                 # Handle old style tag providers gracefully.
411                provider.set_resource_tags(req, resource, current_tags)
412
413    def describe_tagged_resource(self, req, resource):
414        """Returns a short description of a taggable resource."""
415        provider = self._get_provider(resource.realm)
416        try:
417            return provider.describe_tagged_resource(req, resource)
418        except (AttributeError, NotImplementedError):
419            # Fallback to resource provider method.
420            self.env.log.info('ITagProvider %r does not implement '
421                              'describe_tagged_resource()' % provider)
422            return get_resource_description(self.env, resource, 'summary')
423
424    # IPermissionRequestor method
425    def get_permission_actions(self):
426        action = ['TAGS_VIEW', 'TAGS_MODIFY']
427        actions = [action[0], (action[1], [action[0]]),
428                   ('TAGS_ADMIN', action)]
429        return actions
430
431    # IResourceManager methods
432
433    def get_resource_realms(self):
434        yield 'tag'
435
436    def get_resource_url(self, resource, href, form_realms=None, **kwargs):
437        if self.wiki_page_link:
438            page = WikiPage(self.env, self.wiki_page_prefix + resource.id)
439            if page.exists:
440                return get_resource_url(self.env, page.resource, href,
441                                        **kwargs)
442        if form_realms:
443            return href.tags(form_realms, q=to_unicode(resource.id), **kwargs)
444        return href.tags(to_unicode(resource.id), form_realms, **kwargs)
445
446    def get_resource_description(self, resource, format='default',
447                                 context=None, **kwargs):
448        if self.wiki_page_link:
449            page = WikiPage(self.env, self.wiki_page_prefix + resource.id)
450            if page.exists:
451                return get_resource_description(self.env, page.resource,
452                                                format, **kwargs)
453        rid = to_unicode(resource.id)
454        if format in ('compact', 'default'):
455            return rid
456        else:
457            return u'tag:%s' % rid
458
459    # Internal methods
460
461    def _populate_provider_map(self):
462        if self._realm_provider_map is None:
463            # Only use the map once it is fully initialized.
464            map = dict((provider.get_taggable_realm(), provider)
465                       for provider in self.tag_providers)
466            self._realm_provider_map = map
467
468    def _get_provider(self, realm):
469        try:
470            return self._realm_provider_map[realm]
471        except KeyError:
472            raise InvalidTagRealm(_("Tags are not supported on the '%s' realm")
473                                  % realm)
474
475
476class RequestsProxy(object):
477
478    def __init__(self):
479        self.current = threading.local()
480
481    def get(self):
482        try:
483            return self.current.req
484        except:
485            return None
486
487    def set(self, req):
488        self.current.req = req
489
490    def reset(self):
491        self.current.req = None
492
493
494requests = RequestsProxy()
Note: See TracBrowser for help on using the repository browser.