| 1 | #!/usr/bin/env python |
|---|
| 2 | # -*- coding: utf-8 -*- |
|---|
| 3 | # |
|---|
| 4 | # Copyright (C) 2012-2013,2017,2019 MATOBA Akihiro <matobaa+trac-hacks@gmail.com> |
|---|
| 5 | # All rights reserved. |
|---|
| 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 | from trac.core import Component, implements |
|---|
| 11 | from trac.ticket.model import Ticket |
|---|
| 12 | from trac.web.api import IRequestHandler, IRequestFilter |
|---|
| 13 | from trac.config import ListOption, IntOption |
|---|
| 14 | from trac.wiki.model import WikiPage |
|---|
| 15 | from datetime import datetime |
|---|
| 16 | from trac.web.chrome import ITemplateProvider, add_stylesheet, add_script,\ |
|---|
| 17 | add_script_data |
|---|
| 18 | from pkg_resources import resource_filename |
|---|
| 19 | import json |
|---|
| 20 | |
|---|
| 21 | |
|---|
| 22 | class TicketLinkDecorator(Component): |
|---|
| 23 | """ set css-class to ticket link as ticket field value. field name can |
|---|
| 24 | set in [ticket]-decorate_fields in trac.ini |
|---|
| 25 | """ |
|---|
| 26 | implements(IRequestHandler, IRequestFilter, ITemplateProvider) |
|---|
| 27 | |
|---|
| 28 | decorate_fields = ListOption('ticket', 'decorate_fields', default='type', |
|---|
| 29 | doc=""" comma separated List of field names to add css class of ticket link. |
|---|
| 30 | (Provided by !ContextChrome.!TicketLinkDecorator) """) |
|---|
| 31 | |
|---|
| 32 | # IRequestHandler Methods |
|---|
| 33 | def match_request(self, req): |
|---|
| 34 | return req.path_info == '/contextchrome/ticketlink.jsonrpc' |
|---|
| 35 | |
|---|
| 36 | def process_request(self, req): |
|---|
| 37 | payload = json.load(req) |
|---|
| 38 | if not 'method' in payload or not payload['method'] == 'ticket.get': |
|---|
| 39 | req.send_response(501) # Method Not Implemented |
|---|
| 40 | req.end_headers() |
|---|
| 41 | return |
|---|
| 42 | params = payload['params'] |
|---|
| 43 | content = json.dumps(dict(map(lambda id: (id, self.get(req, id)), params)), indent=4) |
|---|
| 44 | req.send_response(200) |
|---|
| 45 | req.send_header('Content-Type', 'application/json') |
|---|
| 46 | req.send_header('Content-Length', len(content)) |
|---|
| 47 | req.end_headers() |
|---|
| 48 | req.write(content) |
|---|
| 49 | |
|---|
| 50 | def format(self, param): |
|---|
| 51 | if isinstance(param, datetime): |
|---|
| 52 | return {"__jsonclass__": ["datetime", param.isoformat()]} |
|---|
| 53 | return param |
|---|
| 54 | |
|---|
| 55 | # if TicketSystem(self.env).resource_exists(resource): |
|---|
| 56 | def get(self, req, id): |
|---|
| 57 | ticket = Ticket(self.env, id) |
|---|
| 58 | req.perm(ticket.resource).require('TICKET_VIEW') # FIXME: skip instead fail |
|---|
| 59 | return { # like XmlRpcPlugin |
|---|
| 60 | 'id': id, |
|---|
| 61 | 'error': None, |
|---|
| 62 | 'result': [ |
|---|
| 63 | id, |
|---|
| 64 | self.format(ticket.values['time']), |
|---|
| 65 | self.format(ticket.values['changetime']), |
|---|
| 66 | dict([(key, self.format(ticket.values[key])) for key in ticket.values |
|---|
| 67 | if key in self.config.getlist('ticket', 'decorate_fields')]) |
|---|
| 68 | ] |
|---|
| 69 | } |
|---|
| 70 | |
|---|
| 71 | # IRequestFilter methods |
|---|
| 72 | def pre_process_request(self, req, handler): |
|---|
| 73 | return handler |
|---|
| 74 | |
|---|
| 75 | def post_process_request(self, req, template, data, content_type): |
|---|
| 76 | if True: # any template |
|---|
| 77 | add_script(req, "contextchrome/js/ticketlinkdecorator.js") |
|---|
| 78 | return template, data, content_type |
|---|
| 79 | |
|---|
| 80 | # ITemplateProvider methods |
|---|
| 81 | def get_htdocs_dirs(self): |
|---|
| 82 | return [('contextchrome', resource_filename(__name__, 'htdocs'))] |
|---|
| 83 | |
|---|
| 84 | def get_templates_dirs(self): |
|---|
| 85 | return [] |
|---|
| 86 | |
|---|
| 87 | |
|---|
| 88 | class WikiLinkNewDecolator(Component): |
|---|
| 89 | """ set \"new\" css-class to wiki link if the page is young. age can set in [wiki]-wiki_new_info_second in trac.ini""" |
|---|
| 90 | implements(IRequestHandler, IRequestFilter, ITemplateProvider) |
|---|
| 91 | |
|---|
| 92 | wiki_new_info_day = IntOption('wiki', 'wiki_new_info_second', '432000', |
|---|
| 93 | doc=u"""age in seconds to add new icon. (Provided by !ContextChrome.!WikiLinkNewDecolator) """) |
|---|
| 94 | |
|---|
| 95 | # IRequestHandler Methods |
|---|
| 96 | def match_request(self, req): |
|---|
| 97 | return req.path_info == '/contextchrome/wikilinknew.jsonrpc' |
|---|
| 98 | |
|---|
| 99 | def process_request(self, req): |
|---|
| 100 | payload = json.load(req) |
|---|
| 101 | if 'method' not in payload or not payload['method'] == 'wiki.getPageInfo': |
|---|
| 102 | req.send_response(501) # Method Not Implemented |
|---|
| 103 | req.end_headers() |
|---|
| 104 | return |
|---|
| 105 | params = payload['params'] |
|---|
| 106 | content = json.dumps(dict(map(lambda id: (id, self.get(req, id)), params)), indent=4) |
|---|
| 107 | req.send_response(200) |
|---|
| 108 | req.send_header('Content-Type', 'application/json') |
|---|
| 109 | req.send_header('Content-Length', len(content)) |
|---|
| 110 | req.end_headers() |
|---|
| 111 | req.write(content) |
|---|
| 112 | |
|---|
| 113 | # IRequestFilter methods |
|---|
| 114 | def pre_process_request(self, req, handler): |
|---|
| 115 | return handler |
|---|
| 116 | |
|---|
| 117 | def post_process_request(self, req, template, data, content_type): |
|---|
| 118 | if True: # any template |
|---|
| 119 | add_script(req, "contextchrome/js/wikilinknewdecorator.js") |
|---|
| 120 | add_script_data(req, {'config__wiki__wiki_new_info_second': |
|---|
| 121 | self.config.getint('wiki', 'wiki_new_info_second')}) |
|---|
| 122 | return template, data, content_type |
|---|
| 123 | |
|---|
| 124 | def get(self, req, id): |
|---|
| 125 | wikipage = WikiPage(self.env, id) |
|---|
| 126 | req.perm(wikipage.resource).require('WIKI_VIEW') # FIXME: skip instead fail |
|---|
| 127 | now = datetime.now(req.tz) |
|---|
| 128 | limit = self.config.getint('wiki', 'wiki_new_info_second') |
|---|
| 129 | delta = now - wikipage.time or 0 |
|---|
| 130 | return { # like XmlRpcPlugin |
|---|
| 131 | 'id': id, |
|---|
| 132 | 'error': None, |
|---|
| 133 | 'result': [ |
|---|
| 134 | id, |
|---|
| 135 | wikipage.time.isoformat(), |
|---|
| 136 | delta.total_seconds(), |
|---|
| 137 | limit > delta.days * 86400 + delta.seconds, # True if page is new |
|---|
| 138 | ] |
|---|
| 139 | } |
|---|
| 140 | |
|---|
| 141 | # ITemplateProvider methods |
|---|
| 142 | def get_htdocs_dirs(self): |
|---|
| 143 | return [('contextchrome', resource_filename(__name__, 'htdocs'))] |
|---|
| 144 | |
|---|
| 145 | def get_templates_dirs(self): |
|---|
| 146 | return [] |
|---|
| 147 | |
|---|
| 148 | |
|---|
| 149 | class InternalStylesheet(Component): |
|---|
| 150 | """ Use internal stylesheet. Off to use your own site.css for \".new\" css-class.""" |
|---|
| 151 | implements(IRequestFilter, ITemplateProvider) |
|---|
| 152 | |
|---|
| 153 | # IRequestFilter methods |
|---|
| 154 | def pre_process_request(self, req, handler): |
|---|
| 155 | add_stylesheet(req, "contextchrome/css/contextchrome.css") |
|---|
| 156 | return handler |
|---|
| 157 | |
|---|
| 158 | def post_process_request(self, req, template, data, content_type): |
|---|
| 159 | return template, data, content_type |
|---|
| 160 | |
|---|
| 161 | # ITemplateProvider methods |
|---|
| 162 | def get_htdocs_dirs(self): |
|---|
| 163 | return [('contextchrome', resource_filename(__name__, 'htdocs'))] |
|---|
| 164 | |
|---|
| 165 | def get_templates_dirs(self): |
|---|
| 166 | return [] |
|---|
| 167 | |
|---|
| 168 | |
|---|
| 169 | class InterTracTicketLinkDecorator(Component): |
|---|
| 170 | """ set css-class to type on external ticket. """ |
|---|
| 171 | implements(IRequestFilter, ITemplateProvider) |
|---|
| 172 | |
|---|
| 173 | # IRequestFilter methods |
|---|
| 174 | def pre_process_request(self, req, handler): |
|---|
| 175 | add_script(req, "contextchrome/js/xdr.js") |
|---|
| 176 | add_script(req, "contextchrome/js/intertracticketlinkdecorator.js") |
|---|
| 177 | add_script_data(req, {'config__ticket__decolate_fields': self.config.getlist('ticket', 'decorate_fields')}) |
|---|
| 178 | return handler |
|---|
| 179 | |
|---|
| 180 | def post_process_request(self, req, template, data, content_type): |
|---|
| 181 | return template, data, content_type |
|---|
| 182 | |
|---|
| 183 | # ITemplateProvider methods |
|---|
| 184 | def get_htdocs_dirs(self): |
|---|
| 185 | return [('contextchrome', resource_filename(__name__, 'htdocs'))] |
|---|
| 186 | |
|---|
| 187 | def get_templates_dirs(self): |
|---|
| 188 | return [] |
|---|