source: tagsplugin/trunk/tractags/wiki.py

Last change on this file was 18146, checked in by Cinc-th, 2 years ago

TagsPlugin: fixed a string/byte problem with QueryNode. Fixed some tests where we expected a list but got dict_keys instead. Fixed test where sort order of a set was assumed.

Refs #13994

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