| 1 | import re |
|---|
| 2 | from tractags.api import TagEngine |
|---|
| 3 | from StringIO import StringIO |
|---|
| 4 | from trac.core import * |
|---|
| 5 | from trac.perm import IPermissionRequestor |
|---|
| 6 | from trac.web.main import IRequestHandler |
|---|
| 7 | from trac.web.chrome import ITemplateProvider, INavigationContributor, add_stylesheet |
|---|
| 8 | from trac.web.api import ITemplateStreamFilter |
|---|
| 9 | from trac.wiki.api import IWikiPageManipulator |
|---|
| 10 | from trac.util.html import Markup, escape |
|---|
| 11 | from trac.util.compat import set |
|---|
| 12 | from trac.wiki.web_ui import WikiModule |
|---|
| 13 | from tractags.expr import Expression |
|---|
| 14 | from genshi.builder import tag as T |
|---|
| 15 | from genshi.filters.transform import Transformer |
|---|
| 16 | |
|---|
| 17 | |
|---|
| 18 | _tag_split = re.compile('[,\s]+') |
|---|
| 19 | |
|---|
| 20 | |
|---|
| 21 | class TagsUserInterface(Component): |
|---|
| 22 | implements(ITemplateStreamFilter, IWikiPageManipulator, IPermissionRequestor) |
|---|
| 23 | |
|---|
| 24 | # Internal methods |
|---|
| 25 | def _page_tags(self, req): |
|---|
| 26 | pagename = req.args.get('page', 'WikiStart') |
|---|
| 27 | |
|---|
| 28 | engine = TagEngine(self.env) |
|---|
| 29 | wikitags = engine.tagspace.wiki |
|---|
| 30 | current_tags = list(wikitags.get_tags([pagename])) |
|---|
| 31 | current_tags.sort() |
|---|
| 32 | return current_tags |
|---|
| 33 | |
|---|
| 34 | def _wiki_view(self, req, stream): |
|---|
| 35 | tags = self._page_tags(req) |
|---|
| 36 | if not tags: |
|---|
| 37 | return stream |
|---|
| 38 | engine = TagEngine(self.env) |
|---|
| 39 | add_stylesheet(req, 'tags/css/tractags.css') |
|---|
| 40 | li = [] |
|---|
| 41 | for tag in tags: |
|---|
| 42 | href, title = engine.get_tag_link(tag) |
|---|
| 43 | li.append(T.li(T.a(title=title, href=href)(tag), ' ')) |
|---|
| 44 | |
|---|
| 45 | insert = T.ul(class_='tags')(T.lh('Tags'), li) |
|---|
| 46 | |
|---|
| 47 | return stream | Transformer('//div[@class="buttons"]').before(insert) |
|---|
| 48 | |
|---|
| 49 | def _update_tags(self, req, page): |
|---|
| 50 | newtags = set([t.strip() for t in |
|---|
| 51 | _tag_split.split(req.args.get('tags')) if t.strip()]) |
|---|
| 52 | wikitags = TagEngine(self.env).tagspace.wiki |
|---|
| 53 | oldtags = wikitags.get_tags([page.name]) |
|---|
| 54 | |
|---|
| 55 | if oldtags != newtags: |
|---|
| 56 | wikitags.replace_tags(req, page.name, newtags) |
|---|
| 57 | |
|---|
| 58 | def _wiki_edit(self, req, stream): |
|---|
| 59 | insert = T.div(class_='field')( |
|---|
| 60 | T.label( |
|---|
| 61 | 'Tag under: (', T.a('view all tags', href=req.href.tags()), ')', |
|---|
| 62 | T.br(), |
|---|
| 63 | T.input(id='tags', type='text', name='tags', size='30', |
|---|
| 64 | value=req.args.get('tags', ' '.join(self._page_tags(req)))), |
|---|
| 65 | ) |
|---|
| 66 | ) |
|---|
| 67 | return stream | Transformer('//div[@id="changeinfo1"]').append(insert) |
|---|
| 68 | |
|---|
| 69 | # IPermissionRequestor methods |
|---|
| 70 | def get_permission_actions(self): |
|---|
| 71 | return ['TAGS_VIEW', 'TAGS_MODIFY'] |
|---|
| 72 | |
|---|
| 73 | # ITemplateStreamFilter methods |
|---|
| 74 | def filter_stream(self, req, method, filename, stream, data): |
|---|
| 75 | if filename == 'wiki_view.html' and 'TAGS_VIEW' in req.perm: |
|---|
| 76 | return self._wiki_view(req, stream) |
|---|
| 77 | elif filename == 'wiki_edit.html' and 'TAGS_MODIFY' in req.perm: |
|---|
| 78 | return self._wiki_edit(req, stream) |
|---|
| 79 | return stream |
|---|
| 80 | |
|---|
| 81 | # IWikiPageManipulator methods |
|---|
| 82 | def prepare_wiki_page(self, req, page, fields): |
|---|
| 83 | pass |
|---|
| 84 | |
|---|
| 85 | def validate_wiki_page(self, req, page): |
|---|
| 86 | if req and 'TAGS_MODIFY' in req.perm and req.path_info.startswith('/wiki') \ |
|---|
| 87 | and 'save' in req.args: |
|---|
| 88 | self._update_tags(req, page) |
|---|
| 89 | return [] |
|---|
| 90 | |
|---|
| 91 | |
|---|
| 92 | class TagsModule(Component): |
|---|
| 93 | """ Serve a /tags namespace. Top-level displays tag cloud, sub-levels |
|---|
| 94 | display output of ListTagged(tag). |
|---|
| 95 | |
|---|
| 96 | The following configuration options are supported: |
|---|
| 97 | |
|---|
| 98 | [tags] |
|---|
| 99 | # Use a tag list or cloud for the main index |
|---|
| 100 | index = cloud|list |
|---|
| 101 | # The keyword arguments to pass to the TagCloud or ListTags macros that |
|---|
| 102 | # is being used for the index. |
|---|
| 103 | index.args = ... |
|---|
| 104 | # Keyword arguments to pass to the listing for each tag under the |
|---|
| 105 | # /tags/ URL space. |
|---|
| 106 | listing.args = ... |
|---|
| 107 | """ |
|---|
| 108 | implements(IRequestHandler, INavigationContributor, ITemplateProvider) |
|---|
| 109 | |
|---|
| 110 | # This method is never really called by anything.... Remove? |
|---|
| 111 | # def _prepare_wiki(self, req): |
|---|
| 112 | # from tractags.api import TagEngine |
|---|
| 113 | # page = req.path_info[6:] or 'WikiStart' |
|---|
| 114 | # engine = TagEngine(self.env) |
|---|
| 115 | # wikitags = engine.tagspace.wiki |
|---|
| 116 | # tags = list(wikitags.get_tags(page)) |
|---|
| 117 | # tags.sort() |
|---|
| 118 | # |
|---|
| 119 | # action = req.args.get('action', 'view') |
|---|
| 120 | # if action == 'edit': |
|---|
| 121 | # req.hdf['tags'] = req.args.get('tags', ', '.join(tags)) |
|---|
| 122 | # elif action == 'view': |
|---|
| 123 | # hdf_tags = [] |
|---|
| 124 | # for tag in tags: |
|---|
| 125 | # href, title = engine.get_tag_link(tag) |
|---|
| 126 | # hdf_tags.append({'name': tag, |
|---|
| 127 | # 'href': href, |
|---|
| 128 | # 'title': title}) |
|---|
| 129 | # req.hdf['tags'] = hdf_tags |
|---|
| 130 | |
|---|
| 131 | # ITemplateProvider methods |
|---|
| 132 | def get_templates_dirs(self): |
|---|
| 133 | """ |
|---|
| 134 | Return the absolute path of the directory containing the provided |
|---|
| 135 | ClearSilver templates. |
|---|
| 136 | """ |
|---|
| 137 | from pkg_resources import resource_filename |
|---|
| 138 | return [resource_filename(__name__, 'templates')] |
|---|
| 139 | |
|---|
| 140 | def get_htdocs_dirs(self): |
|---|
| 141 | """Return the absolute path of a directory containing additional |
|---|
| 142 | static resources (such as images, style sheets, etc). |
|---|
| 143 | """ |
|---|
| 144 | from pkg_resources import resource_filename |
|---|
| 145 | return [('tags', resource_filename(__name__, 'htdocs'))] |
|---|
| 146 | |
|---|
| 147 | # INavigationContributor methods |
|---|
| 148 | def get_active_navigation_item(self, req): |
|---|
| 149 | if 'TAGS_VIEW' in req.perm: |
|---|
| 150 | return 'tags' |
|---|
| 151 | |
|---|
| 152 | def get_navigation_items(self, req): |
|---|
| 153 | from trac.web.chrome import Chrome |
|---|
| 154 | if 'TAGS_VIEW' in req.perm: |
|---|
| 155 | yield ('mainnav', 'tags', |
|---|
| 156 | Markup('<a href="%s" accesskey="T">Tags</a>', |
|---|
| 157 | req.href.tags())) |
|---|
| 158 | |
|---|
| 159 | # IRequestHandler methods |
|---|
| 160 | def match_request(self, req): |
|---|
| 161 | return 'TAGS_VIEW' in req.perm and req.path_info.startswith('/tags') |
|---|
| 162 | |
|---|
| 163 | def process_request(self, req): |
|---|
| 164 | from tractags.macros import TagMacros |
|---|
| 165 | from tractags.parseargs import parseargs |
|---|
| 166 | from trac.web.chrome import add_stylesheet |
|---|
| 167 | |
|---|
| 168 | req.perm.require('TAGS_VIEW') |
|---|
| 169 | |
|---|
| 170 | add_stylesheet(req, 'tags/css/tractags.css') |
|---|
| 171 | data = {} |
|---|
| 172 | |
|---|
| 173 | def update_from_req(args): |
|---|
| 174 | for k in req.args.keys(): |
|---|
| 175 | args[k] = unicode(req.args.get(k)) |
|---|
| 176 | |
|---|
| 177 | if not req.args.has_key('e') and re.match('^/tags/?$', req.path_info): |
|---|
| 178 | index = self.env.config.get('tags', 'index', 'cloud') |
|---|
| 179 | index_kwargs = {'smallest': 10, 'biggest': 30} |
|---|
| 180 | _, config_kwargs = parseargs(self.env.config.get('tags', 'index.args', '')) |
|---|
| 181 | index_kwargs.update(config_kwargs) |
|---|
| 182 | update_from_req(index_kwargs) |
|---|
| 183 | |
|---|
| 184 | if index == 'cloud': |
|---|
| 185 | data['tag_body'] = Markup( |
|---|
| 186 | TagMacros(self.env).render_tagcloud(req, **index_kwargs)) |
|---|
| 187 | elif index == 'list': |
|---|
| 188 | data['tag_body'] = Markup( |
|---|
| 189 | TagMacros(self.env).render_listtagged(req, **index_kwargs)) |
|---|
| 190 | else: |
|---|
| 191 | raise TracError("Invalid index style '%s'" % index) |
|---|
| 192 | else: |
|---|
| 193 | _, args = parseargs(self.env.config.get('tags', 'listing.args', '')) |
|---|
| 194 | if req.args.has_key('e'): |
|---|
| 195 | expr = req.args.get('e') |
|---|
| 196 | else: |
|---|
| 197 | expr = req.path_info[6:] |
|---|
| 198 | data['tag_title'] = Markup('Objects matching the expression <i>%s</i>' % escape(expr)) |
|---|
| 199 | data['tag_expression'] = expr |
|---|
| 200 | try: |
|---|
| 201 | Expression(expr) |
|---|
| 202 | except Exception, e: |
|---|
| 203 | data['tag_expression_error'] = unicode(e).replace(' (line 1)', '') |
|---|
| 204 | args['expression'] = expr |
|---|
| 205 | tags = [] |
|---|
| 206 | update_from_req(args) |
|---|
| 207 | data['tag_body'] = Markup( |
|---|
| 208 | TagMacros(self.env).render_listtagged(req, *tags, **args)) |
|---|
| 209 | return 'tags.html', data, None |
|---|