| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2006 Alec Thomas <alec@swapoff.org> |
|---|
| 4 | # Copyright (C) 2011 Itamar Ostricher <itamarost@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 | |
|---|
| 11 | import re |
|---|
| 12 | import math |
|---|
| 13 | |
|---|
| 14 | from genshi.builder import tag as builder |
|---|
| 15 | from trac.config import ListOption, Option |
|---|
| 16 | from trac.core import ExtensionPoint, implements |
|---|
| 17 | from trac.mimeview import Context |
|---|
| 18 | from trac.resource import Resource |
|---|
| 19 | from trac.util import to_unicode |
|---|
| 20 | from trac.util.text import CRLF |
|---|
| 21 | from trac.web.api import IRequestHandler |
|---|
| 22 | from trac.web.chrome import INavigationContributor |
|---|
| 23 | from trac.web.chrome import add_stylesheet, add_ctxtnav |
|---|
| 24 | from trac.wiki.formatter import Formatter |
|---|
| 25 | from trac.wiki.model import WikiPage |
|---|
| 26 | |
|---|
| 27 | from tractags.api import TagSystem, ITagProvider, _, tag_ |
|---|
| 28 | from tractags.macros import TagTemplateProvider, TagWikiMacros, as_int |
|---|
| 29 | from tractags.macros import query_realms |
|---|
| 30 | from tractags.query import InvalidQuery |
|---|
| 31 | |
|---|
| 32 | |
|---|
| 33 | class TagRequestHandler(TagTemplateProvider): |
|---|
| 34 | """Implements the /tags handler.""" |
|---|
| 35 | |
|---|
| 36 | implements(INavigationContributor, IRequestHandler) |
|---|
| 37 | |
|---|
| 38 | tag_providers = ExtensionPoint(ITagProvider) |
|---|
| 39 | |
|---|
| 40 | cloud_mincount = Option('tags', 'cloud_mincount', 1, |
|---|
| 41 | doc="""Integer threshold to hide tags with smaller count.""") |
|---|
| 42 | default_cols = Option('tags', 'default_table_cols', 'id|description|tags', |
|---|
| 43 | doc="""Select columns and order for table format using a "|"-separated |
|---|
| 44 | list of column names. |
|---|
| 45 | |
|---|
| 46 | Supported columns: realm, id, description, tags |
|---|
| 47 | """) |
|---|
| 48 | default_format = Option('tags', 'default_format', 'oldlist', |
|---|
| 49 | doc="""Set the default format for the handler of the `/tags` domain. |
|---|
| 50 | |
|---|
| 51 | || `oldlist` (default value) || The original format with a |
|---|
| 52 | bulleted-list of "linked-id description (tags)" || |
|---|
| 53 | || `compact` || bulleted-list of "linked-description" || |
|---|
| 54 | || `table` || table... (see corresponding column option) || |
|---|
| 55 | """) |
|---|
| 56 | exclude_realms = ListOption('tags', 'exclude_realms', [], |
|---|
| 57 | doc="""Comma-separated list of realms to exclude from tags queries |
|---|
| 58 | by default, unless specifically included using "realm:realm-name" |
|---|
| 59 | in a query.""") |
|---|
| 60 | |
|---|
| 61 | # INavigationContributor methods |
|---|
| 62 | def get_active_navigation_item(self, req): |
|---|
| 63 | if 'TAGS_VIEW' in req.perm: |
|---|
| 64 | return 'tags' |
|---|
| 65 | |
|---|
| 66 | def get_navigation_items(self, req): |
|---|
| 67 | if 'TAGS_VIEW' in req.perm: |
|---|
| 68 | label = tag_("Tags") |
|---|
| 69 | yield ('mainnav', 'tags', |
|---|
| 70 | builder.a(label, href=req.href.tags(), accesskey='T')) |
|---|
| 71 | |
|---|
| 72 | # IRequestHandler methods |
|---|
| 73 | def match_request(self, req): |
|---|
| 74 | return 'TAGS_VIEW' in req.perm and req.path_info.startswith('/tags') |
|---|
| 75 | |
|---|
| 76 | def process_request(self, req): |
|---|
| 77 | req.perm.require('TAGS_VIEW') |
|---|
| 78 | |
|---|
| 79 | match = re.match(r'/tags/?(.*)', req.path_info) |
|---|
| 80 | tag_id = match.group(1) and match.group(1) or None |
|---|
| 81 | query = req.args.get('q', '') |
|---|
| 82 | |
|---|
| 83 | # Consider only providers, that are permitted for display. |
|---|
| 84 | realms = [p.get_taggable_realm() for p in self.tag_providers |
|---|
| 85 | if (not hasattr(p, 'check_permission') or \ |
|---|
| 86 | p.check_permission(req.perm, 'view'))] |
|---|
| 87 | if not (tag_id or query) or [r for r in realms if r in req.args] == []: |
|---|
| 88 | for realm in realms: |
|---|
| 89 | if not realm in self.exclude_realms: |
|---|
| 90 | req.args[realm] = 'on' |
|---|
| 91 | checked_realms = [r for r in realms if r in req.args] |
|---|
| 92 | if query: |
|---|
| 93 | # Add permitted realms from query expression. |
|---|
| 94 | checked_realms.extend(query_realms(query, realms)) |
|---|
| 95 | realm_args = dict(zip([r for r in checked_realms], |
|---|
| 96 | ['on' for r in checked_realms])) |
|---|
| 97 | # Switch between single tag and tag query expression mode. |
|---|
| 98 | if tag_id and not re.match(r"""(['"]?)(\S+)\1$""", tag_id, re.UNICODE): |
|---|
| 99 | # Convert complex, invalid tag ID's --> query expression. |
|---|
| 100 | req.redirect(req.href.tags(realm_args, q=tag_id)) |
|---|
| 101 | elif query: |
|---|
| 102 | single_page = re.match(r"""(['"]?)(\S+)\1$""", query, re.UNICODE) |
|---|
| 103 | if single_page: |
|---|
| 104 | # Convert simple query --> single tag. |
|---|
| 105 | req.redirect(req.href.tags(single_page.group(2), realm_args)) |
|---|
| 106 | |
|---|
| 107 | data = dict(page_title=_("Tags"), checked_realms=checked_realms) |
|---|
| 108 | # Populate the TagsQuery form field. |
|---|
| 109 | data['tag_query'] = tag_id and tag_id or query |
|---|
| 110 | data['tag_realms'] = list(dict(name=realm, |
|---|
| 111 | checked=realm in checked_realms) |
|---|
| 112 | for realm in realms) |
|---|
| 113 | if tag_id: |
|---|
| 114 | page_name = tag_id |
|---|
| 115 | page = WikiPage(self.env, page_name) |
|---|
| 116 | data['tag_page'] = page |
|---|
| 117 | |
|---|
| 118 | macros = TagWikiMacros(self.env) |
|---|
| 119 | if query or tag_id: |
|---|
| 120 | # TRANSLATOR: The meta-nav link label. |
|---|
| 121 | add_ctxtnav(req, _("Back to Cloud"), req.href.tags()) |
|---|
| 122 | macro = 'ListTagged' |
|---|
| 123 | args = "%s,format=%s,cols=%s" % \ |
|---|
| 124 | (tag_id and tag_id or query, self.default_format, |
|---|
| 125 | self.default_cols) |
|---|
| 126 | data['mincount'] = None |
|---|
| 127 | else: |
|---|
| 128 | macro = 'TagCloud' |
|---|
| 129 | mincount = as_int(req.args.get('mincount', None), |
|---|
| 130 | self.cloud_mincount) |
|---|
| 131 | args = mincount and "mincount=%s" % mincount or None |
|---|
| 132 | data['mincount'] = mincount |
|---|
| 133 | formatter = Formatter(self.env, Context.from_request(req, |
|---|
| 134 | Resource('tag'))) |
|---|
| 135 | self.env.log.debug( |
|---|
| 136 | "Tag macro arguments: %s", args and args or '(none)') |
|---|
| 137 | try: |
|---|
| 138 | # Query string without realm throws 'NotImplementedError'. |
|---|
| 139 | data['tag_body'] = checked_realms and \ |
|---|
| 140 | macros.expand_macro(formatter, macro, args, |
|---|
| 141 | realms=checked_realms) \ |
|---|
| 142 | or '' |
|---|
| 143 | except InvalidQuery, e: |
|---|
| 144 | data['tag_query_error'] = to_unicode(e) |
|---|
| 145 | data['tag_body'] = macros.expand_macro(formatter, 'TagCloud', '') |
|---|
| 146 | add_stylesheet(req, 'tags/css/tractags.css') |
|---|
| 147 | return 'tag_view.html', data, None |
|---|