Index: tractags/api.py
===================================================================
--- tractags/api.py (revision 10506)
+++ tractags/api.py (working copy)
@@ -10,6 +10,7 @@
from pkg_resources import resource_filename
from trac.core import *
+from trac.config import BoolOption
from trac.resource import Resource
from trac.perm import IPermissionRequestor, PermissionError
from trac.web.chrome import add_warning
@@ -42,10 +43,10 @@
class ITagProvider(Interface):
- def get_taggable_realm():
+ def get_taggable_realm(self):
"""Return the realm this provider supports tags on."""
- def get_tagged_resources(req, tags=None):
+ def get_tagged_resources(self, req, tags=None):
"""Return a sequence of resources and *all* their tags.
:param tags: If provided, return only those resources with the given
@@ -54,16 +55,19 @@
:rtype: Sequence of (resource, tags) tuples.
"""
- def get_resource_tags(req, resource):
+ def get_resource_tags(self, req, resource):
"""Get tags for a Resource object."""
- def set_resource_tags(req, resource, tags):
+ def set_resource_tags(self, req, resource, tags):
"""Set tags for a resource."""
- def remove_resource_tags(req, resource):
+ def remove_resource_tags(self, req, resource):
"""Remove all tags from a resource."""
- def describe_tagged_resource(req, resource):
+ def rename_resource(self, req, old_resource, new_resource):
+ """Rename an existing resource, i.e. move tags from one resource to other."""
+
+ def describe_tagged_resource(self, req, resource):
"""Return a one line description of the tagged resource."""
@@ -105,7 +109,7 @@
args = [self.realm]
sql = 'SELECT DISTINCT name FROM tags WHERE tagspace=%s'
if tags:
- sql += ' AND tags.tag IN (%s)' % ', '.join(['%s' for t in tags])
+ sql += ' AND tags.tag IN (%s)' % ', '.join(['%s' for dummy in tags])
args += tags
sql += ' ORDER by name'
cursor.execute(sql, args)
@@ -117,7 +121,7 @@
resources[resource.id] = resource
if not resources:
- return
+ return
args = [self.realm] + list(resources)
# XXX Is this going to be excruciatingly slow?
@@ -175,6 +179,18 @@
def describe_tagged_resource(self, req, resource):
return ''
+ def rename_resource(self, old_resource, new_resource):
+ assert old_resource.realm == self.realm
+ db = self.env.get_db_cnx()
+ try:
+ cursor = db.cursor()
+ cursor.execute('UPDATE tags SET name=%s WHERE tagspace=%s AND name=%s',
+ (new_resource.id, self.realm, old_resource.id))
+ db.commit()
+ except:
+ db.rollback()
+ raise
+
class TagSystem(Component):
"""Tagging system for Trac."""
@@ -182,7 +198,10 @@
implements(IPermissionRequestor, IResourceManager)
tag_providers = ExtensionPoint(ITagProvider)
-
+
+ wiki_page_link = BoolOption('tags', 'wiki_page_link', True,
+ 'Link a tag to the wiki page with the same name if it exists.')
+
# Internal variables
_realm = re.compile('realm:(\w+)', re.U | re.I)
_tag_split = re.compile('[,\s]+')
@@ -211,18 +230,18 @@
'realm': realm_handler,
}
all_attribute_handlers.update(attribute_handlers or {})
- if re.search(r'(expression|tagspace|tagspaces|operation|showheadings'
- '|expression)=', query):
+ if re.search(r'(expression|tagspace|tagspaces|operation|showheadings)=',
+ query):
message = Markup('You seem to be using an old Tag query. '
'Try using the new syntax in your '
'ListTagged macro.',
req.href('tags'))
add_warning(req, message)
query = Query(query, attribute_handlers=all_attribute_handlers)
- providers = []
+ providers = set()
for m in self._realm.finditer(query.as_string()):
realm = m.group(1)
- providers.append(self._get_provider(realm))
+ providers.add(self._get_provider(realm))
if not providers:
providers = self.tag_providers
@@ -280,6 +299,11 @@
current_tags.difference_update(tags)
provider.set_resource_tags(req, resource, current_tags)
+ def rename_resource(self, old_resource, new_resource):
+ assert old_resource.realm == new_resource.realm
+ provider = self._get_provider(old_resource.realm)
+ provider.rename_resource(old_resource, new_resource)
+
def split_into_tags(self, text):
"""Split plain text into tags."""
return set([t.strip() for t in self._tag_split.split(text) if t.strip()])
@@ -293,6 +317,22 @@
self.env.log.warning('ITagProvider %r does not implement '
'describe_tagged_resource()' % provider)
return ''
+
+ def rename_tag(self, req, old_tag, new_tag):
+ """Rename a certain tag in all resources it exists in
+
+ receives self, the request, the old tag's name and the new tag's name.
+ """
+ # Go over all the Resource Providers
+ for resource_provider in self.tag_providers:
+
+ # Go over all the resources in the resource provider which have the old tag we want to rename
+ for resource, tags in resource_provider.get_tagged_resources(req, [old_tag]):
+ # Replacing the tags
+ tags = set(tags)
+ tags.remove(old_tag)
+ tags.add(new_tag)
+ self.set_tags(req, resource, tags)
# IPermissionRequestor methods
def get_permission_actions(self):
@@ -303,17 +343,19 @@
yield 'tag'
def get_resource_url(self, resource, href, **kwargs):
- page = WikiPage(self.env, resource.id)
- if page.exists:
- return get_resource_url(self.env, page.resource, href, **kwargs)
+ if self.wiki_page_link:
+ page = WikiPage(self.env, resource.id)
+ if page.exists:
+ return get_resource_url(self.env, page.resource, href, **kwargs)
return href("tags/'%s'" % unicode(resource.id).replace("'", "\\'"))
def get_resource_description(self, resource, format='default', context=None,
**kwargs):
- page = WikiPage(self.env, resource.id)
- if page.exists:
- return get_resource_description(self.env, page.resource, format,
- **kwargs)
+ if self.wiki_page_link:
+ page = WikiPage(self.env, resource.id)
+ if page.exists:
+ return get_resource_description(self.env, page.resource,
+ format, **kwargs)
rid = to_unicode(resource.id)
if format in ('compact', 'default'):
return rid
Index: tractags/__init__.py
===================================================================
--- tractags/__init__.py (revision 10506)
+++ tractags/__init__.py (working copy)
@@ -16,3 +16,4 @@
import web_ui
import macros
import model
+import admin_ui
Index: tractags/web_ui.py
===================================================================
--- tractags/web_ui.py (revision 10506)
+++ tractags/web_ui.py (working copy)
@@ -19,11 +19,11 @@
from tractags.api import TagSystem, ITagProvider, _, tag_
from tractags.query import InvalidQuery
from trac.resource import Resource
+from trac.config import Option, ChoiceOption, ListOption
from trac.mimeview import Context
from trac.wiki.formatter import Formatter
from trac.wiki.model import WikiPage
-
class TagTemplateProvider(Component):
"""Provides templates and static resources for the tags plugin."""
@@ -52,6 +52,26 @@
implements(IRequestHandler, INavigationContributor)
tag_providers = ExtensionPoint(ITagProvider)
+
+ default_format = ChoiceOption('tags', 'default_format',
+ ['oldlist', 'table', 'short'],
+ '''Set the default format for ListTagged calls.
+
+ || `oldlist` (default value) || The original format \
+ with a bulleted-list of "linked-id description (tags)" ||
+ || `short` || Bulleted-list of "linked-description" ||
+ || `table` || Table... (see column option) ||''')
+ default_cols = Option('tags', 'default_table_cols',
+ 'id|description|tags',
+ '''Select columns and order for table format using a
+ "|"-separated list of column names.
+
+ Supported columns: realm, id, description, tags
+ ''')
+ exclude_realms = ListOption('tags', 'exclude_realms', [],
+ doc='Comma-separated list of realms to exclude '
+ 'from queries, unless specifically included, '
+ 'using "realm:realm-name" in the query.')
# INavigationContributor methods
def get_active_navigation_item(self, req):
@@ -83,6 +103,10 @@
)
realms = [p.get_taggable_realm() for p in self.tag_providers]
+ if 0 == len(req.args) or ('q' in req.args and 1 == len(req.args)):
+ for realm in realms:
+ if not realm in self.exclude_realms:
+ req.args[realm] = 'on'
checked_realms = [r for r in realms if r in req.args] or realms
data['tag_realms'] = [{'name': realm, 'checked': realm in checked_realms}
for realm in realms]
@@ -107,6 +131,8 @@
if r in checked_realms]), query)
self.env.log.debug('Tag query: %s', query)
try:
+ if 'ListTagged' == macro:
+ query += ',format=%s,cols=%s' % (self.default_format, self.default_cols)
data['tag_body'] = macros.expand_macro(formatter, macro, query)
except InvalidQuery, e:
data['tag_query_error'] = to_unicode(e)
Index: tractags/wiki.py
===================================================================
--- tractags/wiki.py (revision 10506)
+++ tractags/wiki.py (working copy)
@@ -25,7 +25,7 @@
"""Tag provider for the Wiki."""
realm = 'wiki'
- first_head = re.compile('=\s+([^=]*)=')
+ first_head = re.compile('=\s+([^=\n]*)={0,1}')
def check_permission(self, perm, operation):
map = {'view': 'WIKI_VIEW', 'modify': 'WIKI_MODIFY'}
@@ -85,6 +85,13 @@
def wiki_page_changed(self, page, version, t, comment, author, ipnr):
pass
+ def wiki_page_renamed(self, page, old_name):
+ new_resource = Resource('wiki', page.name)
+ old_resource = Resource('wiki', old_name)
+ self.log.debug("Moving tags from %s to %s", old_resource.id, new_resource.id)
+ tag_system = TagSystem(self.env)
+ tag_system.rename_resource(old_resource, new_resource)
+
def wiki_page_deleted(self, page):
tag_system = TagSystem(self.env)
# XXX Ugh. Hopefully this will be sufficient to full any endpoints.
Index: tractags/macros.py
===================================================================
--- tractags/macros.py (revision 10506)
+++ tractags/macros.py (working copy)
@@ -9,12 +9,13 @@
from genshi.builder import tag as builder
from trac.core import Component, implements
-from trac.resource import Resource, get_resource_url, render_resource_link
+from trac.resource import Resource, get_resource_url, render_resource_link, get_resource_description
from trac.util import embedded_numbers
from trac.util.compat import sorted, set
from trac.util.text import to_unicode
+from trac.config import Option, ChoiceOption, ListOption
from trac.web.chrome import add_stylesheet
-from trac.wiki import IWikiMacroProvider
+from trac.wiki.api import IWikiMacroProvider, parse_args
from tractags.api import TagSystem, _
@@ -62,6 +63,26 @@
"""Provides macros, that utilize the tagging system in wiki."""
implements(IWikiMacroProvider)
+
+ default_format = ChoiceOption('tags', 'default_listtagged_format',
+ ['oldlist', 'table', 'short'],
+ '''Set the default format for ListTagged calls.
+
+ || `oldlist` (default value) || The original format \
+ with a bulleted-list of "linked-id description (tags)" ||
+ || `short` || Bulleted-list of "linked-description" ||
+ || `table` || Table... (see column option) ||''')
+ default_cols = Option('tags', 'default_listtaged_table_cols',
+ 'id|description|tags',
+ '''Select columns and order for table format using a
+ "|"-separated list of column names.
+
+ Supported columns: realm, id, description, tags
+ ''')
+ exclude_realms = ListOption('tags', 'listtaged_exclude_realms', [],
+ doc='Comma-separated list of realms to exclude '
+ 'from queries, unless specifically included, '
+ 'using "realm:realm-name" in the query.')
def __init__(self):
# TRANSLATOR: Keep macro doc style formatting here, please.
@@ -87,6 +108,8 @@
See tags documentation for the query syntax.
""")
+
+ self.supported_cols = set(['realm', 'id', 'description', 'tags'])
# IWikiMacroProvider
@@ -109,16 +132,37 @@
return render_cloud(self.env, req, all_tags)
elif name == 'ListTagged':
+ args, kw = parse_args(content)
+ format = 'format' in kw and kw['format'] or self.default_format
+ cols = 'cols' in kw and kw['cols'] or self.default_cols
+ query = args and args[0].strip() or None
req = formatter.req
tag_system = TagSystem(self.env)
- query_result = tag_system.query(req, content)
+ realms = [p.get_taggable_realm() for p in tag_system.tag_providers]
+ for realm in self.exclude_realms:
+ import re
+ if not re.search('(^|\W)realm:%s(\W|$)' % (realm), query):
+ realms.remove(realm)
+ if 0 == len(realms):
+ return ''
+ query = '(%s) (%s)' % (query, ' or '.join(['realm:%s' % (r) for r in realms]))
+ query_result = tag_system.query(req, query)
add_stylesheet(req, 'tags/css/tractags.css')
def _link(resource):
- return render_resource_link(self.env, formatter.context,
- resource, 'compact')
-
- ul = builder.ul(class_='taglist')
+ return render_resource_link(self.env, formatter.context, resource,
+ 'oldlist' == format and 'compact' or 'default')
+
+ if 'table' == format:
+ cols = [col.capitalize() for col in cols.split('|')
+ if col.lower() in self.supported_cols]
+ container = builder.table(class_='wiki')
+ thead = builder.thead()
+ for col in cols:
+ thead(builder.th(col))
+ container(thead)
+ else:
+ container = builder.ul(class_='taglist')
for resource, tags in sorted(query_result, key=lambda r: \
embedded_numbers(
to_unicode(r[0].id))):
@@ -131,10 +175,29 @@
_link(resource('tag', tag))
for tag in tags
]
- li = builder.li(_link(resource), ' ', desc, ' (',
- rendered_tags[0], [(' ', tag) for \
- tag in rendered_tags[1:]], ')')
+ if 'oldlist' == format:
+ item = builder.li(_link(resource), ' ', desc, ' (',
+ rendered_tags[0], [(' ', tag) for \
+ tag in rendered_tags[1:]], ')')
+ else:
+ desc = desc or get_resource_description(self.env, resource,
+ context=formatter.context)
+ link = builder.a(desc, href=get_resource_url(self.env, resource,
+ formatter.context.href))
+ if 'table' == format:
+ item = builder.tr()
+ for col in cols:
+ if 'Realm' == col:
+ item(builder.td(resource.realm))
+ elif col in ('ID', 'Id'):
+ item(builder.td(_link(resource)))
+ elif 'Description' == col:
+ item(builder.td(link))
+ elif 'Tags' == col:
+ item(builder.td([(tag,' ') for tag in rendered_tags]))
+ else:
+ item = builder.li(link)
else:
- li = builder.li(_link(resource), ' ', desc)
- ul(li, '\n')
- return ul
+ item = builder.li(_link(resource), ' ', desc)
+ container(item)
+ return container
Index: tractags/templates/tag_change.html
===================================================================
--- tractags/templates/tag_change.html (revision 0)
+++ tractags/templates/tag_change.html (revision 0)
@@ -0,0 +1,32 @@
+
+
+