def _update_tags(self, req, page): tag_system = TagSystem(self.env) newtags = tag_system.split_into_tags(req.args.get('tags', '')) oldtags = tag_system.get_tags(req, page.resource) if oldtags != newtags: tag_system.set_tags(req, page.resource, newtags) return True return False
def _update_tags(self, req, resource, new_tags): # Get old tags of the resource. tag_system = TagSystem(self.env) old_tags = self._get_stored_tags(req, resource) self.log.debug("setting tags: %s" % (new_tags,)) # Replace with new tags if different. if old_tags != new_tags: tag_system.set_tags(req, resource, new_tags) return True return False
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 WikiTagInterface(TagTemplateProvider): """[main] Implements the user interface for tagging Wiki pages.""" implements(IRequestFilter, ITemplateStreamFilter, IWikiChangeListener, IWikiPageManipulator) def __init__(self): self.tag_system = TagSystem(self.env) # IRequestFilter methods def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): if template is not None: if req.method == 'GET' and req.path_info.startswith('/wiki/'): if req.args.get('action') == 'edit' and \ req.args.get('template') and 'tags' not in req.args: self._post_process_request_edit(req) if req.args.get('action') == 'history' and \ data and 'history' in data: self._post_process_request_history(req, data) elif req.method == 'POST' and \ req.path_info.startswith('/wiki/') and \ 'save' in req.args: requests.reset() return template, data, content_type # ITemplateStreamFilter methods def filter_stream(self, req, method, filename, stream, data): page_name = req.args.get('page', 'WikiStart') resource = Resource('wiki', page_name) if filename == 'wiki_view.html' and 'TAGS_VIEW' in req.perm(resource): return self._wiki_view(req, stream) elif filename == 'wiki_edit.html' and \ 'TAGS_MODIFY' in req.perm(resource): return self._wiki_edit(req, stream) elif filename == 'history_view.html' and \ 'TAGS_VIEW' in req.perm(resource): return self._wiki_history(req, stream) return stream # IWikiPageManipulator methods def prepare_wiki_page(self, req, page, fields): pass def validate_wiki_page(self, req, page): # If we're saving the wiki page, and can modify tags, do so. if req and 'TAGS_MODIFY' in req.perm(page.resource) \ and req.path_info.startswith('/wiki') and 'save' in req.args: page_modified = req.args.get('text') != page.old_text or \ page.readonly != int('readonly' in req.args) if page_modified: requests.set(req) req.add_redirect_listener(self._redirect_listener) elif page.version > 0: # If the page hasn't been otherwise modified, save tags and # redirect to avoid the "page has not been modified" warning. if self._update_tags(req, page): req.redirect( get_resource_url(self.env, page.resource, req.href, version=None)) return [] # IWikiChangeListener methods def wiki_page_added(self, page): req = requests.get() if req: self._update_tags(req, page, page.time) def wiki_page_changed(self, page, version, t, comment, author, ipnr): req = requests.get() if req: self._update_tags(req, page, page.time) def wiki_page_renamed(self, page, old_name): """Called when a page has been renamed (since Trac 0.12).""" self.log.debug("Moving wiki page tags from %s to %s", old_name, page.name) req = MockReq() self.tag_system.reparent_tags(req, Resource('wiki', page.name), old_name) def wiki_page_deleted(self, page): # Page gone, so remove all records on it. delete_tags(self.env, page.resource, purge=True) def wiki_page_version_deleted(self, page): pass # Internal methods def _page_tags(self, req): pagename = req.args.get('page', 'WikiStart') version = req.args.get('version') tags_version = req.args.get('tags_version') page = WikiPage(self.env, pagename, version=version) resource = page.resource if version and not tags_version: tags_version = page.time tags = sorted( self.tag_system.get_tags(req, resource, when=tags_version)) return tags def _redirect_listener(self, req, url, permanent): requests.reset() def _post_process_request_edit(self, req): # Retrieve template resource to be queried for tags. template_pagename = ''.join( [WikiModule.PAGE_TEMPLATES_PREFIX, req.args.get('template')]) template_page = WikiPage(self.env, template_pagename) if template_page.exists and \ 'TAGS_VIEW' in req.perm(template_page.resource): tags = sorted(self.tag_system.get_tags(req, template_page.resource)) # Prepare tags as content for the editor field. tags_str = ' '.join(tags) self.env.log.debug("Tags retrieved from template: '%s'", unicode(tags_str).encode('utf-8')) # DEVEL: More arguments need to be propagated here? req.redirect( req.href(req.path_info, action='edit', tags=tags_str, template=req.args.get('template'))) def _post_process_request_history(self, req, data): history = [] page_histories = data.get('history', []) resource = data['resource'] tags_histories = tag_changes(self.env, resource) for page_history in page_histories: while tags_histories and \ tags_histories[0][0] >= page_history['date']: tags_history = tags_histories.pop(0) date = tags_history[0] author = tags_history[1] comment = render_tag_changes(tags_history[2], tags_history[3]) url = req.href(resource.realm, resource.id, version=page_history['version'], tags_version=to_utimestamp(date)) history.append({ 'version': '*', 'url': url, 'date': date, 'author': author, 'comment': comment, 'ipnr': '' }) history.append(page_history) data.update( dict(history=history, wiki_to_oneliner=self._wiki_to_oneliner)) def _wiki_view(self, req, stream): add_stylesheet(req, 'tags/css/tractags.css') tags = self._page_tags(req) if not tags: return stream li = [] for tag_ in tags: resource = Resource('tag', tag_) anchor = render_resource_link(self.env, web_context(req, resource), resource) anchor = anchor(rel='tag') li.append(tag.li(anchor, ' ')) # TRANSLATOR: Header label text for tag list at wiki page bottom. insert = tag.ul(class_='tags')(tag.li(_("Tags"), class_='header'), li) return stream | ( Transformer('//div[contains(@class,"wikipage")]').after(insert)) def _update_tags(self, req, page, when=None): newtags = split_into_tags(req.args.get('tags', '')) oldtags = self.tag_system.get_tags(req, page.resource) if oldtags != newtags: self.tag_system.set_tags(req, page.resource, newtags, when=when) return True return False def _wiki_edit(self, req, stream): # TRANSLATOR: Label text for link to '/tags'. link = tag.a(_("view all tags"), href=req.href.tags()) # TRANSLATOR: ... (view all tags) insert = tag(Markup(_("Tag under: (%(tags_link)s)", tags_link=link))) insert( tag.br(), tag.input(id='tags', type='text', name='tags', size='50', value=req.args.get('tags', ' '.join(self._page_tags(req))))) insert = tag.div(tag.label(insert), class_='field') return stream | Transformer('//div[@id="changeinfo1"]').append(insert) def _wiki_history(self, req, stream): xpath = '//input[@type="radio" and @value="*"]' stream = stream | Transformer(xpath).remove() # Remove invalid links to wiki page revisions (fix for Trac < 0.12). xpath = '//a[contains(@href,"%2A")]' return stream | Transformer(xpath).remove() def _wiki_to_oneliner(self, context, wiki, shorten=None): if isinstance(wiki, Fragment): return wiki return format_to_oneliner(self.env, context, wiki, shorten=shorten)
class WikiTagInterface(TagTemplateProvider): """[main] Implements the user interface for tagging Wiki pages.""" implements(IRequestFilter, ITemplateStreamFilter, IWikiChangeListener, IWikiPageManipulator) def __init__(self): self.tag_system = TagSystem(self.env) # IRequestFilter methods def pre_process_request(self, req, handler): return handler def post_process_request(self, req, template, data, content_type): if template is not None: if req.method == 'GET' and req.path_info.startswith('/wiki/'): if req.args.get('action') == 'edit' and \ req.args.get('template') and 'tags' not in req.args: self._post_process_request_edit(req) if req.args.get('action') == 'history' and \ data and 'history' in data: self._post_process_request_history(req, data) elif req.method == 'POST' and \ req.path_info.startswith('/wiki/') and \ 'save' in req.args: requests.reset() return template, data, content_type # ITemplateStreamFilter methods def filter_stream(self, req, method, filename, stream, data): page_name = req.args.get('page', 'WikiStart') resource = Resource('wiki', page_name) if filename == 'wiki_view.html' and 'TAGS_VIEW' in req.perm(resource): return self._wiki_view(req, stream) elif filename == 'wiki_edit.html' and \ 'TAGS_MODIFY' in req.perm(resource): return self._wiki_edit(req, stream) elif filename == 'history_view.html' and \ 'TAGS_VIEW' in req.perm(resource): return self._wiki_history(req, stream) return stream # IWikiPageManipulator methods def prepare_wiki_page(self, req, page, fields): pass def validate_wiki_page(self, req, page): # If we're saving the wiki page, and can modify tags, do so. if req and 'TAGS_MODIFY' in req.perm(page.resource) \ and req.path_info.startswith('/wiki') and 'save' in req.args: page_modified = req.args.get('text') != page.old_text or \ page.readonly != int('readonly' in req.args) if page_modified: requests.set(req) req.add_redirect_listener(self._redirect_listener) elif page.version > 0: # If the page hasn't been otherwise modified, save tags and # redirect to avoid the "page has not been modified" warning. if self._update_tags(req, page): req.redirect(get_resource_url(self.env, page.resource, req.href, version=None)) return [] # IWikiChangeListener methods def wiki_page_added(self, page): req = requests.get() if req: self._update_tags(req, page, page.time) def wiki_page_changed(self, page, version, t, comment, author, ipnr): req = requests.get() if req: self._update_tags(req, page, page.time) def wiki_page_renamed(self, page, old_name): """Called when a page has been renamed (since Trac 0.12).""" self.log.debug("Moving wiki page tags from %s to %s", old_name, page.name) req = MockReq() self.tag_system.reparent_tags(req, Resource('wiki', page.name), old_name) def wiki_page_deleted(self, page): # Page gone, so remove all records on it. delete_tags(self.env, page.resource, purge=True) def wiki_page_version_deleted(self, page): pass # Internal methods def _page_tags(self, req): pagename = req.args.get('page', 'WikiStart') version = req.args.get('version') tags_version = req.args.get('tags_version') page = WikiPage(self.env, pagename, version=version) resource = page.resource if version and not tags_version: tags_version = page.time tags = sorted(self.tag_system.get_tags(req, resource, when=tags_version)) return tags def _redirect_listener(self, req, url, permanent): requests.reset() def _post_process_request_edit(self, req): # Retrieve template resource to be queried for tags. template_pagename = ''.join([WikiModule.PAGE_TEMPLATES_PREFIX, req.args.get('template')]) template_page = WikiPage(self.env, template_pagename) if template_page.exists and \ 'TAGS_VIEW' in req.perm(template_page.resource): tags = sorted(self.tag_system.get_tags(req, template_page.resource)) # Prepare tags as content for the editor field. tags_str = ' '.join(tags) self.env.log.debug("Tags retrieved from template: '%s'", unicode(tags_str).encode('utf-8')) # DEVEL: More arguments need to be propagated here? req.redirect(req.href(req.path_info, action='edit', tags=tags_str, template=req.args.get('template'))) def _post_process_request_history(self, req, data): history = [] page_histories = data.get('history', []) resource = data['resource'] tags_histories = tag_changes(self.env, resource) for page_history in page_histories: while tags_histories and \ tags_histories[0][0] >= page_history['date']: tags_history = tags_histories.pop(0) date = tags_history[0] author = tags_history[1] comment = render_tag_changes(tags_history[2], tags_history[3]) url = req.href(resource.realm, resource.id, version=page_history['version'], tags_version=to_utimestamp(date)) history.append({'version': '*', 'url': url, 'date': date, 'author': author, 'comment': comment, 'ipnr': ''}) history.append(page_history) data.update(dict(history=history, wiki_to_oneliner=self._wiki_to_oneliner)) def _wiki_view(self, req, stream): add_stylesheet(req, 'tags/css/tractags.css') tags = self._page_tags(req) if not tags: return stream li = [] for tag_ in tags: resource = Resource('tag', tag_) anchor = render_resource_link(self.env, web_context(req, resource), resource) anchor = anchor(rel='tag') li.append(tag.li(anchor, ' ')) # TRANSLATOR: Header label text for tag list at wiki page bottom. insert = tag.ul(class_='tags')(tag.li(_("Tags"), class_='header'), li) return stream | (Transformer('//div[contains(@class,"wikipage")]') .after(insert)) def _update_tags(self, req, page, when=None): newtags = split_into_tags(req.args.get('tags', '')) oldtags = self.tag_system.get_tags(req, page.resource) if oldtags != newtags: self.tag_system.set_tags(req, page.resource, newtags, when=when) return True return False def _wiki_edit(self, req, stream): # TRANSLATOR: Label text for link to '/tags'. link = tag.a(_("view all tags"), href=req.href.tags()) # TRANSLATOR: ... (view all tags) insert = tag(Markup(_("Tag under: (%(tags_link)s)", tags_link=link))) insert( tag.br(), tag.input(id='tags', type='text', name='tags', size='50', value=req.args.get('tags', ' '.join(self._page_tags(req)))) ) insert = tag.div(tag.label(insert), class_='field') return stream | Transformer('//div[@id="changeinfo1"]').append(insert) def _wiki_history(self, req, stream): xpath = '//input[@type="radio" and @value="*"]' stream = stream | Transformer(xpath).remove() # Remove invalid links to wiki page revisions (fix for Trac < 0.12). xpath = '//a[contains(@href,"%2A")]' return stream | Transformer(xpath).remove() def _wiki_to_oneliner(self, context, wiki, shorten=None): if isinstance(wiki, Fragment): return wiki return format_to_oneliner(self.env, context, wiki, shorten=shorten)
class TagSystemTestCase(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.req = Mock() self.actions = ['TAGS_ADMIN', 'TAGS_MODIFY', 'TAGS_VIEW'] self.tag_s = TagSystem(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) 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_available_actions(self): for action in self.actions: self.failIf(action not in self.perms.get_actions()) def test_available_providers(self): # Standard implementations of DefaultTagProvider should be registered. seen = [] for provider in [TicketTagProvider(self.env), WikiTagProvider(self.env)]: self.failIf(provider not in self.tag_s.tag_providers) # Ensure unique provider references, a possible bug in Trac-0.11. self.failIf(provider in seen) seen.append(provider) def test_set_tags_no_perms(self): resource = Resource('wiki', 'WikiStart') tags = ['tag1'] # Mock an anonymous request. self.req.perm = PermissionCache(self.env) self.assertRaises(PermissionError, self.tag_s.set_tags, self.req, resource, tags) def test_set_tags(self): resource = Resource('wiki', 'WikiStart') tags = ['tag1'] self.req.perm = PermissionCache(self.env, username='******') # Shouldn't raise an error with appropriate permission. self.tag_s.set_tags(self.req, resource, tags) def test_query_no_args(self): # Regression test for query without argument, # reported as th:ticket:7857. # Mock an anonymous request. self.req.perm = PermissionCache(self.env) self.assertEquals([(res, tags) for res, tags in self.tag_s.query(self.req, query='')], [])
def _new_blog_post(self, req): """ Generate a new blog post """ action = req.args.get('action', 'edit') wikitext = req.args.get('text', '') blogtitle = req.args.get('blogtitle', '') page_format = req.args.get('pagename', self.page_format) tags = self._get_tags(req) referer = self._get_referer(req) author = req.authname pagename = self._generate_pagename(page_format, blogtitle, author) titleline = ' '.join(["=", blogtitle, "=\n"]) page = WikiPage(self.env, pagename, None) page.text = wikitext comment = req.args.get('comment', '') readonly = int(req.args.has_key('readonly')) edit_rows = req.args.get('edit_rows', '20') scroll_bar_pos = req.args.get('scroll_bar_pos', '') page_source = page.text if blogtitle: wikitext = ''.join([titleline, wikitext]) if req.method == 'POST': if action == 'edit': if req.args.has_key('cancel'): req.redirect(referer) page = WikiPage(self.env, pagename, None) tagsystem = TagSystem(self.env) # Add footer page.text = ''.join([wikitext, "\n\n", self.var_subs(author, self.footer)]) page.readonly = readonly if req.args.has_key('preview'): action = 'preview' else: page.save(author, comment, req.remote_addr) tagsystem.set_tags(req, page.resource, tags) req.redirect(referer) wiki = {'page_name' : pagename, 'comment' : comment, 'author' : author, 'edit_rows' : edit_rows, 'version' : 0, 'read_only' : readonly, 'scroll_bar_pos' : scroll_bar_pos, 'page_source' : page_source, 'action' : action, } data = {'page' : page, 'action' : action, 'title' : self.entry_page_title, 'blog' : {'title' : blogtitle, 'pagename' : pagename, 'referer' : referer, }, 'tags' : ', '.join(tags), 'referer' : referer, } data.update(wiki) return data
class TagSystemTestCase(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.req = Mock() self.actions = ['TAGS_ADMIN', 'TAGS_MODIFY', 'TAGS_VIEW'] self.tag_s = TagSystem(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) 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_available_actions(self): for action in self.actions: self.failIf(action not in self.perms.get_actions()) def test_available_providers(self): # Standard implementations of DefaultTagProvider should be registered. seen = [] for provider in [ TicketTagProvider(self.env), WikiTagProvider(self.env) ]: self.failIf(provider not in self.tag_s.tag_providers) # Ensure unique provider references, a possible bug in Trac-0.11. self.failIf(provider in seen) seen.append(provider) def test_set_tags_no_perms(self): resource = Resource('wiki', 'WikiStart') tags = ['tag1'] # Mock an anonymous request. self.req.perm = PermissionCache(self.env) self.assertRaises(PermissionError, self.tag_s.set_tags, self.req, resource, tags) def test_set_tags(self): resource = Resource('wiki', 'WikiStart') tags = ['tag1'] self.req.perm = PermissionCache(self.env, username='******') # Shouldn't raise an error with appropriate permission. self.tag_s.set_tags(self.req, resource, tags) def test_query_no_args(self): # Regression test for query without argument, # reported as th:ticket:7857. # Mock an anonymous request. self.req.perm = PermissionCache(self.env) self.assertEquals( [(res, tags) for res, tags in self.tag_s.query(self.req, query='')], [])