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

Last change on this file was 2921, checked in by Alec Thomas, 16 years ago

Lots more 0.11 fixes thanks to osimons. Also made expression parser much more robust, adding support for normal Python "and", "or" and "not" operators and fixing the two-tag limit.

File size: 11.0 KB
Line 
1from trac.core import *
2from trac.wiki.api import IWikiMacroProvider
3from trac.wiki import model
4from trac.wiki import wiki_to_html, wiki_to_oneliner
5from StringIO import StringIO
6from tractags.api import TagEngine, ITagSpaceUser, sorted, set
7import inspect
8import re
9import string
10
11class 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        # FIXME Butt ugly - should be formatter.context.resource.id or something
34        return req.path_info.rstrip('/').split('/')[-1]
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 expand_macro(self, formatter, name, content):
48        from trac.web.chrome import add_stylesheet
49        from parseargs import parseargs
50        add_stylesheet(formatter.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)(formatter.req, *args, **kwargs)
65
66    # Macro implementations
67    def render_tagcloud(self, req, smallest=10, biggest=20, showcount=True, tagspace=None, mincount=1, tagspaces=[], **kwargs):
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 = self._current_page(req)
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">\n')
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                self.env.log.debug(repr(expr))
190            except Exception, e:
191                self.env.log.error("Invalid expression '%s'" % expression, exc_info=True)
192                tags.update([x.strip() for x in re.split('[+,]', expression) if x.strip()])
193                expression = None
194            else:
195                self.env.log.debug(expr.ast)
196                tagged_names = {}
197                tags.update(expr.get_tags())
198                for tagspace, name, name_tags in engine.walk_tagged_names(tags=tags,
199                        tagspaces=tagspaces, predicate=lambda ts, n, t: expr(t)):
200                    tagged_names.setdefault(tagspace, {})[name] = name_tags
201                tagged_names = [(tagspace, names) for tagspace, names in tagged_names.iteritems()]
202
203        if not expression:
204            tagged_names = [(tagspace, names) for tagspace, names in
205                            engine.get_tagged_names(tags=tags, tagspaces=tagspaces,
206                                operation=operation, detailed=True).iteritems()
207                            if names]
208
209        for tagspace, tagspace_names in sorted(tagged_names):
210            if showheadings == 'true':
211                out.write('<lh>%s tags</lh>' % tagspace)
212            for name, tags in sorted(tagspace_names.iteritems()):
213                if tagspace == 'wiki' and unicode(name).startswith('tags/'): continue
214                tags = sorted(tags)
215                taginfo = self._tag_details(taginfo, tags)
216                href, link, title = engine.name_details(tagspace, name)
217                htitle = wiki_to_oneliner(title, self.env, req=req)
218                name_tags = ['<a href="%s" title="%s">%s</a>'
219                              % (taginfo[tag][0], taginfo[tag][1], tag)
220                              for tag in tags]
221                if not name_tags:
222                    name_tags = ''
223                else:
224                    name_tags = ' (' + ', '.join(sorted(name_tags)) + ')'
225                out.write('<li>%s %s%s</li>\n' %
226                          (link, htitle, name_tags))
227        out.write('</ul>')
228
229        return out.getvalue()
230
231    def render_tagit(self, req, *tags):
232        """ '''''Deprecated. Does nothing.''''' """
233        return ''
234
235    def render_listtags(self, req, *tags, **kwargs):
236        """ List all tags.
237
238            ||'''Argument'''||'''Description'''||
239            ||`tagspace=<tagspace>`||Specify the tagspace the macro should operate on.||
240            ||`tagspaces=(<tagspace>,...)`||Specify a set of tagspaces the macro should operate on.||
241            ||`shownames=true|false`||Whether to show the objects that tags appear on ''(long)''.||
242            """
243
244        if tags:
245            # Backwards compatibility
246            return self.render_listtagged(req, *tags, **kwargs)
247
248        page = self._current_page(req)
249        engine = TagEngine(self.env)
250
251        showpages = kwargs.get('showpages', None) or kwargs.get('shownames', 'false')
252
253        if 'tagspace' in kwargs:
254            tagspaces = [kwargs['tagspace']]
255        else:
256            tagspaces = kwargs.get('tagspaces', []) or \
257                        list(TagEngine(self.env).tagspaces)
258
259        out = StringIO()
260        out.write('<ul class="listtags">\n')
261        tag_details = {}
262        for tag, names in sorted(engine.get_tags(tagspaces=tagspaces, detailed=True).iteritems()):
263            href, title = engine.get_tag_link(tag)
264            htitle = wiki_to_oneliner(title, self.env, req=req)
265            out.write('<li><a href="%s" title="%s">%s</a> %s <span class="tagcount">(%i)</span>' % (href, title, tag, htitle, len(names)))
266            if showpages == 'true':
267                out.write('\n')
268                out.write(self.render_listtagged(req, tag, tagspaces=tagspaces))
269                out.write('</li>\n')
270
271        out.write('</ul>\n')
272
273        return out.getvalue()
Note: See TracBrowser for help on using the repository browser.