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
RevLine 
[2954]1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006 Alec Thomas <alec@swapoff.org>
[14153]4# Copyright (C) 2011-2014 Steffen Hoffmann <hoff.st@web.de>
[13730]5# Copyright (C) 2014 Jun Omae <jun66j5@gmail.com>
[14153]6# Copyright (C) 2014 Ryan J Ollos <ryan.j.ollos@gmail.com>
[2954]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
[17883]12import collections
13import pkg_resources
[2953]14import re
[17883]15import threading
[12078]16
[13848]17from trac.config import BoolOption, ListOption, Option
[14153]18from trac.core import Component, ExtensionPoint, Interface, TracError
19from trac.core import implements
[13851]20from trac.perm import IPermissionPolicy, IPermissionRequestor
21from trac.perm import PermissionError, PermissionSystem
[14153]22from trac.resource import IResourceManager, get_resource_url
23from trac.resource import get_resource_description
[13862]24from trac.util import get_reporter_id
[12078]25from trac.util.text import to_unicode
[14951]26from trac.util.translation import domain_functions
[12078]27from trac.wiki.model import WikiPage
[2953]28
[9316]29# Import translation functions.
[14951]30add_domain, _, N_, gettext, ngettext, tag_, tagn_ = \
31    domain_functions('tractags', ('add_domain', '_', 'N_', 'gettext',
32                                  'ngettext', 'tag_', 'tagn_'))
33dgettext = None
[2953]34
[13392]35from tractags.model import resource_tags, tag_frequency, tag_resource
36from tractags.model import tagged_resources
[12078]37# Now call module importing i18n methods from here.
[9316]38from tractags.query import *
39
[14154]40REALM_RE = re.compile('realm:(\w+)', re.U | re.I)
[9316]41
[14154]42
[2953]43class InvalidTagRealm(TracError):
44    pass
45
46
47class ITagProvider(Interface):
[10638]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    """
[2953]53    def get_taggable_realm():
54        """Return the realm this provider supports tags on."""
55
[12390]56    def get_tagged_resources(req, tags=None, filter=None):
[2953]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.
[12390]61        :param filter: If provided, skip matching resources.
[2953]62
63        :rtype: Sequence of (resource, tags) tuples.
64        """
65
[13427]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
[13721]75    def get_resource_tags(req, resource, when=None):
[2953]76        """Get tags for a Resource object."""
77
[13851]78    def resource_tags(resource):
79        """Get tags for a Resource object skipping permission checks."""
80
[13724]81    def set_resource_tags(req, resource, tags, comment=u'', when=None):
[2953]82        """Set tags for a resource."""
83
[13429]84    def reparent_resource_tags(req, resource, old_id, comment=u''):
[10632]85        """Move tags, typically when renaming an existing resource."""
86
[10638]87    def remove_resource_tags(req, resource, comment=u''):
[2953]88        """Remove all tags from a resource."""
89
[3882]90    def describe_tagged_resource(req, resource):
[3880]91        """Return a one line description of the tagged resource."""
[2953]92
[3880]93
[2953]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()`.
[2954]99
100    See tractags.wiki.WikiTagProvider for an example.
[2953]101    """
[2954]102
103    implements(ITagProvider)
104
[2953]105    abstract = True
106
[2954]107    # Resource realm this provider manages tags for. Set this.
[2953]108    realm = None
109
[13428]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
[2953]117    # Public methods
[12078]118
[10789]119    def check_permission(self, perm, action):
[2953]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'}
[10789]126        return map[action] in perm('tag')
[2953]127
128    # ITagProvider methods
[12078]129
[2953]130    def get_taggable_realm(self):
131        return self.realm
132
[13461]133    def get_tagged_resources(self, req, tags=None, filter=None):
[2953]134        if not self.check_permission(req.perm, 'view'):
135            return
[12078]136        return tagged_resources(self.env, self.check_permission, req.perm,
[12390]137                                self.realm, tags, filter)
[2953]138
[13427]139    def get_all_tags(self, req, filter=None):
[17883]140        all_tags = collections.Counter()
[13427]141        for tag, count in tag_frequency(self.env, self.realm, filter):
142            all_tags[tag] = count
143        return all_tags
144
[13721]145    def get_resource_tags(self, req, resource, when=None):
[2953]146        assert resource.realm == self.realm
147        if not self.check_permission(req.perm(resource), 'view'):
148            return
[13721]149        return resource_tags(self.env, resource, when=when)
[2953]150
[13851]151    def resource_tags(self, resource):
152        assert resource.realm == self.realm
153        return resource_tags(self.env, resource)
154
[13724]155    def set_resource_tags(self, req, resource, tags, comment=u'', when=None):
[2953]156        assert resource.realm == self.realm
157        if not self.check_permission(req.perm(resource), 'modify'):
158            raise PermissionError(resource=resource, env=self.env)
[13862]159        tag_resource(self.env, resource, author=self._get_author(req),
160                     tags=tags, log=self.revisable, when=when)
[2953]161
[13429]162    def reparent_resource_tags(self, req, resource, old_id, comment=u''):
[12078]163        assert resource.realm == self.realm
164        if not self.check_permission(req.perm(resource), 'modify'):
165            raise PermissionError(resource=resource, env=self.env)
[13862]166        tag_resource(self.env, resource, old_id, self._get_author(req),
[13429]167                     log=self.revisable)
[10632]168
[10638]169    def remove_resource_tags(self, req, resource, comment=u''):
[2953]170        assert resource.realm == self.realm
171        if not self.check_permission(req.perm(resource), 'modify'):
172            raise PermissionError(resource=resource, env=self.env)
[13862]173        tag_resource(self.env, resource, author=self._get_author(req),
[13428]174                     log=self.revisable)
[2953]175
[3882]176    def describe_tagged_resource(self, req, resource):
[14156]177        raise NotImplementedError
[2953]178
[13862]179    def _get_author(self, req):
[14144]180        return get_reporter_id(req, 'author')
[3880]181
[13862]182
[13851]183class TagPolicy(Component):
[13865]184    """[extra] Security policy based on tags."""
[13851]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
[2953]214class TagSystem(Component):
[13865]215    """[main] Tagging system for Trac.
[2953]216
[13865]217    Associating resources with tags is easy, faceted content classification.
218
219    Available components are marked according to their relevance as follows:
[16093]220
[13865]221     `[main]`:: provide core with a generic tagging engine as well as support
[13867]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
[16093]225
[13865]226    Make sure to understand their purpose before deactivating `[main]` or
227    activating `[extra]` components.
228    """
229
[2953]230    implements(IPermissionRequestor, IResourceManager)
231
232    tag_providers = ExtensionPoint(ITagProvider)
233
[13428]234    revisable = ListOption('tags', 'revisable_realms', 'wiki',
235        doc="Comma-separated list of realms requiring tag change history.")
[12078]236    wiki_page_link = BoolOption('tags', 'wiki_page_link', True,
[10621]237        doc="Link a tag to the wiki page with same name, if it exists.")
[13848]238    wiki_page_prefix = Option('tags', 'wiki_page_prefix', '',
239        doc="Prefix for tag wiki page names.")
[10621]240
[2953]241    # Internal variables
242    _realm_provider_map = None
243
[9316]244    def __init__(self):
[12078]245        # Bind the 'tractags' catalog to the specified locale directory.
[17883]246        locale_dir = pkg_resources.resource_filename(__name__, 'locale')
[9316]247        add_domain(self.env.path, locale_dir)
248
[13393]249        self._populate_provider_map()
250
[12078]251    # Public methods
[9316]252
[2968]253    def query(self, req, query='', attribute_handlers=None):
[14153]254        """Returns a sequence of (resource, tags) tuples matching a query.
[2953]255
256        Query syntax is described in tractags.query.
[2968]257
258        :param attribute_handlers: Register additional query attribute
259                                   handlers. See Query documentation for more
260                                   information.
[2953]261        """
[2968]262        def realm_handler(_, node, context):
263            return query.match(node, [context.realm])
[2953]264
[2968]265        all_attribute_handlers = {
[2953]266            'realm': realm_handler,
[12078]267        }
[2968]268        all_attribute_handlers.update(attribute_handlers or {})
269        query = Query(query, attribute_handlers=all_attribute_handlers)
[10629]270        providers = set()
[14154]271        for m in REALM_RE.finditer(query.as_string()):
[7381]272            realm = m.group(1)
[10629]273            providers.add(self._get_provider(realm))
[7381]274        if not providers:
275            providers = self.tag_providers
[2953]276
277        query_tags = set(query.terms())
[7381]278        for provider in providers:
[12390]279            self.env.log.debug('Querying ' + repr(provider))
[12078]280            for resource, tags in provider.get_tagged_resources(req,
[12390]281                                                          query_tags) or []:
[2968]282                if query(tags, context=resource):
[2953]283                    yield resource, tags
284
[14153]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
[13427]297    def get_all_tags(self, req, realms=[]):
[14153]298        """Get all tags for all supported realms or only for specified ones.
[3882]299
[13427]300        Returns a Counter object (special dict) with tag name as key and tag
301        frequency as value.
[3882]302        """
[17883]303        all_tags = collections.Counter()
[14153]304        all_realms = self.get_taggable_realms(req.perm)
[13392]305        if not realms or set(realms) == all_realms:
306            realms = all_realms
[13427]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.
[13799]313                    try:
314                        for resource, tags in \
315                            provider.get_tagged_resources(req):
316                                all_tags.update(tags)
317                    except TypeError:
[13802]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)
[3882]323        return all_tags
324
[13721]325    def get_tags(self, req, resource, when=None):
[2953]326        """Get tags for resource."""
[13851]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))
[2953]332        return set(self._get_provider(resource.realm) \
[13721]333                   .get_resource_tags(req, resource, when=when))
[2953]334
[13724]335    def set_tags(self, req, resource, tags, comment=u'', when=None):
[2953]336        """Set tags on a resource.
337
338        Existing tags are replaced.
339        """
[10638]340        try:
341            return self._get_provider(resource.realm) \
[13724]342                   .set_resource_tags(req, resource, set(tags), comment, when)
[10638]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))
[2953]347
[10638]348    def add_tags(self, req, resource, tags, comment=u''):
[2953]349        """Add to existing tags on a resource."""
350        tags = set(tags)
351        tags.update(self.get_tags(req, resource))
[10638]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)
[2953]357
[13429]358    def reparent_tags(self, req, resource, old_name, comment=u''):
[10632]359        """Move tags, typically when renaming an existing resource.
360
361        Tags can't be moved between different tag realms with intention.
362        """
[13429]363        provider = self._get_provider(resource.realm)
364        provider.reparent_resource_tags(req, resource, old_name, comment)
[10632]365
[10639]366    def replace_tag(self, req, old_tags, new_tag=None, comment=u'',
[13304]367                    allow_delete=False, filter=[]):
[10639]368        """Replace one or more tags in all resources it exists/they exist in.
369
[13304]370        Tagged resources may be filtered by realm and tag deletion is
371        optionally allowed for convenience as well.
[10639]372        """
373        # Provide list regardless of attribute type.
[13304]374        for provider in [p for p in self.tag_providers
375                         if not filter or p.get_taggable_realm() in filter]:
[10639]376            for resource, tags in \
[13304]377                    provider.get_tagged_resources(req, old_tags):
[10639]378                old_tags = set(old_tags)
[13167]379                if old_tags.issuperset(tags) and not new_tag:
[10639]380                    if allow_delete:
381                        self.delete_tags(req, resource, None, comment)
382                else:
[13167]383                    s_tags = set(tags)
[13304]384                    eff_tags = s_tags - old_tags
[10639]385                    if new_tag:
386                        eff_tags.add(new_tag)
387                    # Prevent to touch resources without effective change.
[13167]388                    if eff_tags != s_tags and (allow_delete or new_tag):
[10639]389                        self.set_tags(req, resource, eff_tags, comment)
390
[10638]391    def delete_tags(self, req, resource, tags=None, comment=u''):
[2968]392        """Delete tags on a resource.
[2953]393
[2968]394        If tags is None, remove all tags on the resource.
395        """
396        provider = self._get_provider(resource.realm)
397        if tags is None:
[10638]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)
[2968]403        else:
[3878]404            current_tags = set(provider.get_resource_tags(req, resource))
405            current_tags.difference_update(tags)
[10638]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)
[2968]412
[3882]413    def describe_tagged_resource(self, req, resource):
[14156]414        """Returns a short description of a taggable resource."""
[3880]415        provider = self._get_provider(resource.realm)
[14156]416        try:
[3882]417            return provider.describe_tagged_resource(req, resource)
[14156]418        except (AttributeError, NotImplementedError):
[14159]419            # Fallback to resource provider method.
[14156]420            self.env.log.info('ITagProvider %r does not implement '
421                              'describe_tagged_resource()' % provider)
[14159]422            return get_resource_description(self.env, resource, 'summary')
423
[12078]424    # IPermissionRequestor method
[2953]425    def get_permission_actions(self):
[10627]426        action = ['TAGS_VIEW', 'TAGS_MODIFY']
427        actions = [action[0], (action[1], [action[0]]),
428                   ('TAGS_ADMIN', action)]
429        return actions
[2953]430
431    # IResourceManager methods
[12078]432
[2953]433    def get_resource_realms(self):
434        yield 'tag'
435
[11934]436    def get_resource_url(self, resource, href, form_realms=None, **kwargs):
[10621]437        if self.wiki_page_link:
[13848]438            page = WikiPage(self.env, self.wiki_page_prefix + resource.id)
[10621]439            if page.exists:
440                return get_resource_url(self.env, page.resource, href,
441                                        **kwargs)
[14148]442        if form_realms:
[18143]443            return href.tags(form_realms, q=to_unicode(resource.id), **kwargs)
444        return href.tags(to_unicode(resource.id), form_realms, **kwargs)
[2953]445
[10621]446    def get_resource_description(self, resource, format='default',
447                                 context=None, **kwargs):
448        if self.wiki_page_link:
[13848]449            page = WikiPage(self.env, self.wiki_page_prefix + resource.id)
[10621]450            if page.exists:
451                return get_resource_description(self.env, page.resource,
452                                                format, **kwargs)
[2953]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
[12078]460
[2953]461    def _populate_provider_map(self):
462        if self._realm_provider_map is None:
[13278]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
[2953]467
468    def _get_provider(self, realm):
469        try:
470            return self._realm_provider_map[realm]
471        except KeyError:
[12078]472            raise InvalidTagRealm(_("Tags are not supported on the '%s' realm")
473                                  % realm)
[13724]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.