| [17402] | 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | |
|---|
| [18249] | 3 | from .model import ReviewFileModel |
|---|
| [18270] | 4 | from .peerReviewCommentCallback import writeJSONResponse, writeResponse |
|---|
| [18249] | 5 | from .peerReviewPerform import CommentAnnotator |
|---|
| [18270] | 6 | from .repo import hash_from_file_node |
|---|
| [17402] | 7 | from string import Template |
|---|
| 8 | from trac.core import Component, implements |
|---|
| 9 | from trac.util.html import tag |
|---|
| 10 | from trac.mimeview.api import IHTMLPreviewAnnotator |
|---|
| 11 | from trac.util.translation import _ |
|---|
| [18270] | 12 | from trac.versioncontrol import RepositoryManager, ResourceNotFound |
|---|
| 13 | from trac.versioncontrol.web_ui.util import get_existing_node |
|---|
| [17402] | 14 | from trac.web.api import IRequestFilter, IRequestHandler |
|---|
| 15 | from trac.web.chrome import add_ctxtnav, add_script, add_script_data, add_stylesheet |
|---|
| 16 | |
|---|
| 17 | __author__ = 'Cinc' |
|---|
| 18 | |
|---|
| 19 | |
|---|
| 20 | # Not used |
|---|
| 21 | def files_with_comments(env, path, rev): |
|---|
| 22 | """Return a dict with file_id as key and a comment id list as value.""" |
|---|
| [17451] | 23 | with env.sb_query as db: |
|---|
| 24 | cursor = db.cursor() |
|---|
| 25 | cursor.execute("""SELECT f.file_id, |
|---|
| 26 | f.revision, f.changerevision, f.review_id , c.comment_id, c.line_num |
|---|
| 27 | FROM peerreviewfile AS f |
|---|
| 28 | JOIN peerreviewcomment as c ON c.file_id = f.file_id |
|---|
| 29 | WHERE f.path = %s |
|---|
| 30 | AND f.changerevision = %s |
|---|
| 31 | """, (path, rev)) |
|---|
| [17402] | 32 | |
|---|
| [17451] | 33 | for row in cursor: |
|---|
| 34 | env.log.info('### %s', row) |
|---|
| [17402] | 35 | |
|---|
| 36 | # Not used |
|---|
| 37 | def select_by_path(env, path): |
|---|
| 38 | """Returns a generator.""" |
|---|
| 39 | rf = ReviewFileModel(env) |
|---|
| 40 | rf.clear_props() |
|---|
| 41 | rf['path'] = path |
|---|
| 42 | return rf.list_matching_objects() |
|---|
| 43 | |
|---|
| 44 | |
|---|
| 45 | class PeerReviewBrowser(Component): |
|---|
| 46 | """Show information about file review status in Trac source code browser. |
|---|
| 47 | |
|---|
| 48 | The file review status is only shown when displaying a source file. |
|---|
| 49 | |
|---|
| [18260] | 50 | '''Note''': This plugin may be disabled without side effects. |
|---|
| [17402] | 51 | """ |
|---|
| 52 | implements(IHTMLPreviewAnnotator, IRequestFilter, IRequestHandler) |
|---|
| 53 | |
|---|
| 54 | # IRequestFilter methods |
|---|
| 55 | |
|---|
| 56 | def pre_process_request(self, req, handler): |
|---|
| [18243] | 57 | """Always returns the request handler, even if unchanged.""" |
|---|
| [17402] | 58 | return handler |
|---|
| 59 | |
|---|
| 60 | def post_process_request(self, req, template, data, content_type): |
|---|
| [18243] | 61 | """Do any post-processing the request might need; |
|---|
| [17402] | 62 | `data` may be updated in place. |
|---|
| 63 | |
|---|
| 64 | Always returns a tuple of (template, data, content_type), even if |
|---|
| 65 | unchanged. |
|---|
| 66 | |
|---|
| 67 | Note that `template`, `data`, `content_type` will be `None` if: |
|---|
| 68 | - called when processing an error page |
|---|
| 69 | - the default request handler did not return any result |
|---|
| 70 | """ |
|---|
| [17415] | 71 | # Note that data is already filled with information about the source file, repo and what not |
|---|
| [17402] | 72 | path = req.args.get('path') |
|---|
| 73 | rev = req.args.get('rev') |
|---|
| 74 | |
|---|
| 75 | def is_file_with_comments(env, path, rev): |
|---|
| 76 | """Return a dict with file_id as key and a comment id list as value.""" |
|---|
| [17451] | 77 | with env.db_query as db: |
|---|
| 78 | cursor = db.cursor() |
|---|
| 79 | cursor.execute("""SELECT COUNT(f.file_id) |
|---|
| 80 | FROM peerreviewfile AS f |
|---|
| 81 | JOIN peerreviewcomment as c ON c.file_id = f.file_id |
|---|
| 82 | WHERE f.path = %s |
|---|
| 83 | AND f.changerevision = %s |
|---|
| 84 | """, (path, rev)) |
|---|
| 85 | return cursor.fetchone()[0] != 0 |
|---|
| [17402] | 86 | |
|---|
| [17415] | 87 | # We only handle the browser |
|---|
| [18270] | 88 | split_path = req.path_info.split('/') |
|---|
| [18242] | 89 | if path and req.path_info.startswith('/browser/') and data: |
|---|
| [17402] | 90 | add_stylesheet(req, 'hw/css/peerreview.css') |
|---|
| 91 | add_script_data(req, |
|---|
| [17446] | 92 | {'peer_repo': data.get('reponame', ''), |
|---|
| 93 | 'peer_rev': data.get('created_rev', ''), |
|---|
| [18260] | 94 | 'peer_is_head': 0 if rev else 1, |
|---|
| [17402] | 95 | 'peer_path': path, |
|---|
| 96 | 'peer_status_url': req.href.peerreviewstatus(), |
|---|
| [18270] | 97 | # Index page has no data['dir'] if only the repoindex page is shown and |
|---|
| 98 | # no default repo is defined |
|---|
| 99 | 'peer_is_dir': data.get('dir', None) != None or len(split_path) == 2, |
|---|
| [17446] | 100 | 'tacUrl': req.href.chrome('/hw/images/thumbtac11x11.gif')}) |
|---|
| [17402] | 101 | add_script(req, "hw/js/peer_trac_browser.js") |
|---|
| [17446] | 102 | # add_script(req, "hw/js/peer_review_perform.js") |
|---|
| [17415] | 103 | |
|---|
| [17446] | 104 | # Deactivate code comments in browser view for now |
|---|
| 105 | if path and req.path_info.startswith('/browser_/'): |
|---|
| 106 | if is_file_with_comments(self.env, '/' + data['path'], data.get('created_rev')): |
|---|
| [17402] | 107 | add_ctxtnav(req, _("Code Comments"), req.href(req.path_info, annotate='prcomment', rev=rev), |
|---|
| [17446] | 108 | title=_("Show Code Comments")) |
|---|
| [17402] | 109 | else: |
|---|
| 110 | add_ctxtnav(req, tag.span(_("Code Comments"), class_="missing")) |
|---|
| 111 | return template, data, content_type |
|---|
| 112 | |
|---|
| 113 | # IRequestHandler methods |
|---|
| 114 | |
|---|
| 115 | def _create_status_tbl(self, req): |
|---|
| 116 | tr_tmpl = """<tr${bg_color}> |
|---|
| [18270] | 117 | <td><a href="${file_href}">${file_id}</a></td> |
|---|
| 118 | <td><a href="${review_href}">${review_id}</a></td> |
|---|
| 119 | <td>${chg_rev}</td><td>${hash}</td><td>${status}</td> |
|---|
| [17402] | 120 | </tr> |
|---|
| 121 | """ |
|---|
| [18260] | 122 | tbl_tmpl = """<h3>%s</h3> |
|---|
| [17402] | 123 | <table class="listing peer-status-tbl"> |
|---|
| 124 | <thead> |
|---|
| 125 | <tr> |
|---|
| [18270] | 126 | <th>File ID</th><th>Review</th><th>Change Revision</th><th>Hash</th><th>Status</th> |
|---|
| [17402] | 127 | </tr> |
|---|
| 128 | </thead> |
|---|
| 129 | <tbody> |
|---|
| 130 | %%s |
|---|
| 131 | </tbody> |
|---|
| 132 | </table> |
|---|
| [18260] | 133 | """ % (_('Status Codereview'),) |
|---|
| 134 | not_head_tmpl = "<h3>%s</h3><p>%s</p>" %\ |
|---|
| [17402] | 135 | (_('Status Codereview'), _('Codereview status is only available for HEAD revision.')) |
|---|
| 136 | |
|---|
| 137 | if req.args.get('peer_is_dir') == 'true': |
|---|
| 138 | return '' |
|---|
| 139 | |
|---|
| 140 | repo = req.args.get('peer_repo') |
|---|
| [17415] | 141 | path = req.args.get('peer_path', '') |
|---|
| 142 | |
|---|
| 143 | if repo: |
|---|
| [18260] | 144 | # path starts with slash and reponame, like /reponame/my/path/to/file.txt |
|---|
| 145 | # All path information in the database is with leading '/'. |
|---|
| 146 | path = path[len(repo) + 1:] |
|---|
| [17402] | 147 | rev = req.args.get('peer_rev') |
|---|
| 148 | |
|---|
| [18260] | 149 | # if req.args.getint('peer_is_head') == 0: |
|---|
| 150 | # return not_head_tmpl |
|---|
| 151 | |
|---|
| 152 | res = '<div id="peer-msg" class="system-message warning">%s</div>' %\ |
|---|
| 153 | _('No review for this file revision yet.') |
|---|
| [17402] | 154 | trows = '' |
|---|
| 155 | with self.env.db_query as db: |
|---|
| [18270] | 156 | for row in db("SELECT review_id, changerevision, hash, status, file_id FROM peerreviewfile " |
|---|
| [17402] | 157 | "WHERE path = %s AND repo = %s AND review_id != 0 " |
|---|
| 158 | "ORDER BY review_id", (path, repo)): |
|---|
| [17415] | 159 | # Colorize row with current file revision. Last review wins... |
|---|
| [17402] | 160 | if row[1] == rev and row[3] == 'approved': |
|---|
| 161 | bg = ' style="background-color: #dfd"' |
|---|
| 162 | res = '<div id="peer-msg" class="system-message notice">' \ |
|---|
| 163 | '%s</div>' % _('File is <strong>approved</strong>.') |
|---|
| 164 | elif row[1] == rev and row[3] == 'disapproved': |
|---|
| 165 | bg = ' style="background-color: #ffb"' |
|---|
| 166 | res = '<div id="peer-msg" class="system-message warning">%s' \ |
|---|
| 167 | '</div>' % _('File is not <strong>approved</strong>.') |
|---|
| 168 | else: |
|---|
| 169 | bg = '' |
|---|
| 170 | |
|---|
| 171 | data = {'review_id': row[0], |
|---|
| 172 | 'chg_rev': row[1], |
|---|
| 173 | 'hash': row[2], |
|---|
| 174 | 'status': row[3], |
|---|
| [18270] | 175 | 'file_id': row[4], |
|---|
| [18282] | 176 | 'file_href': req.href.peerreviewfile(row[4]), |
|---|
| [17446] | 177 | 'review_href': req.href.peerreviewview(row[0]), |
|---|
| [17402] | 178 | 'bg_color': bg} |
|---|
| 179 | trows += Template(tr_tmpl).safe_substitute(data) |
|---|
| 180 | if trows: |
|---|
| [18260] | 181 | return tbl_tmpl % trows # + res |
|---|
| [17402] | 182 | else: |
|---|
| [18260] | 183 | return "<h3>%s</h3>" % _('Status Codereview') + res |
|---|
| [17402] | 184 | |
|---|
| 185 | def match_request(self, req): |
|---|
| 186 | return req.path_info == '/peerreviewstatus' |
|---|
| 187 | |
|---|
| 188 | def process_request(self, req): |
|---|
| [18270] | 189 | tr = """<tr><td colspan="2"><strong>{label}</strong> {hash}</td></tr>""" |
|---|
| 190 | data = {'statushtml': self._create_status_tbl(req), |
|---|
| 191 | 'hashhtml': tr.format(label="Hash:", hash=self.get_hash_for_file(req))} |
|---|
| 192 | writeJSONResponse(req, data) |
|---|
| [17402] | 193 | |
|---|
| [18270] | 194 | def get_hash_for_file(self, req): |
|---|
| 195 | """Return the hash for the currently viewed file. |
|---|
| 196 | |
|---|
| 197 | :param req: Request object. The arg dict holds file information like path, revision, ... |
|---|
| 198 | :return file hash string or an empty string. The hash is in hex |
|---|
| 199 | """ |
|---|
| 200 | if req.args.get('peer_dir') == 'true': |
|---|
| 201 | return None |
|---|
| 202 | |
|---|
| 203 | reponame = req.args.get('peer_repo') |
|---|
| 204 | path = req.args.get('peer_path', '') |
|---|
| 205 | if reponame: |
|---|
| 206 | # path starts with slash and reponame, like /reponame/my/path/to/file.txt |
|---|
| 207 | # All path information in the database is with leading '/'. |
|---|
| 208 | path = path[len(reponame) + 1:] |
|---|
| 209 | rev = req.args.get('peer_rev') |
|---|
| 210 | node = None |
|---|
| 211 | repos = RepositoryManager(self.env).get_repository(reponame) |
|---|
| 212 | |
|---|
| 213 | try: |
|---|
| 214 | node = get_existing_node(req, repos, path, rev) |
|---|
| 215 | except ResourceNotFound: |
|---|
| 216 | # The file may be a SVN copy. If so the revision is the one from the |
|---|
| 217 | # source file, the path is the current (not source) path. |
|---|
| 218 | # Search the history. This probably breaks when several copies are made. |
|---|
| 219 | src_path, src_rev, chnge = range(3) |
|---|
| 220 | for item in repos.get_path_history(path, limit=1): # this is a generator |
|---|
| 221 | if item[chnge] in ('copy', 'move'): |
|---|
| 222 | node = get_existing_node(req, repos, item[src_path], item[src_rev]) |
|---|
| 223 | break |
|---|
| 224 | if node: |
|---|
| 225 | return hash_from_file_node(node) |
|---|
| 226 | else: |
|---|
| 227 | return '' |
|---|
| 228 | |
|---|
| [17402] | 229 | # IHTMLPreviewAnnotator methods |
|---|
| 230 | |
|---|
| 231 | def get_annotation_type(self): |
|---|
| 232 | # Disable annotator in browser view for now |
|---|
| 233 | return 'prcomment_', 'Comment', 'Review Coment' |
|---|
| 234 | |
|---|
| 235 | def get_annotation_data(self, context): |
|---|
| 236 | |
|---|
| 237 | |
|---|
| 238 | self.log.info(context) |
|---|
| 239 | self.log.info('parent: %s %s', context.parent, context.parent.resource) |
|---|
| 240 | self.log.info('%s %s', context.resource, context.resource.realm) |
|---|
| 241 | self.log.info('parent: %s %s', context.resource.parent, context.resource.parent.realm) |
|---|
| 242 | self.log.info(context.resource.id) |
|---|
| 243 | |
|---|
| 244 | return CommentAnnotator(self.env, context, 'chrome/hw/images/thumbtac11x11.gif', 'prcomment') |
|---|
| 245 | |
|---|
| 246 | def annotate_row(self, context, row, lineno, line, comment_annotator): |
|---|
| 247 | """line annotator for Perform Code Review page. |
|---|
| 248 | |
|---|
| 249 | If line has a comment, places an icon to indicate comment. |
|---|
| 250 | """ |
|---|
| 251 | # self.log.info(lineno) |
|---|
| 252 | #comment_col = tag.th(style='color: red', class_='prcomment') |
|---|
| 253 | #row.append(comment_col) |
|---|
| 254 | comment_annotator.annotate_browser(row, lineno) |
|---|