| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2015 Jon Ashley <trac@zelatrix.plus.com> |
|---|
| 4 | # All rights reserved. |
|---|
| 5 | # |
|---|
| 6 | # Copyright (C) 2012 Rob Guttman <guttman@alum.mit.edu> |
|---|
| 7 | # All rights reserved. |
|---|
| 8 | # |
|---|
| 9 | # This software is licensed as described in the file COPYING, which |
|---|
| 10 | # you should have received as part of this distribution. |
|---|
| 11 | |
|---|
| 12 | import re |
|---|
| 13 | |
|---|
| 14 | from trac.config import ListOption |
|---|
| 15 | from trac.core import * |
|---|
| 16 | from trac.web.chrome import ITemplateProvider, add_script, add_stylesheet |
|---|
| 17 | from trac.web.main import IRequestFilter, IRequestHandler |
|---|
| 18 | |
|---|
| 19 | class VisualizationModule(Component): |
|---|
| 20 | implements(IRequestHandler, ITemplateProvider, IRequestFilter) |
|---|
| 21 | |
|---|
| 22 | SECTION = 'dyviz' |
|---|
| 23 | DEFAULTS = { |
|---|
| 24 | 'source': 'table', |
|---|
| 25 | 'query': '', |
|---|
| 26 | 'selector': 'table.listing.tickets', |
|---|
| 27 | 'options': 'width:600,height:400', |
|---|
| 28 | } |
|---|
| 29 | |
|---|
| 30 | reports = ListOption(SECTION, 'reports', default=[], |
|---|
| 31 | doc="List of report numbers to treat as queues(?)") |
|---|
| 32 | |
|---|
| 33 | # ITemplateProvider methods. |
|---|
| 34 | def get_htdocs_dirs(self): |
|---|
| 35 | from pkg_resources import resource_filename |
|---|
| 36 | return [('dyviz', resource_filename(__name__, 'htdocs'))] |
|---|
| 37 | |
|---|
| 38 | def get_templates_dirs(self): |
|---|
| 39 | from pkg_resources import resource_filename |
|---|
| 40 | return [resource_filename(__name__, 'templates')] |
|---|
| 41 | |
|---|
| 42 | # IRequestFilter methods. |
|---|
| 43 | def pre_process_request(self, req, handler): |
|---|
| 44 | return handler |
|---|
| 45 | |
|---|
| 46 | def post_process_request(self, req, template, data, content_type): |
|---|
| 47 | if self._is_valid_request(req): |
|---|
| 48 | add_stylesheet(req, 'dyviz/dyviz.css') |
|---|
| 49 | add_script(req, 'dyviz/dygraph-combined-dev.js') |
|---|
| 50 | add_script(req, 'dyviz/dyviz.js') |
|---|
| 51 | add_script(req, '/dyviz/dyviz.html') |
|---|
| 52 | return template, data, content_type |
|---|
| 53 | |
|---|
| 54 | # IRequestHandler methods. |
|---|
| 55 | def match_request(self, req): |
|---|
| 56 | return req.path_info.startswith('/dyviz/') |
|---|
| 57 | |
|---|
| 58 | def process_request(self, req): |
|---|
| 59 | data = self._get_data(req) |
|---|
| 60 | return 'dyviz.html', data, 'text/javascript' |
|---|
| 61 | |
|---|
| 62 | # Private methods. |
|---|
| 63 | def _is_valid_request(self, req): |
|---|
| 64 | """ Checks permissions and that page is visualizable. """ |
|---|
| 65 | if req.perm.has_permission('TICKET_VIEW') and \ |
|---|
| 66 | 'action=' not in req.query_string and \ |
|---|
| 67 | self._get_section(req): |
|---|
| 68 | return True |
|---|
| 69 | return False |
|---|
| 70 | |
|---|
| 71 | def _get_section(self, req, check_referer=False): |
|---|
| 72 | """ Returns the trac.ini section that best matches the page url. |
|---|
| 73 | There's a default section [dyviz] plus regex defined sections. |
|---|
| 74 | The 'options' field is passed directly to dygraphs. |
|---|
| 75 | |
|---|
| 76 | [dyviz] |
|---|
| 77 | reports = 11,12 |
|---|
| 78 | options = width:400,height:300 |
|---|
| 79 | |
|---|
| 80 | [dyviz.report/12] |
|---|
| 81 | options = colors:['red','orange'] |
|---|
| 82 | |
|---|
| 83 | [dyviz.milestone] |
|---|
| 84 | options = plotter:barChartPlotter |
|---|
| 85 | |
|---|
| 86 | In this example, here are results for different page urls: |
|---|
| 87 | |
|---|
| 88 | /report/1 -> None |
|---|
| 89 | /report/11 -> 'dyviz' |
|---|
| 90 | /report/12 -> 'dyviz.report/12' |
|---|
| 91 | /milestone/m1 -> 'dyviz.milestone' |
|---|
| 92 | """ |
|---|
| 93 | if check_referer: |
|---|
| 94 | path = req.environ.get('HTTP_REFERER','') |
|---|
| 95 | else: |
|---|
| 96 | path = req.path_info |
|---|
| 97 | |
|---|
| 98 | # check regex sections |
|---|
| 99 | for section in self.env.config.sections(): |
|---|
| 100 | if not section.startswith('%s.' % self.SECTION): |
|---|
| 101 | continue |
|---|
| 102 | section_re = re.compile(section[len(self.SECTION)+1:]) |
|---|
| 103 | if section_re.search(path): |
|---|
| 104 | return section |
|---|
| 105 | |
|---|
| 106 | # check reports list |
|---|
| 107 | report_re = re.compile(r"/report/(?P<num>[1-9][0-9]*)") |
|---|
| 108 | match = report_re.search(req.path_info) |
|---|
| 109 | if match: |
|---|
| 110 | report = match.groupdict()['num'] |
|---|
| 111 | if report in self.reports: |
|---|
| 112 | return self.SECTION |
|---|
| 113 | |
|---|
| 114 | return None |
|---|
| 115 | |
|---|
| 116 | def _get_data(self, req): |
|---|
| 117 | """Return the template data for the given request url.""" |
|---|
| 118 | data = {} |
|---|
| 119 | section = self._get_section(req, check_referer=True) |
|---|
| 120 | |
|---|
| 121 | # override [dyviz] with regex section |
|---|
| 122 | for key,default in self.DEFAULTS.items(): |
|---|
| 123 | data[key] = self.env.config.get(self.SECTION,key,default) # [dyviz] |
|---|
| 124 | if section != self.SECTION: |
|---|
| 125 | data[key] = self.env.config.get(section,key,data[key]) |
|---|
| 126 | |
|---|
| 127 | # Redo options - make additive. |
|---|
| 128 | key,default = 'options',self.DEFAULTS['options'] |
|---|
| 129 | data[key] = self.env.config.get(self.SECTION,key,default) # [dyviz] |
|---|
| 130 | if section != self.SECTION: |
|---|
| 131 | options = self.env.config.get(section,key,data[key]) |
|---|
| 132 | if data[key] != options: |
|---|
| 133 | data[key] = (data[key] + ',' + options).strip(',') |
|---|
| 134 | |
|---|
| 135 | return data |
|---|