| 1 | from trac.core import * |
|---|
| 2 | from trac.wiki.api import IWikiMacroProvider |
|---|
| 3 | from trac.wiki import model |
|---|
| 4 | from trac.wiki import wiki_to_html, wiki_to_oneliner |
|---|
| 5 | from StringIO import StringIO |
|---|
| 6 | from tractags.api import TagEngine, ITagSpaceUser, sorted, set |
|---|
| 7 | import inspect |
|---|
| 8 | import re |
|---|
| 9 | import string |
|---|
| 10 | |
|---|
| 11 | class TagMacros(Component): |
|---|
| 12 | """ Versions of the old Wiki-only macros using the new tag API. """ |
|---|
| 13 | |
|---|
| 14 | implements(IWikiMacroProvider) |
|---|
| 15 | |
|---|
| 16 | def _page_titles(self, pages): |
|---|
| 17 | """ Extract page titles, if possible. """ |
|---|
| 18 | titles = {} |
|---|
| 19 | tagspace = TagEngine(self.env).tagspace.wiki |
|---|
| 20 | for pagename in pages: |
|---|
| 21 | href, link, title = tagspace.name_details(pagename) |
|---|
| 22 | titles[pagename] = title |
|---|
| 23 | return titles |
|---|
| 24 | |
|---|
| 25 | def _tag_details(self, links, tags): |
|---|
| 26 | """ Extract dictionary of tag:(href, title) for all tags. """ |
|---|
| 27 | for tag in tags: |
|---|
| 28 | if tag not in links: |
|---|
| 29 | links[tag] = TagEngine(self.env).get_tag_link(tag) |
|---|
| 30 | return links |
|---|
| 31 | |
|---|
| 32 | def _current_page(self, req): |
|---|
| 33 | return req.hdf.getValue('wiki.page_name', '') |
|---|
| 34 | |
|---|
| 35 | # IWikiMacroProvider methods |
|---|
| 36 | def get_macros(self): |
|---|
| 37 | yield 'TagCloud' |
|---|
| 38 | yield 'ListTagged' |
|---|
| 39 | yield 'TagIt' |
|---|
| 40 | yield 'ListTags' |
|---|
| 41 | |
|---|
| 42 | def get_macro_description(self, name): |
|---|
| 43 | import pydoc |
|---|
| 44 | return pydoc.getdoc(getattr(self, 'render_' + name.lower())) |
|---|
| 45 | |
|---|
| 46 | def render_macro(self, req, name, content): |
|---|
| 47 | from trac.web.chrome import add_stylesheet |
|---|
| 48 | from parseargs import parseargs |
|---|
| 49 | add_stylesheet(req, 'tags/css/tractags.css') |
|---|
| 50 | # Translate macro args into python args |
|---|
| 51 | args = [] |
|---|
| 52 | kwargs = {} |
|---|
| 53 | try: |
|---|
| 54 | # Set default args from config |
|---|
| 55 | _, config_args = parseargs(self.env.config.get('tags', '%s.args' % name.lower(), '')) |
|---|
| 56 | kwargs.update(config_args) |
|---|
| 57 | if content is not None: |
|---|
| 58 | args, macro_args = parseargs(content) |
|---|
| 59 | kwargs.update(macro_args) |
|---|
| 60 | except Exception, e: |
|---|
| 61 | raise TracError("Invalid arguments '%s' (%s %s)" % (content, e.__class__.__name__, e)) |
|---|
| 62 | |
|---|
| 63 | return getattr(self, 'render_' + name.lower(), content)(req, *args, **kwargs) |
|---|
| 64 | |
|---|
| 65 | # Macro implementations |
|---|
| 66 | def render_tagcloud(self, req, smallest=10, biggest=20, showcount=True, tagspace=None, mincount=1, tagspaces=[]): |
|---|
| 67 | """ This macro displays a [http://en.wikipedia.org/wiki/Tag_cloud tag cloud] (weighted list) |
|---|
| 68 | of all tags. |
|---|
| 69 | |
|---|
| 70 | ||'''Argument'''||'''Description'''|| |
|---|
| 71 | ||`tagspace=<tagspace>`||Specify the tagspace the macro should operate on.|| |
|---|
| 72 | ||`tagspaces=(<tagspace>,...)`||Specify a set of tagspaces the macro should operate on.|| |
|---|
| 73 | ||`smallest=<n>`||The lower bound of the font size for the tag cloud.|| |
|---|
| 74 | ||`biggest=<n>`||The upper bound of the font size for the tag cloud.|| |
|---|
| 75 | ||`showcount=true|false`||Show the count of objects for each tag?|| |
|---|
| 76 | ||`mincount=<n>`||Hide tags with a count less than `<n>`.|| |
|---|
| 77 | """ |
|---|
| 78 | |
|---|
| 79 | smallest = int(smallest) |
|---|
| 80 | biggest = int(biggest) |
|---|
| 81 | mincount = int(mincount) |
|---|
| 82 | |
|---|
| 83 | engine = TagEngine(self.env) |
|---|
| 84 | # Get wiki tagspace |
|---|
| 85 | if tagspace: |
|---|
| 86 | tagspaces = [tagspace] |
|---|
| 87 | else: |
|---|
| 88 | tagspaces = tagspaces or engine.tagspaces |
|---|
| 89 | cloud = {} |
|---|
| 90 | |
|---|
| 91 | for tag, names in engine.get_tags(tagspaces=tagspaces, detailed=True).iteritems(): |
|---|
| 92 | count = len(names) |
|---|
| 93 | if count >= mincount: |
|---|
| 94 | cloud[tag] = len(names) |
|---|
| 95 | |
|---|
| 96 | tags = cloud.keys() |
|---|
| 97 | |
|---|
| 98 | # No tags? |
|---|
| 99 | if not tags: return '' |
|---|
| 100 | |
|---|
| 101 | # by_count maps tag counts to an index in the set of counts |
|---|
| 102 | by_count = list(set(cloud.values())) |
|---|
| 103 | by_count.sort() |
|---|
| 104 | by_count = dict([(c, float(i)) for i, c in enumerate(by_count)]) |
|---|
| 105 | |
|---|
| 106 | taginfo = self._tag_details({}, tags) |
|---|
| 107 | tags.sort() |
|---|
| 108 | rlen = float(biggest - smallest) |
|---|
| 109 | tlen = float(len(by_count)) |
|---|
| 110 | scale = 1.0 |
|---|
| 111 | if tlen: |
|---|
| 112 | scale = rlen / tlen |
|---|
| 113 | out = StringIO() |
|---|
| 114 | out.write('<ul class="tagcloud">\n') |
|---|
| 115 | last = tags[-1] |
|---|
| 116 | for tag in tags: |
|---|
| 117 | if tag == last: |
|---|
| 118 | cls = ' class="last"' |
|---|
| 119 | else: |
|---|
| 120 | cls = '' |
|---|
| 121 | if showcount != 'false': |
|---|
| 122 | count = ' <span class="tagcount">(%i)</span>' % cloud[tag] |
|---|
| 123 | else: |
|---|
| 124 | count = '' |
|---|
| 125 | out.write('<li%s><a rel="tag" title="%s" style="font-size: %ipx" href="%s">%s</a>%s</li>\n' % ( |
|---|
| 126 | cls, |
|---|
| 127 | taginfo[tag][1] + ' (%i)' % cloud[tag], |
|---|
| 128 | smallest + int(by_count[cloud[tag]] * scale), |
|---|
| 129 | taginfo[tag][0], |
|---|
| 130 | tag, |
|---|
| 131 | count)) |
|---|
| 132 | out.write('</ul>\n') |
|---|
| 133 | return out.getvalue() |
|---|
| 134 | |
|---|
| 135 | def render_listtagged(self, req, *tags, **kwargs): |
|---|
| 136 | """ List tagged objects. Optionally accepts a list of tags to match |
|---|
| 137 | against. The special tag '''. (dot)''' inserts the current Wiki page name. |
|---|
| 138 | |
|---|
| 139 | `[[ListTagged(<tag>, ...)]]` |
|---|
| 140 | |
|---|
| 141 | ||'''Argument'''||'''Description'''|| |
|---|
| 142 | ||`tagspace=<tagspace>`||Specify the tagspace the macro should operate on.|| |
|---|
| 143 | ||`tagspaces=(<tagspace>,...)`||Specify a set of tagspaces the macro should operate on.|| |
|---|
| 144 | ||`operation=intersection|union`||The set operation to perform on the discovered objects.|| |
|---|
| 145 | ||`showheadings=true|false`||List objects under the tagspace they occur in.|| |
|---|
| 146 | ||`expression=<expr>`||Match object tags against the given expression.|| |
|---|
| 147 | |
|---|
| 148 | The supported expression operators are: unary - (not); binary +, - |
|---|
| 149 | and | (and, and not, or). All values in the expression are treated |
|---|
| 150 | as tags. Any tag not in the same form as a Python variable must be |
|---|
| 151 | quoted. |
|---|
| 152 | |
|---|
| 153 | eg. Match all objects tagged with ticket and workflow, and not |
|---|
| 154 | tagged with wiki or closed. |
|---|
| 155 | |
|---|
| 156 | (ticket+workflow)-(wiki|closed) |
|---|
| 157 | |
|---|
| 158 | If an expression is provided operation is ignored. |
|---|
| 159 | """ |
|---|
| 160 | |
|---|
| 161 | if 'tagspace' in kwargs: |
|---|
| 162 | tagspaces = [kwargs.get('tagspace', None)] |
|---|
| 163 | else: |
|---|
| 164 | tagspaces = kwargs.get('tagspaces', '') or \ |
|---|
| 165 | list(TagEngine(self.env).tagspaces) |
|---|
| 166 | expression = kwargs.get('expression', None) |
|---|
| 167 | showheadings = kwargs.get('showheadings', 'false') |
|---|
| 168 | operation = kwargs.get('operation', 'intersection') |
|---|
| 169 | if operation not in ('union', 'intersection'): |
|---|
| 170 | raise TracError("Invalid tag set operation '%s'" % operation) |
|---|
| 171 | |
|---|
| 172 | engine = TagEngine(self.env) |
|---|
| 173 | page_name = req.hdf.get('wiki.page_name') |
|---|
| 174 | if page_name: |
|---|
| 175 | tags = [tag == '.' and page_name or tag for tag in tags] |
|---|
| 176 | |
|---|
| 177 | tags = set(tags) |
|---|
| 178 | taginfo = {} |
|---|
| 179 | out = StringIO() |
|---|
| 180 | out.write('<ul class="listtagged">\n') |
|---|
| 181 | # If expression was passed as an argument, do a full walk, using the |
|---|
| 182 | # expression as the predicate. Silently assumes that failed expressions |
|---|
| 183 | # are normal tags. |
|---|
| 184 | if expression: |
|---|
| 185 | from tractags.expr import Expression |
|---|
| 186 | try: |
|---|
| 187 | expr = Expression(expression) |
|---|
| 188 | except Exception, e: |
|---|
| 189 | self.env.log.error("Invalid expression '%s'" % expression, exc_info=True) |
|---|
| 190 | tags.update([x.strip() for x in re.split('[+,]', expression) if x.strip()]) |
|---|
| 191 | expression = None |
|---|
| 192 | else: |
|---|
| 193 | self.env.log.debug(expr.ast) |
|---|
| 194 | tagged_names = {} |
|---|
| 195 | tags.update(expr.get_tags()) |
|---|
| 196 | for tagspace, name, name_tags in engine.walk_tagged_names(tags=tags, |
|---|
| 197 | tagspaces=tagspaces, predicate=lambda ts, n, t: expr(t)): |
|---|
| 198 | tagged_names.setdefault(tagspace, {})[name] = name_tags |
|---|
| 199 | tagged_names = [(tagspace, names) for tagspace, names in tagged_names.iteritems()] |
|---|
| 200 | |
|---|
| 201 | if not expression: |
|---|
| 202 | tagged_names = [(tagspace, names) for tagspace, names in |
|---|
| 203 | engine.get_tagged_names(tags=tags, tagspaces=tagspaces, |
|---|
| 204 | operation=operation, detailed=True).iteritems() |
|---|
| 205 | if names] |
|---|
| 206 | |
|---|
| 207 | for tagspace, tagspace_names in sorted(tagged_names): |
|---|
| 208 | if showheadings == 'true': |
|---|
| 209 | out.write('<lh>%s tags</lh>' % tagspace) |
|---|
| 210 | for name, tags in sorted(tagspace_names.iteritems()): |
|---|
| 211 | if tagspace == 'wiki' and unicode(name).startswith('tags/'): continue |
|---|
| 212 | tags = sorted(tags) |
|---|
| 213 | taginfo = self._tag_details(taginfo, tags) |
|---|
| 214 | href, link, title = engine.name_details(tagspace, name) |
|---|
| 215 | htitle = wiki_to_oneliner(title, self.env) |
|---|
| 216 | name_tags = ['<a href="%s" title="%s">%s</a>' |
|---|
| 217 | % (taginfo[tag][0], taginfo[tag][1], tag) |
|---|
| 218 | for tag in tags] |
|---|
| 219 | if not name_tags: |
|---|
| 220 | name_tags = '' |
|---|
| 221 | else: |
|---|
| 222 | name_tags = ' (' + ', '.join(sorted(name_tags)) + ')' |
|---|
| 223 | out.write('<li>%s %s%s</li>\n' % |
|---|
| 224 | (link, htitle, name_tags)) |
|---|
| 225 | out.write('</ul>') |
|---|
| 226 | |
|---|
| 227 | return out.getvalue() |
|---|
| 228 | |
|---|
| 229 | def render_tagit(self, req, *tags): |
|---|
| 230 | """ '''''Deprecated. Does nothing.''''' """ |
|---|
| 231 | return '' |
|---|
| 232 | |
|---|
| 233 | def render_listtags(self, req, *tags, **kwargs): |
|---|
| 234 | """ List all tags. |
|---|
| 235 | |
|---|
| 236 | ||'''Argument'''||'''Description'''|| |
|---|
| 237 | ||`tagspace=<tagspace>`||Specify the tagspace the macro should operate on.|| |
|---|
| 238 | ||`tagspaces=(<tagspace>,...)`||Specify a set of tagspaces the macro should operate on.|| |
|---|
| 239 | ||`shownames=true|false`||Whether to show the objects that tags appear on ''(long)''.|| |
|---|
| 240 | """ |
|---|
| 241 | |
|---|
| 242 | if tags: |
|---|
| 243 | # Backwards compatibility |
|---|
| 244 | return self.render_listtagged(req, *tags, **kwargs) |
|---|
| 245 | |
|---|
| 246 | page = self._current_page(req) |
|---|
| 247 | engine = TagEngine(self.env) |
|---|
| 248 | |
|---|
| 249 | showpages = kwargs.get('showpages', None) or kwargs.get('shownames', 'false') |
|---|
| 250 | |
|---|
| 251 | if 'tagspace' in kwargs: |
|---|
| 252 | tagspaces = [kwargs['tagspace']] |
|---|
| 253 | else: |
|---|
| 254 | tagspaces = kwargs.get('tagspaces', []) or \ |
|---|
| 255 | list(TagEngine(self.env).tagspaces) |
|---|
| 256 | |
|---|
| 257 | out = StringIO() |
|---|
| 258 | out.write('<ul class="listtags">\n') |
|---|
| 259 | tag_details = {} |
|---|
| 260 | for tag, names in sorted(engine.get_tags(tagspaces=tagspaces, detailed=True).iteritems()): |
|---|
| 261 | href, title = engine.get_tag_link(tag) |
|---|
| 262 | htitle = wiki_to_oneliner(title, self.env) |
|---|
| 263 | out.write('<li><a href="%s" title="%s">%s</a> %s <span class="tagcount">(%i)</span>' % (href, title, tag, htitle, len(names))) |
|---|
| 264 | if showpages == 'true': |
|---|
| 265 | out.write('\n') |
|---|
| 266 | out.write(self.render_listtagged(req, tag, tagspaces=tagspaces)) |
|---|
| 267 | out.write('</li>\n') |
|---|
| 268 | |
|---|
| 269 | out.write('</ul>\n') |
|---|
| 270 | |
|---|
| 271 | return out.getvalue() |
|---|