| 1 | from trac.core import * |
|---|
| 2 | from trac.wiki.api import IWikiMacroProvider |
|---|
| 3 | from trac.wiki import model |
|---|
| 4 | from trac.util import Markup |
|---|
| 5 | from trac.wiki import wiki_to_html, wiki_to_oneliner |
|---|
| 6 | from StringIO import StringIO |
|---|
| 7 | from tractags.api import TagEngine, ITagSpaceUser |
|---|
| 8 | import inspect |
|---|
| 9 | import re |
|---|
| 10 | import string |
|---|
| 11 | |
|---|
| 12 | try: |
|---|
| 13 | sorted = sorted |
|---|
| 14 | except NameError: |
|---|
| 15 | def sorted(iterable, cmp=None, key=str, reverse=False): |
|---|
| 16 | lst = [(key(i), i) for i in iterable] |
|---|
| 17 | lst.sort() |
|---|
| 18 | if reverse: |
|---|
| 19 | lst = reversed(lst) |
|---|
| 20 | return [i for __, i in lst] |
|---|
| 21 | |
|---|
| 22 | try: |
|---|
| 23 | set = set |
|---|
| 24 | except: |
|---|
| 25 | from sets import Set as set |
|---|
| 26 | |
|---|
| 27 | class TagMacros(Component): |
|---|
| 28 | """ Versions of the old Wiki-only macros using the new tag API. """ |
|---|
| 29 | |
|---|
| 30 | implements(IWikiMacroProvider) |
|---|
| 31 | |
|---|
| 32 | def _page_titles(self, pages): |
|---|
| 33 | """ Extract page titles, if possible. """ |
|---|
| 34 | titles = {} |
|---|
| 35 | tagspace = TagEngine(self.env).tagspace.wiki |
|---|
| 36 | for pagename in pages: |
|---|
| 37 | href, link, title = tagspace.name_details(pagename) |
|---|
| 38 | titles[pagename] = title |
|---|
| 39 | return titles |
|---|
| 40 | |
|---|
| 41 | def _tag_details(self, links, tags): |
|---|
| 42 | """ Extract dictionary of tag:(href, title) for all tags. """ |
|---|
| 43 | for tag in tags: |
|---|
| 44 | if tag not in links: |
|---|
| 45 | links[tag] = TagEngine(self.env).get_tag_link(tag) |
|---|
| 46 | return links |
|---|
| 47 | |
|---|
| 48 | def _current_page(self, req): |
|---|
| 49 | return req.hdf.getValue('wiki.page_name', '') |
|---|
| 50 | |
|---|
| 51 | # IWikiMacroProvider methods |
|---|
| 52 | def get_macros(self): |
|---|
| 53 | yield 'TagCloud' |
|---|
| 54 | yield 'ListTagged' |
|---|
| 55 | yield 'TagIt' |
|---|
| 56 | yield 'ListTags' |
|---|
| 57 | |
|---|
| 58 | def get_macro_description(self, name): |
|---|
| 59 | import pydoc |
|---|
| 60 | return pydoc.getdoc(getattr(self, 'render_' + name.lower())) |
|---|
| 61 | |
|---|
| 62 | def render_macro(self, req, name, content): |
|---|
| 63 | from trac.web.chrome import add_stylesheet |
|---|
| 64 | add_stylesheet(req, 'tags/css/tractags.css') |
|---|
| 65 | # Translate macro args into python args |
|---|
| 66 | args = [] |
|---|
| 67 | kwargs = {} |
|---|
| 68 | if content is not None: |
|---|
| 69 | try: |
|---|
| 70 | from parseargs import parseargs |
|---|
| 71 | args, kwargs = parseargs(content) |
|---|
| 72 | except Exception, e: |
|---|
| 73 | raise TracError("Invalid arguments '%s' (%s %s)" % (content, e.__class__.__name__, e)) |
|---|
| 74 | return getattr(self, 'render_' + name.lower(), content)(req, *args, **kwargs) |
|---|
| 75 | |
|---|
| 76 | # Macro implementations |
|---|
| 77 | def render_tagcloud(self, req, smallest=10, biggest=20, tagspace=None, tagspaces=[]): |
|---|
| 78 | """ Display a summary of all tags, with the font size reflecting the |
|---|
| 79 | number of pages the tag applies to. Font size ranges from 10 to 22 |
|---|
| 80 | pixels, but this can be overridden by the smallest=n and biggest=n |
|---|
| 81 | macro parameters. By default, all tagspaces are displayed, but this |
|---|
| 82 | can be overridden with tagspaces=(wiki, ticket) or tagspace=wiki.""" |
|---|
| 83 | smallest = int(smallest) |
|---|
| 84 | biggest = int(biggest) |
|---|
| 85 | engine = TagEngine(self.env) |
|---|
| 86 | # Get wiki tagspace |
|---|
| 87 | if tagspace: |
|---|
| 88 | tagspaces = [tagspace] |
|---|
| 89 | else: |
|---|
| 90 | tagspaces = tagspaces or engine.tagspaces |
|---|
| 91 | cloud = {} |
|---|
| 92 | |
|---|
| 93 | for tag, names in engine.get_tags(tagspaces=tagspaces, detailed=True).iteritems(): |
|---|
| 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 | out.write('<li%s><a rel="tag" title="%s" style="font-size: %ipx" href="%s">%s</a> <span class="tagcount">(%i)</span></li>\n' % ( |
|---|
| 122 | cls, |
|---|
| 123 | taginfo[tag][1], |
|---|
| 124 | smallest + int(by_count[cloud[tag]] * scale), |
|---|
| 125 | taginfo[tag][0], |
|---|
| 126 | tag, |
|---|
| 127 | cloud[tag])) |
|---|
| 128 | out.write('</ul>\n') |
|---|
| 129 | return out.getvalue() |
|---|
| 130 | |
|---|
| 131 | def render_listtagged(self, req, *tags, **kwargs): |
|---|
| 132 | """ List tagged objects. Takes a list of tags to match against. |
|---|
| 133 | The special tag '.' inserts the current Wiki page name. |
|---|
| 134 | |
|---|
| 135 | Optional keyword arguments are tagspace=wiki, |
|---|
| 136 | tagspaces=(wiki, title, ...) and showheadings=true. |
|---|
| 137 | |
|---|
| 138 | By default displays the intersection of objects matching each tag. |
|---|
| 139 | By passing operation=union this can be modified to display |
|---|
| 140 | the union of objects matching each tag. |
|---|
| 141 | """ |
|---|
| 142 | |
|---|
| 143 | if 'tagspace' in kwargs: |
|---|
| 144 | tagspaces = [kwargs.get('tagspace', None)] |
|---|
| 145 | else: |
|---|
| 146 | tagspaces = kwargs.get('tagspaces', '') or \ |
|---|
| 147 | list(TagEngine(self.env).tagspaces) |
|---|
| 148 | showheadings = kwargs.get('showheadings', 'false') |
|---|
| 149 | operation = kwargs.get('operation', 'intersection') |
|---|
| 150 | if operation not in ('union', 'intersection'): |
|---|
| 151 | raise TracError("Invalid tag set operation '%s'" % operation) |
|---|
| 152 | |
|---|
| 153 | engine = TagEngine(self.env) |
|---|
| 154 | page_name = req.hdf.get('wiki.page_name') |
|---|
| 155 | if page_name: |
|---|
| 156 | tags = [tag == '.' and page_name or tag for tag in tags] |
|---|
| 157 | |
|---|
| 158 | taginfo = {} |
|---|
| 159 | out = StringIO() |
|---|
| 160 | out.write('<ul class="listtagged">') |
|---|
| 161 | for tagspace, tagspace_names in sorted(engine.get_tagged_names(tags=tags, tagspaces=tagspaces, operation=operation, detailed=True).iteritems()): |
|---|
| 162 | if showheadings == 'true': |
|---|
| 163 | out.write('<lh>%s tags</lh>' % tagspace) |
|---|
| 164 | for name, tags in sorted(tagspace_names.iteritems()): |
|---|
| 165 | if tagspace == 'wiki' and unicode(name).startswith('tags/'): continue |
|---|
| 166 | tags = sorted(tags) |
|---|
| 167 | taginfo = self._tag_details(taginfo, tags) |
|---|
| 168 | href, link, title = engine.name_details(tagspace, name) |
|---|
| 169 | htitle = wiki_to_oneliner(title, self.env) |
|---|
| 170 | name_tags = ['<a href="%s" title="%s">%s</a>' |
|---|
| 171 | % (taginfo[tag][0], taginfo[tag][1], tag) |
|---|
| 172 | for tag in tags] |
|---|
| 173 | if not name_tags: |
|---|
| 174 | name_tags = '' |
|---|
| 175 | else: |
|---|
| 176 | name_tags = ' (' + ', '.join(sorted(name_tags)) + ')' |
|---|
| 177 | out.write('<li>%s %s%s</li>\n' % |
|---|
| 178 | (link, htitle, name_tags)) |
|---|
| 179 | out.write('</ul>') |
|---|
| 180 | |
|---|
| 181 | return out.getvalue() |
|---|
| 182 | |
|---|
| 183 | def render_tagit(self, req, *tags): |
|---|
| 184 | """ Tag the current page and display them (deprecated). """ |
|---|
| 185 | return '' |
|---|
| 186 | |
|---|
| 187 | def render_listtags(self, req, *tags, **kwargs): |
|---|
| 188 | """ List tags. For backwards compatibility, can accept a list of tags. |
|---|
| 189 | This will simply call ListTagged. Optional keyword arguments are |
|---|
| 190 | tagspace=wiki, tagspaces=(wiki, ticket, ...) and shownames=true. """ |
|---|
| 191 | if tags: |
|---|
| 192 | # Backwards compatibility |
|---|
| 193 | return self.render_listtagged(req, *tags, **kwargs) |
|---|
| 194 | |
|---|
| 195 | page = self._current_page(req) |
|---|
| 196 | engine = TagEngine(self.env) |
|---|
| 197 | |
|---|
| 198 | showpages = kwargs.get('showpages', None) or kwargs.get('shownames', 'false') |
|---|
| 199 | |
|---|
| 200 | if 'tagspace' in kwargs: |
|---|
| 201 | tagspaces = [kwargs['tagspace']] |
|---|
| 202 | else: |
|---|
| 203 | tagspaces = kwargs.get('tagspaces', []) or \ |
|---|
| 204 | list(TagEngine(self.env).tagspaces) |
|---|
| 205 | |
|---|
| 206 | out = StringIO() |
|---|
| 207 | out.write('<ul class="listtags">\n') |
|---|
| 208 | tag_details = {} |
|---|
| 209 | for tag, names in sorted(engine.get_tags(tagspaces=tagspaces, detailed=True).iteritems()): |
|---|
| 210 | href, title = engine.get_tag_link(tag) |
|---|
| 211 | htitle = wiki_to_oneliner(title, self.env) |
|---|
| 212 | out.write('<li><a href="%s" title="%s">%s</a> %s <span class="tagcount">(%i)</span>' % (href, title, tag, htitle, len(names))) |
|---|
| 213 | if showpages == 'true': |
|---|
| 214 | out.write('\n') |
|---|
| 215 | out.write(self.render_listtagged(req, tag, tagspaces=tagspaces)) |
|---|
| 216 | out.write('</li>\n') |
|---|
| 217 | |
|---|
| 218 | out.write('</ul>\n') |
|---|
| 219 | |
|---|
| 220 | return out.getvalue() |
|---|