| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2011-2014 Rob Guttman <guttman@alum.mit.edu> |
|---|
| 4 | # All rights reserved. |
|---|
| 5 | # |
|---|
| 6 | # This software is licensed as described in the file COPYING, which |
|---|
| 7 | # you should have received as part of this distribution. |
|---|
| 8 | # |
|---|
| 9 | |
|---|
| 10 | import re |
|---|
| 11 | import json |
|---|
| 12 | from trac.core import * |
|---|
| 13 | from trac.web.chrome import ITemplateProvider, add_script, add_stylesheet |
|---|
| 14 | from trac.web.main import IRequestFilter, IRequestHandler |
|---|
| 15 | from trac.perm import IPermissionRequestor |
|---|
| 16 | from analysis import * |
|---|
| 17 | |
|---|
| 18 | class AnalyzeModule(Component): |
|---|
| 19 | """Base component for analyzing tickets.""" |
|---|
| 20 | |
|---|
| 21 | implements(IRequestHandler, ITemplateProvider, IRequestFilter, |
|---|
| 22 | IPermissionRequestor) |
|---|
| 23 | |
|---|
| 24 | analyses = ExtensionPoint(IAnalysis) |
|---|
| 25 | |
|---|
| 26 | # IPermissionRequestor methods |
|---|
| 27 | def get_permission_actions(self): |
|---|
| 28 | return ['ANALYZE_VIEW'] |
|---|
| 29 | |
|---|
| 30 | # ITemplateProvider methods |
|---|
| 31 | def get_htdocs_dirs(self): |
|---|
| 32 | from pkg_resources import resource_filename |
|---|
| 33 | return [('analyze', resource_filename(__name__, 'htdocs'))] |
|---|
| 34 | |
|---|
| 35 | def get_templates_dirs(self): |
|---|
| 36 | from pkg_resources import resource_filename |
|---|
| 37 | return [resource_filename(__name__, 'templates')] |
|---|
| 38 | |
|---|
| 39 | # IRequestFilter methods |
|---|
| 40 | def pre_process_request(self, req, handler): |
|---|
| 41 | return handler |
|---|
| 42 | |
|---|
| 43 | def post_process_request(self, req, template, data, content_type): |
|---|
| 44 | if self._valid_request(req): |
|---|
| 45 | add_stylesheet(req, 'analyze/analyze.css') |
|---|
| 46 | add_stylesheet(req, 'analyze/jquery-ui-1.8.16.custom.css') |
|---|
| 47 | add_script(req, 'analyze/jquery-ui-1.8.16.custom.min.js') |
|---|
| 48 | add_script(req, '/analyze/analyze.html') |
|---|
| 49 | add_script(req, 'analyze/analyze.js') |
|---|
| 50 | return template, data, content_type |
|---|
| 51 | |
|---|
| 52 | # IRequestHandler methods |
|---|
| 53 | def match_request(self, req): |
|---|
| 54 | return req.path_info.startswith('/analyze/') |
|---|
| 55 | |
|---|
| 56 | def process_request(self, req): |
|---|
| 57 | data = {'analyses':self._get_analyses(req, check_referer=True), |
|---|
| 58 | 'report':get_report(req, check_referer=True)} |
|---|
| 59 | return 'analyze.html', data, 'text/javascript' |
|---|
| 60 | |
|---|
| 61 | # private methods |
|---|
| 62 | def _valid_request(self, req): |
|---|
| 63 | """Checks permissions and that report can be analyzed.""" |
|---|
| 64 | if req.perm.has_permission('ANALYZE_VIEW') and \ |
|---|
| 65 | 'action=' not in req.query_string and \ |
|---|
| 66 | self._get_analyses(req): |
|---|
| 67 | return True |
|---|
| 68 | return False |
|---|
| 69 | |
|---|
| 70 | def _get_analyses(self, req, check_referer=False): |
|---|
| 71 | """Returns a list of analyses for the given report.""" |
|---|
| 72 | report = get_report(req, check_referer) |
|---|
| 73 | analyses = [] |
|---|
| 74 | for analysis in self.analyses: |
|---|
| 75 | if report and analysis.can_analyze(report): |
|---|
| 76 | analyses.append(analysis) |
|---|
| 77 | return analyses |
|---|
| 78 | |
|---|
| 79 | |
|---|
| 80 | class AnalyzeAjaxModule(Component): |
|---|
| 81 | """Ajax handler for suggesting solutions to users and fixing issues.""" |
|---|
| 82 | implements(IRequestHandler) |
|---|
| 83 | |
|---|
| 84 | analyses = ExtensionPoint(IAnalysis) |
|---|
| 85 | |
|---|
| 86 | # IRequestHandler methods |
|---|
| 87 | def match_request(self, req): |
|---|
| 88 | if req.path_info.startswith('/analyzeajax/'): |
|---|
| 89 | return True |
|---|
| 90 | |
|---|
| 91 | def process_request(self, req): |
|---|
| 92 | """Process AJAX request.""" |
|---|
| 93 | try: |
|---|
| 94 | if req.path_info.endswith('/list'): |
|---|
| 95 | result = self._get_analyses(req.args['report']) |
|---|
| 96 | else: |
|---|
| 97 | for analysis in self.analyses: |
|---|
| 98 | if not req.path_info.endswith('/'+analysis.path) and \ |
|---|
| 99 | not req.path_info.endswith('/'+analysis.path+'/fix'): |
|---|
| 100 | continue |
|---|
| 101 | db = self.env.get_db_cnx() |
|---|
| 102 | if req.path_info.endswith('/fix'): |
|---|
| 103 | result = self._fix_issue(analysis, db, req) |
|---|
| 104 | else: |
|---|
| 105 | report = get_report(req, check_referer=True) |
|---|
| 106 | result = self._get_solutions(analysis, db, req, report) |
|---|
| 107 | break |
|---|
| 108 | else: |
|---|
| 109 | raise Exception("Unknown path: %s" % req.path_info) |
|---|
| 110 | code,type,msg = 200,'application/json',json.dumps(result) |
|---|
| 111 | except Exception, e: |
|---|
| 112 | import traceback; |
|---|
| 113 | code,type = 500,'text/plain' |
|---|
| 114 | msg = "Oops...\n"+traceback.format_exc()+"\n" |
|---|
| 115 | req.send_response(code) |
|---|
| 116 | req.send_header('Content-Type', type) |
|---|
| 117 | req.send_header('Content-Length', len(msg)) |
|---|
| 118 | req.end_headers() |
|---|
| 119 | req.write(msg) |
|---|
| 120 | |
|---|
| 121 | def _get_analyses(self, report): |
|---|
| 122 | result = [] |
|---|
| 123 | for analysis in self.analyses: |
|---|
| 124 | if analysis.can_analyze(report): |
|---|
| 125 | result.append({ |
|---|
| 126 | 'path': analysis.path, |
|---|
| 127 | 'num': analysis.num, |
|---|
| 128 | 'title': analysis.title, |
|---|
| 129 | }) |
|---|
| 130 | return result |
|---|
| 131 | |
|---|
| 132 | def _get_solutions(self, analysis, db, req, report): |
|---|
| 133 | """Return the solutions with serialized data to use for the fix. |
|---|
| 134 | Ticket references are converted to hrefs.""" |
|---|
| 135 | issue,solutions = analysis.get_solutions(db, req.args, report) |
|---|
| 136 | base = req.base_url+'/ticket/' |
|---|
| 137 | id = re.compile(r"(#([1-9][0-9]*))") |
|---|
| 138 | issue = id.sub(r'<a href="%s\2">\1</a>' % base, issue) |
|---|
| 139 | |
|---|
| 140 | # serialize solution data; convert ticket refs to hrefs |
|---|
| 141 | for solution in solutions: |
|---|
| 142 | solution['disabled'] = not req.perm.has_permission('TICKET_MODIFY') |
|---|
| 143 | solution['data'] = json.dumps(solution['data']) |
|---|
| 144 | name = solution['name'] |
|---|
| 145 | solution['name'] = id.sub(r'<a href="%s\2">\1</a>' % base, name) |
|---|
| 146 | |
|---|
| 147 | return {'title': analysis.title, 'label': issue, |
|---|
| 148 | 'exists': len(solutions) > 0, 'solutions': solutions, |
|---|
| 149 | 'refresh': analysis.get_refresh_report() or \ |
|---|
| 150 | get_report(req, check_referer=True)} |
|---|
| 151 | |
|---|
| 152 | def _fix_issue(self, analysis, db, req): |
|---|
| 153 | """Return the result of the fix.""" |
|---|
| 154 | data = json.loads(req.args['data']) |
|---|
| 155 | return analysis.fix_issue(db, data, req.authname) |
|---|
| 156 | |
|---|
| 157 | |
|---|
| 158 | # common functions |
|---|
| 159 | def get_report(req, check_referer=False): |
|---|
| 160 | """Returns the report number as a string.""" |
|---|
| 161 | report_re = re.compile(r"/report/(?P<num>[1-9][0-9]*)") |
|---|
| 162 | if check_referer: |
|---|
| 163 | path = req.environ.get('HTTP_REFERER','') |
|---|
| 164 | else: |
|---|
| 165 | path = req.path_info |
|---|
| 166 | match = report_re.search(path) |
|---|
| 167 | if match: |
|---|
| 168 | return match.groupdict()['num'] |
|---|
| 169 | return None |
|---|