source: tagsplugin/tags/0.3.1/tractags/macros.py

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