source: tagsplugin/tags/0.9/tractags/wiki.py

Last change on this file was 16054, checked in by Jun Omae, 7 years ago

0.9dev: fix unit tests with Trac 1.0 on Python 2.5

File size: 15.1 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006 Alec Thomas <alec@swapoff.org>
4# Copyright (C) 2014 Jun Omae <jun66j5@gmail.com>
5# Copyright (C) 2011-2014 Steffen Hoffmann <hoff.st@web.de>
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution.
9#
10
11from __future__ import with_statement
12
13import re
14
15from genshi.builder import Fragment, Markup, tag
16from genshi.filters.transform import Transformer
17
18from trac.config import BoolOption
19from trac.core import Component, implements
20from trac.resource import Resource, render_resource_link, get_resource_url
21from trac.util.datefmt import to_utimestamp
22from trac.web.api import IRequestFilter, ITemplateStreamFilter
23from trac.web.chrome import add_stylesheet, web_context
24from trac.wiki.api import IWikiChangeListener, IWikiPageManipulator
25from trac.wiki.api import IWikiSyntaxProvider
26from trac.wiki.formatter import format_to_oneliner
27from trac.wiki.model import WikiPage
28from trac.wiki.parser import WikiParser
29from trac.wiki.web_ui import WikiModule
30
31from tractags.api import Counter, DefaultTagProvider, TagSystem, _, requests
32from tractags.macros import TagTemplateProvider
33from tractags.model import delete_tags, tag_changes
34from tractags.web_ui import render_tag_changes
35from tractags.util import MockReq, query_realms, split_into_tags
36
37
38class WikiTagProvider(DefaultTagProvider):
39    """[main] Tag provider for Trac wiki."""
40
41    realm = 'wiki'
42
43    exclude_templates = BoolOption('tags', 'query_exclude_wiki_templates',
44        default=True,
45        doc="Whether tagged wiki page templates should be queried.")
46
47    first_head = re.compile('=\s+([^=\n]*)={0,1}')
48
49    def check_permission(self, perm, action):
50        map = {'view': 'WIKI_VIEW', 'modify': 'WIKI_MODIFY'}
51        return super(WikiTagProvider, self).check_permission(perm, action) \
52            and map[action] in perm
53
54    def get_tagged_resources(self, req, tags=None, filter=None):
55        if self.exclude_templates:
56            with self.env.db_query as db:
57                like_templates = ''.join(
58                    ["'", db.like_escape(WikiModule.PAGE_TEMPLATES_PREFIX),
59                     "%%'"])
60                filter = (' '.join(['name NOT', db.like() % like_templates]),)
61        return super(WikiTagProvider, self).get_tagged_resources(req, tags,
62                                                                 filter)
63
64    def get_all_tags(self, req, filter=None):
65        if not self.check_permission(req.perm, 'view'):
66            return Counter()
67        if self.exclude_templates:
68            with self.env.db_transaction as db:
69                like_templates = ''.join(
70                    ["'", db.like_escape(WikiModule.PAGE_TEMPLATES_PREFIX),
71                     "%%'"])
72                filter = (' '.join(['name NOT', db.like() % like_templates]),)
73        return super(WikiTagProvider, self).get_all_tags(req, filter)
74
75    def describe_tagged_resource(self, req, resource):
76        if not self.check_permission(req.perm(resource), 'view'):
77            return ''
78        page = WikiPage(self.env, resource.id)
79        if page.exists:
80            ret = self.first_head.search(page.text)
81            return ret and ret.group(1) or ''
82        return ''
83
84
85class WikiTagInterface(TagTemplateProvider):
86    """[main] Implements the user interface for tagging Wiki pages."""
87
88    implements(IRequestFilter, ITemplateStreamFilter,
89               IWikiChangeListener, IWikiPageManipulator)
90
91    def __init__(self):
92        self.tag_system = TagSystem(self.env)
93
94    # IRequestFilter methods
95
96    def pre_process_request(self, req, handler):
97        return handler
98
99    def post_process_request(self, req, template, data, content_type):
100        if template is not None:
101            if req.method == 'GET' and req.path_info.startswith('/wiki/'):
102                if req.args.get('action') == 'edit' and \
103                        req.args.get('template') and 'tags' not in req.args:
104                    self._post_process_request_edit(req)
105                if req.args.get('action') == 'history' and \
106                        data and 'history' in data:
107                    self._post_process_request_history(req, data)
108            elif req.method == 'POST' and \
109                    req.path_info.startswith('/wiki/') and \
110                    'save' in req.args:
111                requests.reset()
112        return template, data, content_type
113
114    # ITemplateStreamFilter methods
115    def filter_stream(self, req, method, filename, stream, data):
116        page_name = req.args.get('page', 'WikiStart')
117        resource = Resource('wiki', page_name)
118        if filename == 'wiki_view.html' and 'TAGS_VIEW' in req.perm(resource):
119            return self._wiki_view(req, stream)
120        elif filename == 'wiki_edit.html' and \
121                         'TAGS_MODIFY' in req.perm(resource):
122            return self._wiki_edit(req, stream)
123        elif filename == 'history_view.html' and \
124                         'TAGS_VIEW' in req.perm(resource):
125            return self._wiki_history(req, stream)
126        return stream
127
128    # IWikiPageManipulator methods
129    def prepare_wiki_page(self, req, page, fields):
130        pass
131
132    def validate_wiki_page(self, req, page):
133        # If we're saving the wiki page, and can modify tags, do so.
134        if req and 'TAGS_MODIFY' in req.perm(page.resource) \
135                and req.path_info.startswith('/wiki') and 'save' in req.args:
136            page_modified = req.args.get('text') != page.old_text or \
137                    page.readonly != int('readonly' in req.args)
138            if page_modified:
139                requests.set(req)
140                req.add_redirect_listener(self._redirect_listener)
141            elif page.version > 0:
142                # If the page hasn't been otherwise modified, save tags and
143                # redirect to avoid the "page has not been modified" warning.
144                if self._update_tags(req, page):
145                    req.redirect(get_resource_url(self.env, page.resource,
146                                                  req.href, version=None))
147        return []
148
149    # IWikiChangeListener methods
150    def wiki_page_added(self, page):
151        req = requests.get()
152        if req:
153            self._update_tags(req, page, page.time)
154
155    def wiki_page_changed(self, page, version, t, comment, author, ipnr):
156        req = requests.get()
157        if req:
158            self._update_tags(req, page, page.time)
159
160    def wiki_page_renamed(self, page, old_name):
161        """Called when a page has been renamed (since Trac 0.12)."""
162        self.log.debug("Moving wiki page tags from %s to %s",
163                       old_name, page.name)
164        req = MockReq()
165        self.tag_system.reparent_tags(req, Resource('wiki', page.name),
166                                      old_name)
167
168    def wiki_page_deleted(self, page):
169        # Page gone, so remove all records on it.
170        delete_tags(self.env, page.resource, purge=True)
171
172    def wiki_page_version_deleted(self, page):
173        pass
174
175    # Internal methods
176    def _page_tags(self, req):
177        pagename = req.args.get('page', 'WikiStart')
178        version = req.args.get('version')
179        tags_version = req.args.get('tags_version')
180
181        page = WikiPage(self.env, pagename, version=version)
182        resource = page.resource
183        if version and not tags_version:
184            tags_version = page.time
185        tags = sorted(self.tag_system.get_tags(req, resource,
186                                               when=tags_version))
187        return tags
188
189    def _redirect_listener(self, req, url, permanent):
190        requests.reset()
191
192    def _post_process_request_edit(self, req):
193        # Retrieve template resource to be queried for tags.
194        template_pagename = ''.join([WikiModule.PAGE_TEMPLATES_PREFIX,
195                                     req.args.get('template')])
196        template_page = WikiPage(self.env, template_pagename)
197        if template_page.exists and \
198                'TAGS_VIEW' in req.perm(template_page.resource):
199            tags = sorted(self.tag_system.get_tags(req,
200                                                   template_page.resource))
201            # Prepare tags as content for the editor field.
202            tags_str = ' '.join(tags)
203            self.env.log.debug("Tags retrieved from template: '%s'",
204                               unicode(tags_str).encode('utf-8'))
205            # DEVEL: More arguments need to be propagated here?
206            req.redirect(req.href(req.path_info,
207                                  action='edit', tags=tags_str,
208                                  template=req.args.get('template')))
209
210    def _post_process_request_history(self, req, data):
211        history = []
212        page_histories = data.get('history', [])
213        resource = data['resource']
214        tags_histories = tag_changes(self.env, resource)
215
216        for page_history in page_histories:
217            while tags_histories and \
218                    tags_histories[0][0] >= page_history['date']:
219                tags_history = tags_histories.pop(0)
220                date = tags_history[0]
221                author = tags_history[1]
222                comment = render_tag_changes(tags_history[2], tags_history[3])
223                url = req.href(resource.realm, resource.id,
224                               version=page_history['version'],
225                               tags_version=to_utimestamp(date))
226                history.append({'version': '*', 'url': url, 'date': date,
227                                'author': author, 'comment': comment,
228                                'ipnr': ''})
229            history.append(page_history)
230
231        data.update(dict(history=history,
232                         wiki_to_oneliner=self._wiki_to_oneliner))
233
234    def _wiki_view(self, req, stream):
235        add_stylesheet(req, 'tags/css/tractags.css')
236        tags = self._page_tags(req)
237        if not tags:
238            return stream
239        li = []
240        for tag_ in tags:
241            resource = Resource('tag', tag_)
242            anchor = render_resource_link(self.env,
243                                          web_context(req, resource),
244                                          resource)
245            anchor = anchor(rel='tag')
246            li.append(tag.li(anchor, ' '))
247
248        # TRANSLATOR: Header label text for tag list at wiki page bottom.
249        insert = tag.ul(class_='tags')(tag.li(_("Tags"), class_='header'), li)
250        return stream | (Transformer('//div[contains(@class,"wikipage")]')
251                         .after(insert))
252
253    def _update_tags(self, req, page, when=None):
254        newtags = split_into_tags(req.args.get('tags', ''))
255        oldtags = self.tag_system.get_tags(req, page.resource)
256
257        if oldtags != newtags:
258            self.tag_system.set_tags(req, page.resource, newtags, when=when)
259            return True
260        return False
261
262    def _wiki_edit(self, req, stream):
263        # TRANSLATOR: Label text for link to '/tags'.
264        link = tag.a(_("view all tags"), href=req.href.tags())
265        # TRANSLATOR: ... (view all tags)
266        insert = tag(Markup(_("Tag under: (%(tags_link)s)", tags_link=link)))
267        insert(
268            tag.br(),
269            tag.input(id='tags', type='text', name='tags', size='50',
270                value=req.args.get('tags', ' '.join(self._page_tags(req))))
271        )
272        insert = tag.div(tag.label(insert), class_='field')
273        return stream | Transformer('//div[@id="changeinfo1"]').append(insert)
274
275    def _wiki_history(self, req, stream):
276        xpath = '//input[@type="radio" and @value="*"]'
277        stream = stream | Transformer(xpath).remove()
278        # Remove invalid links to wiki page revisions (fix for Trac < 0.12).
279        xpath = '//a[contains(@href,"%2A")]'
280        return stream | Transformer(xpath).remove()
281
282    def _wiki_to_oneliner(self, context, wiki, shorten=None):
283        if isinstance(wiki, Fragment):
284            return wiki
285        return format_to_oneliner(self.env, context, wiki, shorten=shorten)
286
287
288class TagWikiSyntaxProvider(Component):
289    """[opt] Provides tag:<expr> links.
290
291    This extends TracLinks via WikiFormatting to point at tag queries or
292    at specific tags.
293    """
294
295    implements(IWikiSyntaxProvider)
296
297    def __init__(self):
298        self.tag_system = TagSystem(self.env)
299
300    # IWikiSyntaxProvider methods
301
302    def get_wiki_syntax(self):
303        """Additional syntax for quoted tags or tag expression."""
304        tag_expr = (
305            r"(%s)" % (WikiParser.QUOTED_STRING)
306            )
307
308        # Simple (tag|tagged):link syntax
309        yield (r'''(?P<qualifier>tag(?:ged)?):(?P<tag_expr>%s)''' % tag_expr,
310               lambda f, ns, match: self._format_tagged(
311                   f, match.group('qualifier'), match.group('tag_expr'),
312                   '%s:%s' % (match.group('qualifier'),
313                              match.group('tag_expr'))))
314
315        # [(tag|tagged):link with label]
316        yield (r'''\[tag(?:ged)?:'''
317               r'''(?P<ltag_expr>%s)\s*(?P<tag_title>[^\]]+)?\]''' % tag_expr,
318               lambda f, ns, match: self._format_tagged(f, 'tag',
319                                    match.group('ltag_expr'),
320                                    match.group('tag_title')))
321
322    def get_link_resolvers(self):
323        return [('tag', self._format_tagged),
324                ('tagged', self._format_tagged)]
325
326    def _format_tagged(self, formatter, ns, target, label, fullmatch=None):
327        """Tag and tag query expression link formatter."""
328
329        def unquote(text):
330            """Strip all matching pairs of outer quotes from string."""
331            while re.match(WikiParser.QUOTED_STRING, text):
332                # Remove outer whitespace after stripped quotation too.
333                text = text[1:-1].strip()
334            return text
335
336        label = label and unquote(label.strip()) or ''
337        target = unquote(target.strip())
338
339        query = target
340        # Pop realms from query expression.
341        all_realms = self.tag_system.get_taggable_realms(formatter.perm)
342        realms = query_realms(target, all_realms)
343        if realms:
344            kwargs = dict((realm, 'on') for realm in realms)
345            target = re.sub('(^|\W)realm:\S+(\W|$)', ' ', target).strip()
346        else:
347            kwargs = {}
348
349        tag_res = Resource('tag', target)
350        if 'TAGS_VIEW' not in formatter.perm(tag_res):
351            return tag.span(label, class_='forbidden tags',
352                            title=_("no permission to view tags"))
353
354        context = formatter.context
355        href = self.tag_system.get_resource_url(tag_res, context.href, kwargs)
356        if all_realms and (
357                target in self.tag_system.get_all_tags(formatter.req) or
358                not iter_is_empty(self.tag_system.query(formatter.req,
359                                                        query))):
360            # At least one tag provider is available and tag exists or
361            # tags query yields at least one match.
362            if label:
363                return tag.a(label, href=href)
364            return render_resource_link(self.env, context, tag_res)
365
366        return tag.a(label+'?', href=href, class_='missing tags',
367                     rel='nofollow')
368
369
370def iter_is_empty(i):
371    """Test for empty iterator without consuming more than first element."""
372    try:
373        i.next()
374    except StopIteration:
375        return True
376    return False
Note: See TracBrowser for help on using the repository browser.