source: tagsplugin/branches/0.8-stable/tractags/wiki.py

Last change on this file was 14942, checked in by Ryan J Ollos, 8 years ago

0.8dev: Skip post_process_request during exception handling.

Refs #12486.

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