def render_admin_panel(self, req, cat, page, version): req.perm.require("TAGS_ADMIN") data = {} tag_system = TagSystem(self.env) if req.method == "POST": # Replace Tag allow_delete = req.args.get("allow_delete") new_tag = req.args.get("tag_new_name").strip() new_tag = not new_tag == u"" and new_tag or None if not (allow_delete or new_tag): data["error"] = _( """Selected current tag(s) and either new tag or delete approval are required""" ) else: comment = req.args.get("comment", u"") old_tags = req.args.get("tag_name") if old_tags: # Provide list regardless of single or multiple selection. old_tags = isinstance(old_tags, list) and old_tags or [old_tags] tag_system.replace_tag(req, old_tags, new_tag, comment, allow_delete) data["selected"] = new_tag all_tags = sorted(tag_system.get_all_tags(req, "-dummy")) data["tags"] = all_tags try: Chrome(self.env).add_textarea_grips(req) except AttributeError: # Element modifiers unavailable before Trac 0.12, skip gracefully. pass return "admin_tag_change.html", data
def _format_tagged(self, formatter, ns, target, label, fullmatch=None): """Tag and tag query expression link formatter.""" def unquote(text): """Strip all matching pairs of outer quotes from string.""" while re.match(WikiParser.QUOTED_STRING, text): # Remove outer whitespace after stripped quotation too. text = text[1:-1].strip() return text label = label and unquote(label.strip()) or '' target = unquote(target.strip()) tag_res = Resource('tag', target) if 'TAGS_VIEW' in formatter.perm(tag_res): context = formatter.context href = get_resource_url(self.env, tag_res, context.href) tag_sys = TagSystem(self.env) # Tag exists or tags query yields at least one match. if target in tag_sys.get_all_tags(formatter.req) or \ [(res, tags) for res, tags in tag_sys.query(formatter.req, target)]: if label: return tag.a(label, href=href) return render_resource_link(self.env, context, tag_res) else: return tag.a(label+'?', href=href, class_='missing tags', rel='nofollow') else: return tag.span(label, class_='forbidden tags', title=_("no permission to view tags"))
def render_admin_panel(self, req, cat, page, version): req.perm.require('TAGS_ADMIN') data = {} tag_system = TagSystem(self.env) if req.method == 'POST': # Replace Tag allow_delete = req.args.get('allow_delete') new_tag = req.args.get('tag_new_name').strip() new_tag = not new_tag == u'' and new_tag or None if not (allow_delete or new_tag): data['error'] = _("""Selected current tag(s) and either new tag or delete approval are required""") else: comment = req.args.get('comment', u'') old_tags = req.args.get('tag_name') if old_tags: # Provide list regardless of single or multiple selection. old_tags = isinstance(old_tags, list) and old_tags or \ [old_tags] tag_system.replace_tag(req, old_tags, new_tag, comment, allow_delete) data['selected'] = new_tag all_tags = sorted(tag_system.get_all_tags(req, '-dummy')) data['tags'] = all_tags try: Chrome(self.env).add_textarea_grips(req) except AttributeError: # Element modifiers unavailable before Trac 0.12, skip gracefully. pass return 'admin_tag_change.html', data
def render_admin_panel(self, req, cat, page, version): req.perm.require('TAGS_ADMIN') realms = [p.get_taggable_realm() for p in self.tag_providers if (not hasattr(p, 'check_permission') or \ p.check_permission(req.perm, 'view'))] # Check request for enabled filters, or use default. if [r for r in realms if r in req.args] == []: for realm in realms: req.args[realm] = 'on' checked_realms = [r for r in realms if r in req.args] data = dict(checked_realms=checked_realms, tag_realms=list( dict(name=realm, checked=realm in checked_realms) for realm in realms)) tag_system = TagSystem(self.env) if req.method == 'POST': # Replace Tag allow_delete = req.args.get('allow_delete') new_tag = req.args.get('tag_new_name').strip() new_tag = not new_tag == u'' and new_tag or None if not (allow_delete or new_tag): data['error'] = _("Selected current tag(s) and either " "new tag or delete approval are required") else: comment = req.args.get('comment', u'') old_tags = req.args.get('tag_name') if old_tags: # Provide list regardless of single or multiple selection. old_tags = isinstance(old_tags, list) and old_tags or \ [old_tags] tag_system.replace_tag(req, old_tags, new_tag, comment, allow_delete, filter=checked_realms) data['selected'] = new_tag query = ' or '.join(['realm:%s' % r for r in checked_realms]) all_tags = sorted(tag_system.get_all_tags(req, query)) data['tags'] = all_tags try: Chrome(self.env).add_textarea_grips(req) except AttributeError: # Element modifiers unavailable before Trac 0.12, skip gracefully. pass return 'admin_tag_change.html', data
def render_admin_panel(self, req, cat, page, version): req.perm.require('TAGS_ADMIN') tag_system = TagSystem(self.env) all_realms = tag_system.get_taggable_realms(req.perm) # Check request for enabled filters, or use default. if not [r for r in all_realms if r in req.args]: for realm in all_realms: req.args[realm] = 'on' checked_realms = [r for r in all_realms if r in req.args] data = dict(checked_realms=checked_realms, tag_realms=list( dict(name=realm, checked=realm in checked_realms) for realm in all_realms)) if req.method == 'POST': # Replace Tag allow_delete = req.args.get('allow_delete') new_tag = req.args.get('tag_new_name').strip() new_tag = not new_tag == u'' and new_tag or None if not (allow_delete or new_tag): add_warning( req, _("Selected current tag(s) and either " "new tag or delete approval are required")) else: comment = req.args.get('comment', u'') old_tags = req.args.getlist('tag_name') if old_tags: tag_system.replace_tag(req, old_tags, new_tag, comment, allow_delete, filter=checked_realms) data['selected'] = new_tag req.redirect(req.href.admin('tags', 'replace')) query = ' or '.join('realm:%s' % r for r in checked_realms) all_tags = sorted(tag_system.get_all_tags(req, query)) data['tags'] = all_tags chrome = Chrome(self.env) chrome.add_textarea_grips(req) if hasattr(chrome, 'jenv'): return 'admin_tag_change.html', data, None else: return 'admin_tag_change.html', data
def render_admin_panel(self, req, cat, page, version): req.perm.require('TAGS_ADMIN') tag_system = TagSystem(self.env) all_realms = tag_system.get_taggable_realms(req.perm) # Check request for enabled filters, or use default. if not [r for r in all_realms if r in req.args]: for realm in all_realms: req.args[realm] = 'on' checked_realms = [r for r in all_realms if r in req.args] data = dict(checked_realms=checked_realms, tag_realms=list(dict(name=realm, checked=realm in checked_realms) for realm in all_realms)) if req.method == 'POST': # Replace Tag allow_delete = req.args.get('allow_delete') new_tag = req.args.get('tag_new_name').strip() new_tag = not new_tag == u'' and new_tag or None if not (allow_delete or new_tag): data['error'] = _("Selected current tag(s) and either " "new tag or delete approval are required") else: comment = req.args.get('comment', u'') old_tags = req.args.get('tag_name') if old_tags: # Provide list regardless of single or multiple selection. old_tags = isinstance(old_tags, list) and old_tags or \ [old_tags] tag_system.replace_tag(req, old_tags, new_tag, comment, allow_delete, filter=checked_realms) data['selected'] = new_tag query = ' or '.join(['realm:%s' % r for r in checked_realms]) all_tags = sorted(tag_system.get_all_tags(req, query)) data['tags'] = all_tags try: Chrome(self.env).add_textarea_grips(req) except AttributeError: # Element modifiers unavailable before Trac 0.12, skip gracefully. pass return 'admin_tag_change.html', data
class TicketTagProviderTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True, enable=['trac.*', 'tractags.*']) self.env.path = tempfile.mkdtemp() self.perms = PermissionSystem(self.env) self.db = self.env.get_db_cnx() setup = TagSetup(self.env) # Current tractags schema is setup with enabled component anyway. # Revert these changes for getting default permissions inserted. self._revert_tractags_schema_init() setup.upgrade_environment(self.db) self.provider = TicketTagProvider(self.env) self.realm = 'ticket' self.tag_sys = TagSystem(self.env) self.tags = ['tag1', 'tag2'] cursor = self.db.cursor() # Populate tables with initial test data. self._create_ticket(self.tags) # Mock an anonymous request. self.anon_req = Mock() self.anon_req.perm = PermissionCache(self.env) self.req = Mock(authname='editor') self.req.authname = 'editor' self.req.perm = PermissionCache(self.env, username='******') def tearDown(self): self.db.close() # Really close db connections. self.env.shutdown() shutil.rmtree(self.env.path) # Helpers def _create_ticket(self, tags, **kwargs): ticket = Ticket(self.env) ticket['keywords'] = u' '.join(sorted(map(to_unicode, tags))) ticket['summary'] = 'summary' ticket['reporter'] = 'admin' for name, value in kwargs.iteritems(): ticket[name] = value ticket.insert() return ticket def _revert_tractags_schema_init(self): cursor = self.db.cursor() cursor.execute("DROP TABLE IF EXISTS tags") cursor.execute("DROP TABLE IF EXISTS tags_change") cursor.execute("DELETE FROM system WHERE name='tags_version'") cursor.execute("DELETE FROM permission WHERE action %s" % self.db.like(), ('TAGS_%',)) def _tags(self): tags = {} cursor = self.db.cursor() cursor.execute("SELECT name,tag FROM tags") for name, tag in cursor: if name in tags: tags[name].add(tag) else: tags[name] = set([tag]) return tags # Tests def test_get_tagged_resources(self): # No tags, no restrictions, all resources. self.assertEquals( [r for r in self.provider.get_tagged_resources(self.req, None)][0][1], set(self.tags)) # Force fine-grained perm-check check for all tags, not just the one # from query. self.provider.fast_permcheck = False self.assertEquals( [r for r in self.provider.get_tagged_resources(self.req, set(self.tags[:1]))][0][1], set(self.tags)) def test_get_tags(self): resource = Resource('ticket', 2) self.assertRaises(ResourceNotFound, self.provider.get_resource_tags, self.req, resource) self._create_ticket(self.tags) self.assertEquals( [tag for tag in self.provider.get_resource_tags(self.req, resource)], self.tags) #ignore_closed_tickets def test_set_tags(self): tags = ['tag3'] ticket = Ticket(self.env, 1) ticket['keywords'] = tags[0] # Tags get updated by TicketChangeListener method. ticket.save_changes(self.req.authname) self.assertEquals(self.tag_sys.get_all_tags(self.req).keys(), tags) def test_remove_tags(self): resource = Resource('ticket', 1) # Anonymous lacks required permissions. self.assertRaises(PermissionError, self.provider.remove_resource_tags, self.anon_req, resource) # Shouldn't raise an error with appropriate permission. self.provider.remove_resource_tags(self.req, resource, 'comment') ticket = Ticket(self.env, 1) self.assertEquals(ticket['keywords'], '') def test_describe_tagged_resource(self): resource = Resource('ticket', 1) self.assertEquals( self.provider.describe_tagged_resource(self.req, resource), 'defect: summary') def test_create_ticket_by_anonymous(self): ticket = self._create_ticket(self.tags, reporter='anonymous') tags = self.provider.get_resource_tags(self.req, ticket.resource) self.assertEquals(tags, set(self.tags)) def test_update_ticket_by_anonymous(self): ticket = self._create_ticket([]) tags = self.provider.get_resource_tags(self.req, ticket.resource) self.assertEquals(tags, set([])) ticket['keywords'] = ', '.join(self.tags) ticket.save_changes('anonymous', comment='Adding keywords') tags = self.provider.get_resource_tags(self.req, ticket.resource) self.assertEquals(tags, set(self.tags))
def expand_macro(self, formatter, name, content): env = self.env req = formatter.req args, kw = parse_args(content) # Use macro arguments (most likely wiki macro calls). realms = 'realm' in kw and kw['realm'].split('|') or [] tag_system = TagSystem(env) all_realms = [p.get_taggable_realm() for p in tag_system.tag_providers] self.all_realms = all_realms self.realms = realms if name == 'TagCloud': args.append(' or '.join(['realm:%s' % r for r in realms])) all_tags = tag_system.get_all_tags(req, ' '.join(args)) mincount = 'mincount' in kw and kw['mincount'] or None return self.render_cloud(req, all_tags, caseless_sort=self.caseless_sort, mincount=mincount) elif name == 'ListTagged': if _OBSOLETE_ARGS_RE.search(content): data = {'warning': 'obsolete_args'} else: data = {'warning': None} context = formatter.context # Use TagsQuery arguments (most likely wiki macro calls). cols = 'cols' in kw and kw['cols'] or self.default_cols format = 'format' in kw and kw['format'] or self.default_format query = args and args[0].strip() or None if query and not realms: # First read query arguments (most likely a web-UI call). for realm in all_realms: if re.search('(^|\W)realm:%s(\W|$)' % (realm), query): realms = realms and realms.append(realm) or [realm] if not realms: # Apply ListTagged defaults to macro call w/o realm. realms = list(set(all_realms) - set(self.exclude_realms)) if not realms: return '' else: self.query = query self.realms = realms query = '(%s) (%s)' % (query or '', ' or '.join( ['realm:%s' % (r) for r in realms])) env.log.debug('LISTTAGGED_QUERY: ' + query) query_result = tag_system.query(req, query) if not query_result: return '' def _link(resource): if resource.realm == 'tag': # Keep realm selection in tag links. return builder.a(resource.id, href=self.get_href(req, tag=resource)) elif resource.realm == 'ticket': # Return resource link including ticket status dependend # class to allow for common Trac ticket link style. ticket = Ticket(env, resource.id) return builder.a('#%s' % ticket.id, class_=ticket['status'], href=formatter.href.ticket(ticket.id), title=shorten_line(ticket['summary'])) return render_resource_link(env, context, resource, 'compact') if format == 'table': cols = [ col for col in cols.split('|') if col in self.supported_cols ] # Use available translations from Trac core. try: labels = TicketSystem(env).get_ticket_field_labels() labels['id'] = _('Id') except AttributeError: # Trac 0.11 neither has the attribute nor uses i18n. labels = {'id': 'Id', 'description': 'Description'} labels['realm'] = _('Realm') labels['tags'] = _('Tags') headers = [{'label': labels.get(col)} for col in cols] data.update({'cols': cols, 'headers': headers}) results = sorted(query_result, key=lambda r: \ embedded_numbers(to_unicode(r[0].id))) results = self._paginate(req, results) rows = [] for resource, tags in results: desc = tag_system.describe_tagged_resource(req, resource) tags = sorted(tags) if tags: rendered_tags = [ _link(Resource('tag', tag)) for tag in tags ] if 'oldlist' == format: resource_link = _link(resource) else: desc = desc or \ get_resource_description(env, resource, context=context) resource_link = builder.a(desc, href=get_resource_url( env, resource, context.href)) if 'table' == format: cells = [] for col in cols: if col == 'id': cells.append(_link(resource)) # Don't duplicate links to resource in both. elif col == 'description' and 'id' in cols: cells.append(desc) elif col == 'description': cells.append(resource_link) elif col == 'realm': cells.append(resource.realm) elif col == 'tags': cells.append( builder([(tag, ' ') for tag in rendered_tags])) rows.append({'cells': cells}) continue rows.append({ 'desc': desc, 'rendered_tags': None, 'resource_link': _link(resource) }) data.update({ 'format': format, 'paginator': results, 'results': rows, 'tags_url': req.href('tags') }) # Work around a bug in trac/templates/layout.html, that causes a # TypeError for the wiki macro call, if we use add_link() alone. add_stylesheet(req, 'common/css/search.css') return Chrome(env).render_template(req, 'listtagged_results.html', data, 'text/html', True)
def expand_macro(self, formatter, name, content, realms=[]): """Evaluate macro call and render results. Calls from web-UI come with pre-processed realm selection. """ env = self.env req = formatter.req tag_system = TagSystem(env) all_realms = tag_system.get_taggable_realms() if not all_realms: # Tag providers are required, no result without at least one. return '' args, kw = parse_args(content) query = args and args[0].strip() or None if not realms: # Check macro arguments for realms (typical wiki macro call). realms = 'realm' in kw and kw['realm'].split('|') or [] if query: # Add realms from query expression. realms.extend(query_realms(query, all_realms)) # Remove redundant realm selection for performance. if set(realms) == all_realms: query = re.sub('(^|\W)realm:\S+(\W|$)', ' ', query).strip() if name == 'TagCloud': # Set implicit 'all tagged realms' as default. if not realms: realms = all_realms if query: all_tags = Counter() # Require per resource query including view permission checks. for resource, tags in tag_system.query(req, query): all_tags.update(tags) else: # Allow faster per tag query, side steps permission checks. all_tags = tag_system.get_all_tags(req, realms=realms) mincount = 'mincount' in kw and kw['mincount'] or None return self.render_cloud(req, all_tags, caseless_sort=self.caseless_sort, mincount=mincount, realms=realms) elif name == 'ListTagged': if content and _OBSOLETE_ARGS_RE.search(content): data = {'warning': 'obsolete_args'} else: data = {'warning': None} context = formatter.context # Use TagsQuery arguments (most likely wiki macro calls). cols = 'cols' in kw and kw['cols'] or self.default_cols format = 'format' in kw and kw['format'] or self.default_format if not realms: # Apply ListTagged defaults to macro call w/o realm. realms = list(set(all_realms)-set(self.exclude_realms)) if not realms: return '' query = '(%s) (%s)' % (query or '', ' or '.join(['realm:%s' % (r) for r in realms])) query_result = tag_system.query(req, query) excludes = [exc.strip() for exc in kw.get('exclude', '' ).split(':') if exc.strip()] if excludes and query_result: filtered_result = [(resource, tags) for resource, tags in query_result if not any(fnmatchcase(resource.id, exc) for exc in excludes)] query_result = filtered_result if not query_result: return '' def _link(resource): if resource.realm == 'tag': # Keep realm selection in tag links. return builder.a(resource.id, href=self.get_href(req, realms, tag=resource)) elif resource.realm == 'ticket': # Return resource link including ticket status dependend # class to allow for common Trac ticket link style. ticket = Ticket(env, resource.id) return builder.a('#%s' % ticket.id, class_=ticket['status'], href=formatter.href.ticket(ticket.id), title=shorten_line(ticket['summary'])) return render_resource_link(env, context, resource, 'compact') if format == 'table': cols = [col for col in cols.split('|') if col in self.supported_cols] # Use available translations from Trac core. try: labels = TicketSystem(env).get_ticket_field_labels() labels['id'] = _('Id') except AttributeError: # Trac 0.11 neither has the attribute nor uses i18n. labels = {'id': 'Id', 'description': 'Description'} labels['realm'] = _('Realm') labels['tags'] = _('Tags') headers = [{'label': labels.get(col)} for col in cols] data.update({'cols': cols, 'headers': headers}) results = sorted(query_result, key=lambda r: \ embedded_numbers(to_unicode(r[0].id))) results = self._paginate(req, results, realms) rows = [] for resource, tags in results: desc = tag_system.describe_tagged_resource(req, resource) tags = sorted(tags) wiki_desc = format_to_oneliner(env, context, desc) if tags: rendered_tags = [_link(Resource('tag', tag)) for tag in tags] if 'oldlist' == format: resource_link = _link(resource) else: resource_link = builder.a(wiki_desc, href=get_resource_url( env, resource, context.href)) if 'table' == format: cells = [] for col in cols: if col == 'id': cells.append(_link(resource)) # Don't duplicate links to resource in both. elif col == 'description' and 'id' in cols: cells.append(wiki_desc) elif col == 'description': cells.append(resource_link) elif col == 'realm': cells.append(resource.realm) elif col == 'tags': cells.append( builder([(tag, ' ') for tag in rendered_tags])) rows.append({'cells': cells}) continue rows.append({'desc': wiki_desc, 'rendered_tags': None, 'resource_link': _link(resource)}) data.update({'format': format, 'paginator': results, 'results': rows, 'tags_url': req.href('tags')}) # Work around a bug in trac/templates/layout.html, that causes a # TypeError for the wiki macro call, if we use add_link() alone. add_stylesheet(req, 'common/css/search.css') return Chrome(env).render_template( req, 'listtagged_results.html', data, 'text/html', True)
class WikiTagProviderTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True, enable=['trac.*', 'tractags.*']) self.env.path = tempfile.mkdtemp() setup = TagSetup(self.env) # Current tractags schema is partially setup with enabled component. # Revert these changes for getting a clean setup. self._revert_tractags_schema_init() setup.upgrade_environment() self.perms = PermissionSystem(self.env) self.tag_s = TagSystem(self.env) self.tag_wp = WikiTagProvider(self.env) # Populate table with initial test data. self.env.db_transaction(""" INSERT INTO tags (tagspace, name, tag) VALUES ('wiki', 'WikiStart', 'tag1') """) self.req = Mock(authname='editor') # Mock an anonymous request. self.req.perm = PermissionCache(self.env) self.realm = 'wiki' self.tags = ['tag1'] def tearDown(self): self.env.shutdown() shutil.rmtree(self.env.path) # Helpers def _revert_tractags_schema_init(self): _revert_tractags_schema_init(self.env) # Tests def test_get_tags(self): resource = Resource('wiki', 'WikiStart') self.assertEquals([tag for tag in self.tag_wp.get_resource_tags(self.req, resource)], self.tags) def test_exclude_template_tags(self): # Populate table with more test data. self.env.db_transaction(""" INSERT INTO tags (tagspace, name, tag) VALUES ('wiki', 'PageTemplates/Template', 'tag2') """) tags = ['tag1', 'tag2'] self.assertEquals(self.tag_s.get_all_tags(self.req).keys(), self.tags) self.env.config.set('tags', 'query_exclude_wiki_templates', False) self.assertEquals(self.tag_s.get_all_tags(self.req).keys(), tags) def test_set_tags_no_perms(self): resource = Resource('wiki', 'TaggedPage') self.assertRaises(PermissionError, self.tag_wp.set_resource_tags, self.req, resource, self.tags) def test_set_tags(self): resource = Resource('wiki', 'TaggedPage') self.req.perm = PermissionCache(self.env, username='******') # Shouldn't raise an error with appropriate permission. self.tag_wp.set_resource_tags(self.req, resource, self.tags) self.tag_wp.set_resource_tags(self.req, resource, ['tag2']) # Check change records. rows = self.env.db_query(""" SELECT author,oldtags,newtags FROM tags_change WHERE tagspace=%s AND name=%s ORDER by time DESC """, ('wiki', 'TaggedPage')) self.assertEqual(rows[0], ('editor', 'tag1', 'tag2')) self.assertEqual(rows[1], ('editor', '', 'tag1'))
class TagWikiSyntaxProvider(Component): """[opt] Provides tag:<expr> links. This extends TracLinks via WikiFormatting to point at tag queries or at specific tags. """ implements(IWikiSyntaxProvider) def __init__(self): self.tag_system = TagSystem(self.env) # IWikiSyntaxProvider methods def get_wiki_syntax(self): """Additional syntax for quoted tags or tag expression.""" tag_expr = (r"(%s)" % (WikiParser.QUOTED_STRING)) # Simple (tag|tagged):link syntax yield ( r'''(?P<qualifier>tag(?:ged)?):(?P<tag_expr>%s)''' % tag_expr, lambda f, ns, match: self._format_tagged( f, match.group('qualifier'), match.group('tag_expr'), '%s:%s' % (match.group('qualifier'), match.group('tag_expr')))) # [(tag|tagged):link with label] yield ( r'''\[tag(?:ged)?:''' r'''(?P<ltag_expr>%s)\s*(?P<tag_title>[^\]]+)?\]''' % tag_expr, lambda f, ns, match: self._format_tagged( f, 'tag', match.group('ltag_expr'), match.group('tag_title'))) def get_link_resolvers(self): return [('tag', self._format_tagged), ('tagged', self._format_tagged)] def _format_tagged(self, formatter, ns, target, label, fullmatch=None): """Tag and tag query expression link formatter.""" def unquote(text): """Strip all matching pairs of outer quotes from string.""" while re.match(WikiParser.QUOTED_STRING, text): # Remove outer whitespace after stripped quotation too. text = text[1:-1].strip() return text label = label and unquote(label.strip()) or '' target = unquote(target.strip()) query = target # Pop realms from query expression. all_realms = self.tag_system.get_taggable_realms(formatter.perm) realms = query_realms(target, all_realms) if realms: kwargs = dict((realm, 'on') for realm in realms) target = re.sub('(^|\W)realm:\S+(\W|$)', ' ', target).strip() else: kwargs = {} tag_res = Resource('tag', target) if 'TAGS_VIEW' not in formatter.perm(tag_res): return tag.span(label, class_='forbidden tags', title=_("no permission to view tags")) context = formatter.context href = self.tag_system.get_resource_url(tag_res, context.href, kwargs) if all_realms and (target in self.tag_system.get_all_tags( formatter.req) or not iter_is_empty( self.tag_system.query(formatter.req, query))): # At least one tag provider is available and tag exists or # tags query yields at least one match. if label: return tag.a(label, href=href) return render_resource_link(self.env, context, tag_res) return tag.a(label + '?', href=href, class_='missing tags', rel='nofollow')
class WikiTagProviderTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True, path=tempfile.mkdtemp(), enable=['trac.*', 'tractags.*']) setup = TagSetup(self.env) # Current tractags schema is partially setup with enabled component. # Revert these changes for getting a clean setup. self._revert_tractags_schema_init() setup.upgrade_environment() self.perms = PermissionSystem(self.env) self.tag_s = TagSystem(self.env) self.tag_wp = WikiTagProvider(self.env) # Populate table with initial test data. self.env.db_transaction(""" INSERT INTO tags (tagspace, name, tag) VALUES ('wiki', 'WikiStart', 'tag1') """) self.realm = 'wiki' self.tags = ['tag1'] def tearDown(self): self.env.shutdown() shutil.rmtree(self.env.path) # Helpers def _revert_tractags_schema_init(self): _revert_tractags_schema_init(self.env) # Tests def test_get_tags(self): resource = Resource('wiki', 'WikiStart') req = MockRequest(self.env, authname='editor') self.assertEquals([tag for tag in self.tag_wp.get_resource_tags(req, resource)], self.tags) def test_exclude_template_tags(self): # Populate table with more test data. req = MockRequest(self.env, authname='editor') self.env.db_transaction(""" INSERT INTO tags (tagspace, name, tag) VALUES ('wiki', 'PageTemplates/Template', 'tag2') """) tags = ['tag1', 'tag2'] self.assertEquals(self.tag_s.get_all_tags(req).keys(), self.tags) self.env.config.set('tags', 'query_exclude_wiki_templates', False) self.assertEquals(self.tag_s.get_all_tags(req).keys(), tags) def test_set_tags_no_perms(self): resource = Resource('wiki', 'TaggedPage') req = MockRequest(self.env, authname='anonymous') self.assertRaises(PermissionError, self.tag_wp.set_resource_tags, req, resource, self.tags) def test_set_tags(self): resource = Resource('wiki', 'TaggedPage') req = MockRequest(self.env, authname='editor') # Shouldn't raise an error with appropriate permission. self.tag_wp.set_resource_tags(req, resource, self.tags) self.tag_wp.set_resource_tags(req, resource, ['tag2']) # Check change records. rows = self.env.db_query(""" SELECT author,oldtags,newtags FROM tags_change WHERE tagspace=%s AND name=%s ORDER by time DESC """, ('wiki', 'TaggedPage')) self.assertEqual(rows[0], ('editor', 'tag1', 'tag2')) self.assertEqual(rows[1], ('editor', '', 'tag1'))
class WikiTagProviderTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True, enable=['trac.*', 'tractags.*']) self.env.path = tempfile.mkdtemp() self.perms = PermissionSystem(self.env) self.tag_s = TagSystem(self.env) self.tag_wp = WikiTagProvider(self.env) self.db = self.env.get_db_cnx() setup = TagSetup(self.env) # Current tractags schema is partially setup with enabled component. # Revert these changes for getting a clean setup. self._revert_tractags_schema_init() setup.upgrade_environment(self.db) cursor = self.db.cursor() # Populate table with initial test data. cursor.execute(""" INSERT INTO tags (tagspace, name, tag) VALUES ('wiki', 'WikiStart', 'tag1') """) self.req = Mock() # Mock an anonymous request. self.req.perm = PermissionCache(self.env) self.realm = 'wiki' self.tags = ['tag1'] def tearDown(self): self.db.close() # Really close db connections. self.env.shutdown() shutil.rmtree(self.env.path) # Helpers def _revert_tractags_schema_init(self): cursor = self.db.cursor() cursor.execute("DROP TABLE IF EXISTS tags") cursor.execute("DELETE FROM system WHERE name='tags_version'") cursor.execute( "DELETE FROM permission WHERE action %s" % self.db.like(), ('TAGS_%', )) # Tests def test_get_tags(self): resource = Resource('wiki', 'WikiStart') self.assertEquals( [tag for tag in self.tag_wp.get_resource_tags(self.req, resource)], self.tags) def test_exclude_template_tags(self): cursor = self.db.cursor() # Populate table with more test data. cursor.execute(""" INSERT INTO tags (tagspace, name, tag) VALUES ('wiki', 'PageTemplates/Template', 'tag2') """) tags = ['tag1', 'tag2'] # Previous workaround for regression in query parser still works, # but the query without argument works again as well. self.assertEquals( self.tag_s.get_all_tags(self.req, '-invalid').keys(), self.tags) self.assertEquals( self.tag_s.get_all_tags(self.req, '').keys(), self.tags) self.env.config.set('tags', 'query_exclude_wiki_templates', False) self.assertEquals( self.tag_s.get_all_tags(self.req, '-invalid').keys(), tags) self.assertEquals(self.tag_s.get_all_tags(self.req, '').keys(), tags) def test_set_tags_no_perms(self): resource = Resource('wiki', 'TaggedPage') self.assertRaises(PermissionError, self.tag_wp.set_resource_tags, self.req, resource, self.tags) def test_set_tags(self): resource = Resource('wiki', 'TaggedPage') self.req.perm = PermissionCache(self.env, username='******') # Shouldn't raise an error with appropriate permission. self.tag_wp.set_resource_tags(self.req, resource, self.tags)
def expand_macro(self, formatter, name, content, realms=[]): """Evaluate macro call and render results. Calls from web-UI come with pre-processed realm selection. """ env = self.env req = formatter.req tag_system = TagSystem(env) all_realms = tag_system.get_taggable_realms() if not all_realms: # Tag providers are required, no result without at least one. return '' args, kw = parse_args(content) query = args and args[0].strip() or None if not realms: # Check macro arguments for realms (typical wiki macro call). realms = 'realm' in kw and kw['realm'].split('|') or [] if query: # Add realms from query expression. realms.extend(query_realms(query, all_realms)) # Remove redundant realm selection for performance. if set(realms) == all_realms: query = re.sub('(^|\W)realm:\S+(\W|$)', ' ', query).strip() if name == 'TagCloud': # Set implicit 'all tagged realms' as default. if not realms: realms = all_realms if query: all_tags = Counter() # Require per resource query including view permission checks. for resource, tags in tag_system.query(req, query): all_tags.update(tags) else: # Allow faster per tag query, side steps permission checks. all_tags = tag_system.get_all_tags(req, realms=realms) mincount = 'mincount' in kw and kw['mincount'] or None return self.render_cloud(req, all_tags, caseless_sort=self.caseless_sort, mincount=mincount, realms=realms) elif name == 'ListTagged': if content and _OBSOLETE_ARGS_RE.search(content): data = {'warning': 'obsolete_args'} else: data = {'warning': None} context = formatter.context # Use TagsQuery arguments (most likely wiki macro calls). cols = 'cols' in kw and kw['cols'] or self.default_cols format = 'format' in kw and kw['format'] or self.default_format if not realms: # Apply ListTagged defaults to macro call w/o realm. realms = list(set(all_realms) - set(self.exclude_realms)) if not realms: return '' query = '(%s) (%s)' % (query or '', ' or '.join( ['realm:%s' % (r) for r in realms])) query_result = tag_system.query(req, query) excludes = [ exc.strip() for exc in kw.get('exclude', '').split(':') if exc.strip() ] if excludes and query_result: filtered_result = [(resource, tags) for resource, tags in query_result if not any( fnmatchcase(resource.id, exc) for exc in excludes)] query_result = filtered_result if not query_result: return '' def _link(resource): if resource.realm == 'tag': # Keep realm selection in tag links. return builder.a(resource.id, href=self.get_href(req, realms, tag=resource)) elif resource.realm == 'ticket': # Return resource link including ticket status dependend # class to allow for common Trac ticket link style. ticket = Ticket(env, resource.id) return builder.a('#%s' % ticket.id, class_=ticket['status'], href=formatter.href.ticket(ticket.id), title=shorten_line(ticket['summary'])) return render_resource_link(env, context, resource, 'compact') if format == 'table': cols = [ col for col in cols.split('|') if col in self.supported_cols ] # Use available translations from Trac core. try: labels = TicketSystem(env).get_ticket_field_labels() labels['id'] = _('Id') except AttributeError: # Trac 0.11 neither has the attribute nor uses i18n. labels = {'id': 'Id', 'description': 'Description'} labels['realm'] = _('Realm') labels['tags'] = _('Tags') headers = [{'label': labels.get(col)} for col in cols] data.update({'cols': cols, 'headers': headers}) try: results = sorted( query_result, key=lambda r: embedded_numbers(to_unicode(r[0].id))) except (InvalidQuery, InvalidTagRealm), e: return system_message(_("ListTagged macro error"), e) results = self._paginate(req, results, realms) rows = [] for resource, tags in results: desc = tag_system.describe_tagged_resource(req, resource) tags = sorted(tags) wiki_desc = format_to_oneliner(env, context, desc) if tags: rendered_tags = [ _link(Resource('tag', tag)) for tag in tags ] if 'oldlist' == format: resource_link = _link(resource) else: resource_link = builder.a(wiki_desc, href=get_resource_url( env, resource, context.href)) if 'table' == format: cells = [] for col in cols: if col == 'id': cells.append(_link(resource)) # Don't duplicate links to resource in both. elif col == 'description' and 'id' in cols: cells.append(wiki_desc) elif col == 'description': cells.append(resource_link) elif col == 'realm': cells.append(resource.realm) elif col == 'tags': cells.append( builder([(tag, ' ') for tag in rendered_tags])) rows.append({'cells': cells}) continue rows.append({ 'desc': wiki_desc, 'rendered_tags': None, 'resource_link': _link(resource) }) data.update({ 'format': format, 'paginator': results, 'results': rows, 'tags_url': req.href('tags') }) # Work around a bug in trac/templates/layout.html, that causes a # TypeError for the wiki macro call, if we use add_link() alone. add_stylesheet(req, 'common/css/search.css') return Chrome(env).render_template(req, 'listtagged_results.html', data, 'text/html', True)
class TicketTagProviderTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True, enable=['trac.*', 'tractags.*']) self.env.path = tempfile.mkdtemp() self.perms = PermissionSystem(self.env) self.db = self.env.get_db_cnx() setup = TagSetup(self.env) # Current tractags schema is setup with enabled component anyway. # Revert these changes for getting default permissions inserted. self._revert_tractags_schema_init() setup.upgrade_environment(self.db) self.provider = TicketTagProvider(self.env) self.realm = 'ticket' self.tag_sys = TagSystem(self.env) self.tags = ['tag1'] cursor = self.db.cursor() # Populate table with initial test data, not synced with tickets yet. cursor.execute(""" INSERT INTO tags (tagspace, name, tag) VALUES ('ticket', '1', 'deleted')""") self.realm = 'ticket' self._create_ticket(self.tags) self.req = Mock() # Mock an anonymous request. self.req.perm = PermissionCache(self.env) def tearDown(self): self.db.close() # Really close db connections. self.env.shutdown() shutil.rmtree(self.env.path) # Helpers def _create_ticket(self, tags): ticket = Ticket(self.env) ticket['keywords'] = u' '.join(sorted(map(to_unicode, tags))) ticket['summary'] = 'summary' ticket.insert() def _revert_tractags_schema_init(self): cursor = self.db.cursor() cursor.execute("DROP TABLE IF EXISTS tags") cursor.execute("DELETE FROM system WHERE name='tags_version'") cursor.execute( "DELETE FROM permission WHERE action %s" % self.db.like(), ('TAGS_%', )) def _tags(self): tags = {} cursor = self.db.cursor() cursor.execute("SELECT name,tag FROM tags") for name, tag in cursor.fetchall(): if name in tags: tags[name].add(tag) else: tags[name] = set([tag]) return tags # Tests def test_get_tags(self): resource = Resource('ticket', 2) self.assertRaises(ResourceNotFound, self.provider.get_resource_tags, self.req, resource) self._create_ticket(self.tags) self.assertEquals([ tag for tag in self.provider.get_resource_tags(self.req, resource) ], self.tags) #ignore_closed_tickets def test_set_tags(self): resource = Resource('ticket', 1) self.tags = ['tag2'] # Anonymous lacks required permissions. self.assertRaises(PermissionError, self.provider.set_resource_tags, self.req, resource, self.tags) self.req.authname = 'user' self.req.perm = PermissionCache(self.env, username='******') # Shouldn't raise an error with appropriate permission. self.provider.set_resource_tags(self.req, resource, self.tags) self.assertEquals( self.tag_sys.get_all_tags(self.req, '').keys(), self.tags) def test_remove_tags(self): resource = Resource('ticket', 1) # Anonymous lacks required permissions. self.assertRaises(PermissionError, self.provider.remove_resource_tags, self.req, resource) self.req.authname = 'user' self.req.perm = PermissionCache(self.env, username='******') # Shouldn't raise an error with appropriate permission. self.provider.remove_resource_tags(self.req, resource, 'comment') tkt = Ticket(self.env, 1) self.assertEquals(tkt['keywords'], '') def test_describe_tagged_resource(self): resource = Resource('ticket', 1) self.assertEquals( self.provider.describe_tagged_resource(self.req, resource), 'defect: summary')
class WikiTagProviderTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True, enable=['trac.*', 'tractags.*']) self.env.path = tempfile.mkdtemp() self.db = self.env.get_db_cnx() setup = TagSetup(self.env) # Current tractags schema is partially setup with enabled component. # Revert these changes for getting a clean setup. self._revert_tractags_schema_init() setup.upgrade_environment(self.db) self.perms = PermissionSystem(self.env) self.tag_s = TagSystem(self.env) self.tag_wp = WikiTagProvider(self.env) cursor = self.db.cursor() # Populate table with initial test data. cursor.execute(""" INSERT INTO tags (tagspace, name, tag) VALUES ('wiki', 'WikiStart', 'tag1') """) self.req = Mock(authname='editor') # Mock an anonymous request. self.req.perm = PermissionCache(self.env) self.realm = 'wiki' self.tags = ['tag1'] def tearDown(self): self.db.close() # Really close db connections. self.env.shutdown() shutil.rmtree(self.env.path) # Helpers def _revert_tractags_schema_init(self): cursor = self.db.cursor() cursor.execute("DROP TABLE IF EXISTS tags") cursor.execute("DROP TABLE IF EXISTS tags_change") cursor.execute("DELETE FROM system WHERE name='tags_version'") cursor.execute("DELETE FROM permission WHERE action %s" % self.db.like(), ('TAGS_%',)) # Tests def test_get_tags(self): resource = Resource('wiki', 'WikiStart') self.assertEquals([tag for tag in self.tag_wp.get_resource_tags(self.req, resource)], self.tags) def test_exclude_template_tags(self): cursor = self.db.cursor() # Populate table with more test data. cursor.execute(""" INSERT INTO tags (tagspace, name, tag) VALUES ('wiki', 'PageTemplates/Template', 'tag2') """) tags = ['tag1', 'tag2'] self.assertEquals(self.tag_s.get_all_tags(self.req).keys(), self.tags) self.env.config.set('tags', 'query_exclude_wiki_templates', False) self.assertEquals(self.tag_s.get_all_tags(self.req).keys(), tags) def test_set_tags_no_perms(self): resource = Resource('wiki', 'TaggedPage') self.assertRaises(PermissionError, self.tag_wp.set_resource_tags, self.req, resource, self.tags) def test_set_tags(self): resource = Resource('wiki', 'TaggedPage') self.req.perm = PermissionCache(self.env, username='******') # Shouldn't raise an error with appropriate permission. self.tag_wp.set_resource_tags(self.req, resource, self.tags) self.tag_wp.set_resource_tags(self.req, resource, ['tag2']) cursor = self.db.cursor() # Check change records. cursor.execute(""" SELECT * FROM tags_change WHERE tagspace=%s AND name=%s ORDER by time DESC """, ('wiki', 'TaggedPage')) row = cursor.fetchone() self.assertEqual([v for v in row[-3:]], ['editor', 'tag1', 'tag2']) row = cursor.fetchone() self.assertEqual([v for v in row[-3:]], ['editor', '', 'tag1'])
def expand_macro(self, formatter, name, content): env = self.env req = formatter.req args, kw = parse_args(content) # Use macro arguments (most likely wiki macro calls). realms = 'realm' in kw and kw['realm'].split('|') or [] tag_system = TagSystem(env) all_realms = [p.get_taggable_realm() for p in tag_system.tag_providers] self.all_realms = all_realms self.realms = realms if name == 'TagCloud': args.append(' or '.join(['realm:%s' % r for r in realms])) all_tags = tag_system.get_all_tags(req, ' '.join(args)) mincount = 'mincount' in kw and kw['mincount'] or None return self.render_cloud(req, all_tags, caseless_sort=self.caseless_sort, mincount=mincount) elif name == 'ListTagged': if _OBSOLETE_ARGS_RE.search(content): data = {'warning': 'obsolete_args'} else: data = {'warning': None} context=formatter.context # Use TagsQuery arguments (most likely wiki macro calls). cols = 'cols' in kw and kw['cols'] or self.default_cols format = 'format' in kw and kw['format'] or self.default_format query = args and args[0].strip() or None if query and not realms: # First read query arguments (most likely a web-UI call). for realm in all_realms: if re.search('(^|\W)realm:%s(\W|$)' % (realm), query): realms = realms and realms.append(realm) or [realm] if not realms: # Apply ListTagged defaults to macro call w/o realm. realms = list(set(all_realms)-set(self.exclude_realms)) if not realms: return '' else: self.query = query self.realms = realms query = '(%s) (%s)' % (query or '', ' or '.join(['realm:%s' % (r) for r in realms])) env.log.debug('LISTTAGGED_QUERY: ' + query) query_result = tag_system.query(req, query) if not query_result: return '' def _link(resource): if resource.realm == 'tag': # Keep realm selection in tag links. return builder.a(resource.id, href=self.get_href(req, tag=resource)) elif resource.realm == 'ticket': # Return resource link including ticket status dependend # class to allow for common Trac ticket link style. ticket = Ticket(env, resource.id) return builder.a('#%s' % ticket.id, class_=ticket['status'], href=formatter.href.ticket(ticket.id), title=shorten_line(ticket['summary'])) return render_resource_link(env, context, resource, 'compact') if format == 'table': cols = [col for col in cols.split('|') if col in self.supported_cols] # Use available translations from Trac core. try: labels = TicketSystem(env).get_ticket_field_labels() labels['id'] = _('Id') except AttributeError: # Trac 0.11 neither has the attribute nor uses i18n. labels = {'id': 'Id', 'description': 'Description'} labels['realm'] = _('Realm') labels['tags'] = _('Tags') headers = [{'label': labels.get(col)} for col in cols] data.update({'cols': cols, 'headers': headers}) results = sorted(query_result, key=lambda r: \ embedded_numbers(to_unicode(r[0].id))) results = self._paginate(req, results) rows = [] for resource, tags in results: desc = tag_system.describe_tagged_resource(req, resource) tags = sorted(tags) if tags: rendered_tags = [_link(Resource('tag', tag)) for tag in tags] if 'oldlist' == format: resource_link = _link(resource) else: desc = desc or \ get_resource_description(env, resource, context=context) resource_link = builder.a(desc, href=get_resource_url( env, resource, context.href)) if 'table' == format: cells = [] for col in cols: if col == 'id': cells.append(_link(resource)) # Don't duplicate links to resource in both. elif col == 'description' and 'id' in cols: cells.append(desc) elif col == 'description': cells.append(resource_link) elif col == 'realm': cells.append(resource.realm) elif col == 'tags': cells.append( builder([(tag, ' ') for tag in rendered_tags])) rows.append({'cells': cells}) continue rows.append({'desc': desc, 'rendered_tags': None, 'resource_link': _link(resource)}) data.update({'format': format, 'paginator': results, 'results': rows, 'tags_url': req.href('tags')}) # Work around a bug in trac/templates/layout.html, that causes a # TypeError for the wiki macro call, if we use add_link() alone. add_stylesheet(req, 'common/css/search.css') return Chrome(env).render_template( req, 'listtagged_results.html', data, 'text/html', True)
class TicketTagProviderTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True, enable=['trac.*', 'tractags.*']) self.env.path = tempfile.mkdtemp() self.perms = PermissionSystem(self.env) setup = TagSetup(self.env) # Current tractags schema is setup with enabled component anyway. # Revert these changes for getting default permissions inserted. self._revert_tractags_schema_init() setup.upgrade_environment() self.provider = TicketTagProvider(self.env) self.realm = 'ticket' self.tag_sys = TagSystem(self.env) self.tags = ['tag1', 'tag2'] # Populate tables with initial test data. self._create_ticket(self.tags) def tearDown(self): self.env.shutdown() shutil.rmtree(self.env.path) # Helpers def _create_ticket(self, tags, **kwargs): ticket = Ticket(self.env) ticket['keywords'] = u' '.join(sorted(map(to_unicode, tags))) ticket['summary'] = 'summary' ticket['reporter'] = 'admin' for name, value in kwargs.iteritems(): ticket[name] = value ticket.insert() return ticket def _revert_tractags_schema_init(self): with self.env.db_transaction as db: db("DROP TABLE IF EXISTS tags") db("DROP TABLE IF EXISTS tags_change") db("DELETE FROM system WHERE name='tags_version'") db("DELETE FROM permission WHERE action %s" % db.like(), ('TAGS_%', )) def _tags(self): tags = {} for name, tag in self.env.db_query(""" SELECT name,tag FROM tags """): if name in tags: tags[name].add(tag) else: tags[name] = set([tag]) return tags # Tests def test_get_tagged_resources(self): # No tags, no restrictions, all resources. req = MockRequest(self.env, authname='editor') self.assertEquals( [r for r in self.provider.get_tagged_resources(req, None)][0][1], set(self.tags)) # Force fine-grained perm-check check for all tags, not just the one # from query. self.provider.fast_permcheck = False self.assertEquals([ r for r in self.provider.get_tagged_resources( req, set(self.tags[:1])) ][0][1], set(self.tags)) def test_get_tags(self): req = MockRequest(self.env, authname='editor') resource = Resource('ticket', 2) self.assertRaises(ResourceNotFound, self.provider.get_resource_tags, req, resource) self._create_ticket(self.tags) self.assertEquals( [tag for tag in self.provider.get_resource_tags(req, resource)], self.tags) def test_set_tags(self): req = MockRequest(self.env, authname='editor') tags = ['tag3'] ticket = Ticket(self.env, 1) ticket['keywords'] = tags[0] # Tags get updated by TicketChangeListener method. ticket.save_changes(req.authname) self.assertEquals(self.tag_sys.get_all_tags(req).keys(), tags) def test_remove_tags(self): req = MockRequest(self.env, authname='editor') anon_req = MockRequest(self.env, authname='anonymous') resource = Resource('ticket', 1) # Anonymous lacks required permissions. self.assertRaises(PermissionError, self.provider.remove_resource_tags, anon_req, resource) # Shouldn't raise an error with appropriate permission. self.provider.remove_resource_tags(req, resource, 'comment') ticket = Ticket(self.env, 1) self.assertEquals(ticket['keywords'], '') def test_describe_tagged_resource(self): req = MockRequest(self.env, authname='editor') resource = Resource('ticket', 1) self.assertEquals( self.provider.describe_tagged_resource(req, resource), 'defect: summary') def test_create_ticket_by_anonymous(self): req = MockRequest(self.env, authname='editor') ticket = self._create_ticket(self.tags, reporter='anonymous') tags = self.provider.get_resource_tags(req, ticket.resource) self.assertEquals(tags, set(self.tags)) def test_update_ticket_by_anonymous(self): req = MockRequest(self.env, authname='editor') ticket = self._create_ticket([]) tags = self.provider.get_resource_tags(req, ticket.resource) self.assertEquals(tags, set([])) ticket['keywords'] = ', '.join(self.tags) ticket.save_changes('anonymous', comment='Adding keywords') tags = self.provider.get_resource_tags(req, ticket.resource) self.assertEquals(tags, set(self.tags))
class TagWikiSyntaxProvider(Component): """[opt] Provides tag:<expr> links. This extends TracLinks via WikiFormatting to point at tag queries or at specific tags. """ implements(IWikiSyntaxProvider) def __init__(self): self.tag_system = TagSystem(self.env) # IWikiSyntaxProvider methods def get_wiki_syntax(self): """Additional syntax for quoted tags or tag expression.""" tag_expr = ( r"(%s)" % (WikiParser.QUOTED_STRING) ) # Simple (tag|tagged):link syntax yield (r'''(?P<qualifier>tag(?:ged)?):(?P<tag_expr>%s)''' % tag_expr, lambda f, ns, match: self._format_tagged( f, match.group('qualifier'), match.group('tag_expr'), '%s:%s' % (match.group('qualifier'), match.group('tag_expr')))) # [(tag|tagged):link with label] yield (r'''\[tag(?:ged)?:''' r'''(?P<ltag_expr>%s)\s*(?P<tag_title>[^\]]+)?\]''' % tag_expr, lambda f, ns, match: self._format_tagged(f, 'tag', match.group('ltag_expr'), match.group('tag_title'))) def get_link_resolvers(self): return [('tag', self._format_tagged), ('tagged', self._format_tagged)] def _format_tagged(self, formatter, ns, target, label, fullmatch=None): """Tag and tag query expression link formatter.""" def unquote(text): """Strip all matching pairs of outer quotes from string.""" while re.match(WikiParser.QUOTED_STRING, text): # Remove outer whitespace after stripped quotation too. text = text[1:-1].strip() return text label = label and unquote(label.strip()) or '' target = unquote(target.strip()) query = target # Pop realms from query expression. all_realms = self.tag_system.get_taggable_realms(formatter.perm) realms = query_realms(target, all_realms) if realms: kwargs = dict((realm, 'on') for realm in realms) target = re.sub('(^|\W)realm:\S+(\W|$)', ' ', target).strip() else: kwargs = {} tag_res = Resource('tag', target) if 'TAGS_VIEW' not in formatter.perm(tag_res): return tag.span(label, class_='forbidden tags', title=_("no permission to view tags")) context = formatter.context href = self.tag_system.get_resource_url(tag_res, context.href, kwargs) if all_realms and ( target in self.tag_system.get_all_tags(formatter.req) or not iter_is_empty(self.tag_system.query(formatter.req, query))): # At least one tag provider is available and tag exists or # tags query yields at least one match. if label: return tag.a(label, href=href) return render_resource_link(self.env, context, tag_res) return tag.a(label+'?', href=href, class_='missing tags', rel='nofollow')
class TagRPC(Component): """[extra] RPC interface for the tag system. Access Trac resource tagging system through methods provided by [https://trac-hacks.org/wiki/XmlRpcPlugin XmlRpcPlugin]. """ implements(IXMLRPCHandler) def __init__(self): self.tag_system = TagSystem(self.env) # IXMLRPCHandler methods def xmlrpc_namespace(self): return 'tags' def xmlrpc_methods(self): yield (None, ((list, str),), self.splitIntoTags) yield ('TAGS_VIEW', ((list,),), self.getTaggableRealms) yield ('TAGS_VIEW', ((dict,), (dict, list)), self.getAllTags) yield ('TAGS_VIEW', ((list, str, str),), self.getTags) yield ('TAGS_VIEW', ((list, str),), self.query) yield ('TAGS_MODIFY', ((list, str, str, list), (list, str, str, list, str)), self.addTags) yield ('TAGS_MODIFY', ((list, str, str, list), (list, str, str, list, str)), self.setTags) # Exported functions and TagSystem methods def addTags(self, req, realm, id, tags, comment=u''): """Add the supplied list of tags to a taggable Trac resource. Returns the updated list of resource tags. """ resource = Resource(realm, id) # Replicate TagSystem.add_tags() method due to xmlrpclib issue. tags = set(tags) tags.update(self._get_tags(req, resource)) self.tag_system.set_tags(req, resource, tags, comment) return self._get_tags(req, resource) def getAllTags(self, req, realms=[]): """Returns a dict of all tags as keys and occurrences as values. If a realm list is supplied, only tags from these taggable realms are shown. """ # Type conversion needed for content transfer of Counter object. return dict(self.tag_system.get_all_tags(req, realms)) def getTaggableRealms(self, req): """Returns the list of taggable Trac realms.""" return list(self.tag_system.get_taggable_realms()) def getTags(self, req, realm, id): """Returns the list of tags for a Trac resource.""" return self._get_tags(req, Resource(realm, id)) def query(self, req, query_str): """Returns a list of tagged Trac resources, whose tags match the supplied tag query expression. """ # Type conversion needed for content transfer of Python set objects. return [(resource.realm, resource.id, list(tags)) for resource, tags in self.tag_system.query(req, query_str)] def setTags(self, req, realm, id, tags, comment=u''): """Replace tags for a Trac resource with the supplied list of tags. Returns the updated list of resource tags. """ resource = Resource(realm, id) self._get_tags(req, resource) # Trac resource exists? self.tag_system.set_tags(req, resource, tags, comment) return self._get_tags(req, resource) def splitIntoTags(self, req, tag_str): """Returns a list of tags from a string. Comma, whitespace and combinations of these characters are recognized as delimiter, that get stripped from the output. """ return split_into_tags(tag_str) # Private methods def _get_tags(self, req, resource): if not resource_exists(self.env, resource): raise ResourceNotFound('Resource "%r" does not exists' % resource) # Workaround for ServiceException when calling TagSystem.get_tags(). provider = [p for p in self.tag_system.tag_providers if p.get_taggable_realm() == resource.realm][0] # Type conversion needed for content transfer of Python set objects. return list(provider.get_resource_tags(req, resource))
class TicketTagProviderTestCase(unittest.TestCase): def setUp(self): self.env = EnvironmentStub(default_data=True, enable=['trac.*', 'tractags.*']) self.env.path = tempfile.mkdtemp() self.perms = PermissionSystem(self.env) self.db = self.env.get_db_cnx() setup = TagSetup(self.env) # Current tractags schema is setup with enabled component anyway. # Revert these changes for getting default permissions inserted. self._revert_tractags_schema_init() setup.upgrade_environment(self.db) self.provider = TicketTagProvider(self.env) self.realm = 'ticket' self.tag_sys = TagSystem(self.env) self.tags = ['tag1'] cursor = self.db.cursor() # Populate table with initial test data, not synced with tickets yet. cursor.execute(""" INSERT INTO tags (tagspace, name, tag) VALUES ('ticket', '1', 'deleted')""") self.realm = 'ticket' self._create_ticket(self.tags) self.req = Mock() # Mock an anonymous request. self.req.perm = PermissionCache(self.env) def tearDown(self): self.db.close() # Really close db connections. self.env.shutdown() shutil.rmtree(self.env.path) # Helpers def _create_ticket(self, tags): ticket = Ticket(self.env) ticket['keywords'] = u' '.join(sorted(map(to_unicode, tags))) ticket['summary'] = 'summary' ticket.insert() def _revert_tractags_schema_init(self): cursor = self.db.cursor() cursor.execute("DROP TABLE IF EXISTS tags") cursor.execute("DELETE FROM system WHERE name='tags_version'") cursor.execute("DELETE FROM permission WHERE action %s" % self.db.like(), ('TAGS_%',)) def _tags(self): tags = {} cursor = self.db.cursor() cursor.execute("SELECT name,tag FROM tags") for name, tag in cursor.fetchall(): if name in tags: tags[name].add(tag) else: tags[name] = set([tag]) return tags # Tests def test_get_tags(self): resource = Resource('ticket', 2) self.assertRaises(ResourceNotFound, self.provider.get_resource_tags, self.req, resource) self._create_ticket(self.tags) self.assertEquals( [tag for tag in self.provider.get_resource_tags(self.req, resource)], self.tags) #ignore_closed_tickets def test_set_tags(self): resource = Resource('ticket', 1) self.tags = ['tag2'] # Anonymous lacks required permissions. self.assertRaises(PermissionError, self.provider.set_resource_tags, self.req, resource, self.tags) self.req.authname = 'user' self.req.perm = PermissionCache(self.env, username='******') # Shouldn't raise an error with appropriate permission. self.provider.set_resource_tags(self.req, resource, self.tags) self.assertEquals(self.tag_sys.get_all_tags(self.req, '').keys(), self.tags) def test_remove_tags(self): resource = Resource('ticket', 1) # Anonymous lacks required permissions. self.assertRaises(PermissionError, self.provider.remove_resource_tags, self.req, resource) self.req.authname = 'user' self.req.perm = PermissionCache(self.env, username='******') # Shouldn't raise an error with appropriate permission. self.provider.remove_resource_tags(self.req, resource, 'comment') tkt = Ticket(self.env, 1) self.assertEquals(tkt['keywords'], '') def test_describe_tagged_resource(self): resource = Resource('ticket', 1) self.assertEquals( self.provider.describe_tagged_resource(self.req, resource), 'defect: summary')