| 1 | # |
|---|
| 2 | # Copyright (C) 2005-2006 Team5 |
|---|
| 3 | # Copyright (C) 2016 Cinc |
|---|
| 4 | # |
|---|
| 5 | # All rights reserved. |
|---|
| 6 | # |
|---|
| 7 | # This software is licensed as described in the file COPYING.txt, which |
|---|
| 8 | # you should have received as part of this distribution. |
|---|
| 9 | # |
|---|
| 10 | |
|---|
| 11 | # Provides functionality to create a new code review. |
|---|
| 12 | # Works with peerReviewNew.html |
|---|
| 13 | |
|---|
| 14 | import itertools |
|---|
| 15 | import time |
|---|
| 16 | from pkg_resources import get_distribution, parse_version |
|---|
| 17 | from trac import util |
|---|
| 18 | from trac.core import Component, implements, TracError |
|---|
| 19 | from trac.util.text import CRLF |
|---|
| 20 | from trac.web.chrome import INavigationContributor, add_script, add_script_data, \ |
|---|
| 21 | add_warning, add_notice, add_stylesheet, Chrome |
|---|
| 22 | from trac.web.main import IRequestHandler |
|---|
| 23 | from trac.versioncontrol.api import RepositoryManager |
|---|
| 24 | from CodeReviewStruct import * |
|---|
| 25 | from model import Comment, get_users, \ |
|---|
| 26 | PeerReviewerModel, PeerReviewModel, Reviewer, ReviewFileModel |
|---|
| 27 | from peerReviewMain import add_ctxt_nav_items |
|---|
| 28 | from repobrowser import get_node_from_repo |
|---|
| 29 | from .repo import hash_from_file_node |
|---|
| 30 | |
|---|
| 31 | |
|---|
| 32 | def java_string_hashcode(s): |
|---|
| 33 | # See: http://garage.pimentech.net/libcommonPython_src_python_libcommon_javastringhashcode/ |
|---|
| 34 | h = 0 |
|---|
| 35 | for c in s: |
|---|
| 36 | h = (31 * h + ord(c)) & 0xFFFFFFFF |
|---|
| 37 | return ((h + 0x80000000) & 0xFFFFFFFF) - 0x80000000 |
|---|
| 38 | |
|---|
| 39 | |
|---|
| 40 | def create_id_string(f, rev=None): |
|---|
| 41 | # Use rev to override the revision in the id string. Used in followup review creation |
|---|
| 42 | f_rev = rev or f['revision'] |
|---|
| 43 | return "%s,%s,%s,%s,%s" %\ |
|---|
| 44 | (f['path'], f_rev, f['line_start'], f['line_end'], f['repo']) |
|---|
| 45 | |
|---|
| 46 | |
|---|
| 47 | def create_file_hash_id(f): |
|---|
| 48 | return 'id%s' % java_string_hashcode(create_id_string(f)) |
|---|
| 49 | |
|---|
| 50 | |
|---|
| 51 | def add_users_to_data(env, reviewID, data): |
|---|
| 52 | """Add user, assigned and unassigned users to dict data. |
|---|
| 53 | |
|---|
| 54 | This function searches all users assigned to the given review and adds the list to the data dictionary using |
|---|
| 55 | key 'assigned_users'. Not yet assigned users are added using the key 'unassigned_users'. |
|---|
| 56 | If data['user'] doesn't exist this function will query the list of available users and add them. |
|---|
| 57 | |
|---|
| 58 | :param env: Trac environment object |
|---|
| 59 | :param reviewID: Id of a review |
|---|
| 60 | :param data: |
|---|
| 61 | |
|---|
| 62 | :return: None. Data is added to dict data using keys 'users', 'assigned_users', 'unassigned_users', 'emptyList' |
|---|
| 63 | """ |
|---|
| 64 | if 'users' not in data: |
|---|
| 65 | data['users'] = get_users(env) |
|---|
| 66 | all_users = data['users'] |
|---|
| 67 | |
|---|
| 68 | # get code review data and populate |
|---|
| 69 | reviewers = PeerReviewerModel.select_by_review_id(env, reviewID) |
|---|
| 70 | popUsers = [] |
|---|
| 71 | for reviewer in reviewers: |
|---|
| 72 | popUsers.append(reviewer['reviewer']) |
|---|
| 73 | data['assigned_users'] = popUsers |
|---|
| 74 | |
|---|
| 75 | # Figure out the users that were not included |
|---|
| 76 | # in the previous code review so that they can be |
|---|
| 77 | # added to the dropdown to select more users |
|---|
| 78 | # (only check if all users were not included in previous code review) |
|---|
| 79 | notUsers = [] |
|---|
| 80 | if len(popUsers) != len(all_users): |
|---|
| 81 | notUsers = list(set(all_users)-set(popUsers)) |
|---|
| 82 | data['emptyList'] = 0 |
|---|
| 83 | else: |
|---|
| 84 | data['emptyList'] = 1 |
|---|
| 85 | |
|---|
| 86 | data['unassigned_users'] = notUsers |
|---|
| 87 | |
|---|
| 88 | |
|---|
| 89 | class NewReviewModule(Component): |
|---|
| 90 | """Component handling the creation of code reviews.""" |
|---|
| 91 | |
|---|
| 92 | implements(IRequestHandler, INavigationContributor) |
|---|
| 93 | |
|---|
| 94 | trac_version = get_distribution('trac').version |
|---|
| 95 | legacy_trac = parse_version(trac_version) < parse_version('1.0.0') # True if Trac V0.12.x |
|---|
| 96 | |
|---|
| 97 | # INavigationContributor methods |
|---|
| 98 | |
|---|
| 99 | def get_active_navigation_item(self, req): |
|---|
| 100 | return 'peerReviewMain' |
|---|
| 101 | |
|---|
| 102 | def get_navigation_items(self, req): |
|---|
| 103 | return [] |
|---|
| 104 | |
|---|
| 105 | # IRequestHandler methods |
|---|
| 106 | |
|---|
| 107 | def match_request(self, req): |
|---|
| 108 | return req.path_info == '/peerReviewNew' |
|---|
| 109 | |
|---|
| 110 | def process_request(self, req): |
|---|
| 111 | req.perm.require('CODE_REVIEW_DEV') |
|---|
| 112 | |
|---|
| 113 | if req.method == 'POST': |
|---|
| 114 | oldid = req.args.get('oldid') |
|---|
| 115 | if req.args.get('create'): |
|---|
| 116 | returnid = self.createCodeReview(req, 'create') |
|---|
| 117 | if oldid: |
|---|
| 118 | # Automatically close the review we resubmitted from |
|---|
| 119 | review = PeerReviewModel(self.env, oldid) |
|---|
| 120 | review['status'] = "closed" |
|---|
| 121 | review.save_changes(req.authname, comment="Closed after resubmitting as review '#%s'." % |
|---|
| 122 | returnid) |
|---|
| 123 | add_notice(req, "Review '%s' (#%s) was automatically closed after resubmitting as '#%s'." % |
|---|
| 124 | (review['name'], oldid, returnid)) |
|---|
| 125 | # If no errors then redirect to the viewCodeReview page |
|---|
| 126 | req.redirect(self.env.href.peerReviewView() + '?Review=' + str(returnid)) |
|---|
| 127 | if req.args.get('createfollowup'): |
|---|
| 128 | returnid = self.createCodeReview(req, 'followup') |
|---|
| 129 | # If no errors then redirect to the viewCodeReview page of the new review |
|---|
| 130 | req.redirect(self.env.href.peerReviewView(Review=returnid)) |
|---|
| 131 | if req.args.get('save'): |
|---|
| 132 | self.save_changes(req) |
|---|
| 133 | req.redirect(self.env.href.peerReviewView(Review=oldid)) |
|---|
| 134 | if req.args.get('cancel'): |
|---|
| 135 | req.redirect(self.env.href.peerReviewView(Review=oldid)) |
|---|
| 136 | |
|---|
| 137 | # Handling of GET request |
|---|
| 138 | |
|---|
| 139 | data = {'users': get_users(self.env), |
|---|
| 140 | 'new': "no", |
|---|
| 141 | 'cycle': itertools.cycle, |
|---|
| 142 | 'followup': req.args.get('followup') |
|---|
| 143 | } |
|---|
| 144 | |
|---|
| 145 | is_followup = req.args.get('followup', None) |
|---|
| 146 | review_id = req.args.get('resubmit') |
|---|
| 147 | review = PeerReviewModel(self.env, review_id) |
|---|
| 148 | |
|---|
| 149 | # If we tried resubmitting and the review_id is not a valid number or not a valid code review, error |
|---|
| 150 | if review_id and (not review_id.isdigit() or not review): |
|---|
| 151 | raise TracError("Invalid resubmit ID supplied - unable to load page correctly.", "Resubmit ID error") |
|---|
| 152 | |
|---|
| 153 | if review['status'] == 'closed' and req.args.get('modify'): |
|---|
| 154 | raise TracError("The Review '#%s' is already closed and can't be modified." % review['review_id'], |
|---|
| 155 | "Modify Review error") |
|---|
| 156 | |
|---|
| 157 | # If we are resubmitting a code review, and are neither the author nor the manager |
|---|
| 158 | if review_id and not review['owner'] == req.authname and not 'CODE_REVIEW_MGR' in req.perm: |
|---|
| 159 | raise TracError("You need to be a manager or the author of this code review to resubmit it.", |
|---|
| 160 | "Access error") |
|---|
| 161 | |
|---|
| 162 | # If we are resubmitting a code review and we are the author or the manager |
|---|
| 163 | if review_id and (review['owner'] == req.authname or 'CODE_REVIEW_MGR' in req.perm): |
|---|
| 164 | data['oldid'] = review_id |
|---|
| 165 | |
|---|
| 166 | add_users_to_data(self.env, review_id, data) |
|---|
| 167 | |
|---|
| 168 | rfiles = ReviewFileModel.select_by_review(self.env, review_id) |
|---|
| 169 | popFiles = [] |
|---|
| 170 | # Set up the file information |
|---|
| 171 | for f in rfiles: |
|---|
| 172 | if is_followup: |
|---|
| 173 | # Get the current file and repo revision |
|---|
| 174 | repo = RepositoryManager(self.env).get_repository(f['repo']) |
|---|
| 175 | node, display_rev, context = get_node_from_repo(req, repo, f['path'], None) |
|---|
| 176 | f.curchangerevision = unicode(node.created_rev) |
|---|
| 177 | f.curreporev = repo.youngest_rev |
|---|
| 178 | # We use the current repo revision here so on POST that revision is used for creating |
|---|
| 179 | # the file entry in the database. The POST handler parses the string for necessary information. |
|---|
| 180 | f.id_string = create_id_string(f, repo.youngest_rev) |
|---|
| 181 | else: |
|---|
| 182 | # The id_String holds info like revision, line numbers, path and repo. It is later used to save |
|---|
| 183 | # file info to the database during a post. |
|---|
| 184 | f.id_string = create_id_string(f) |
|---|
| 185 | # This id is used by the javascript code to find duplicate entries. |
|---|
| 186 | f.element_id = create_file_hash_id(f) |
|---|
| 187 | if req.args.get('modify'): |
|---|
| 188 | comments = Comment.select_by_file_id(self.env, f['file_id']) |
|---|
| 189 | f.num_comments = len(comments) or 0 |
|---|
| 190 | popFiles.append(f) |
|---|
| 191 | |
|---|
| 192 | data['name'] = review['name'] |
|---|
| 193 | if req.args.get('modify') or req.args.get('followup'): |
|---|
| 194 | data['notes'] = review['notes'] |
|---|
| 195 | else: |
|---|
| 196 | data['notes'] = "%sReview based on ''%s'' (resubmitted)." %\ |
|---|
| 197 | (review['notes']+ CRLF + CRLF, review['name']) |
|---|
| 198 | data['prevFiles'] = popFiles |
|---|
| 199 | # If we are not resubmitting |
|---|
| 200 | else: |
|---|
| 201 | data['new'] = "yes" |
|---|
| 202 | |
|---|
| 203 | prj = self.env.config.getlist("peerreview", "projects", default=[]) |
|---|
| 204 | if not prj: |
|---|
| 205 | prj = self.env.config.getlist("ticket-custom", "project.options", default=[], sep='|') |
|---|
| 206 | |
|---|
| 207 | data['projects'] = prj |
|---|
| 208 | data['curproj'] = review['project'] |
|---|
| 209 | |
|---|
| 210 | if self.legacy_trac: |
|---|
| 211 | add_script(req, self.env.config.get('trac', 'jquery_ui_location') or |
|---|
| 212 | 'hw/js/jquery-ui-1.11.4.min.js') |
|---|
| 213 | add_stylesheet(req, self.env.config.get('trac', 'jquery_ui_theme_location') or |
|---|
| 214 | 'hw/css/jquery-ui-1.11.4.min.css') |
|---|
| 215 | else: |
|---|
| 216 | Chrome(self.env).add_jquery_ui(req) |
|---|
| 217 | add_stylesheet(req, 'common/css/browser.css') |
|---|
| 218 | add_stylesheet(req, 'common/css/code.css') |
|---|
| 219 | add_stylesheet(req, 'hw/css/peerreview.css') |
|---|
| 220 | add_script(req, 'common/js/auto_preview.js') |
|---|
| 221 | add_script_data(req, {'repo_browser': self.env.href.peerReviewBrowser(), |
|---|
| 222 | 'auto_preview_timeout': self.env.config.get('trac', 'auto_preview_timeout', '2.0'), |
|---|
| 223 | 'form_token': req.form_token, |
|---|
| 224 | 'peer_is_modify': req.args.get('modify', '0'), |
|---|
| 225 | 'peer_is_followup': req.args.get('followup', '0')}) |
|---|
| 226 | add_script(req, "hw/js/peer_review_new.js") |
|---|
| 227 | add_script(req, 'hw/js/peer_user_list.js') |
|---|
| 228 | add_ctxt_nav_items(req) |
|---|
| 229 | return 'peerReviewNew.html', data, None |
|---|
| 230 | |
|---|
| 231 | def createCodeReview(self, req, action): |
|---|
| 232 | """Create a new code review from the data in the request object req. |
|---|
| 233 | |
|---|
| 234 | Takes the information given when the page is posted and creates a |
|---|
| 235 | new code review struct in the database and populates it with the |
|---|
| 236 | information. Also creates new reviewer structs and file structs for |
|---|
| 237 | the review. |
|---|
| 238 | """ |
|---|
| 239 | oldid = req.args.get('oldid', 0) |
|---|
| 240 | review = PeerReviewModel(self.env) |
|---|
| 241 | review['owner'] = req.authname |
|---|
| 242 | review['name'] = req.args.get('Name') |
|---|
| 243 | review['notes'] = req.args.get('Notes') |
|---|
| 244 | if req.args.get('project'): |
|---|
| 245 | review['project'] = req.args.get('project') |
|---|
| 246 | if oldid: |
|---|
| 247 | # Resubmit or follow up |
|---|
| 248 | if action == 'followup': |
|---|
| 249 | review['parent_id'] = oldid |
|---|
| 250 | else: |
|---|
| 251 | # Keep parent -> follow up relationship when resubmitting |
|---|
| 252 | old_review = PeerReviewModel(self.env, oldid) |
|---|
| 253 | review['parent_id'] = old_review['parent_id'] |
|---|
| 254 | review.insert() |
|---|
| 255 | id_ = review['review_id'] |
|---|
| 256 | self.log.debug('New review created: %s', id_) |
|---|
| 257 | |
|---|
| 258 | # loop here through all the reviewers |
|---|
| 259 | # and create new reviewer structs based on them |
|---|
| 260 | user = req.args.get('user', []) |
|---|
| 261 | if not type(user) is list: |
|---|
| 262 | user = [user] |
|---|
| 263 | for name in user: |
|---|
| 264 | if name != "": |
|---|
| 265 | reviewer = PeerReviewerModel(self.env) |
|---|
| 266 | reviewer['review_id'] = id_ |
|---|
| 267 | reviewer['reviewer'] = name |
|---|
| 268 | reviewer['vote'] = -1 |
|---|
| 269 | reviewer.insert() |
|---|
| 270 | |
|---|
| 271 | # loop here through all included files |
|---|
| 272 | # and create new file structs based on them |
|---|
| 273 | files = req.args.get('file', []) |
|---|
| 274 | if not type(files) is list: |
|---|
| 275 | files = [files] |
|---|
| 276 | for item in files: |
|---|
| 277 | segment = item.split(',') |
|---|
| 278 | rfile = ReviewFileModel(self.env) |
|---|
| 279 | rfile['review_id'] = id_ |
|---|
| 280 | rfile['path'] = segment[0] |
|---|
| 281 | rfile['revision'] = segment[1] # If we create a followup review this is the current repo revision |
|---|
| 282 | rfile['line_start'] = segment[2] |
|---|
| 283 | rfile['line_end'] = segment[3] |
|---|
| 284 | rfile['repo'] = segment[4] |
|---|
| 285 | repo = RepositoryManager(self.env).get_repository(rfile['repo']) |
|---|
| 286 | node, display_rev, context = get_node_from_repo(req, repo, rfile['path'], rfile['revision']) |
|---|
| 287 | rfile['changerevision'] = unicode(node.created_rev) |
|---|
| 288 | rfile['hash'] = self._hash_from_file_node(node) |
|---|
| 289 | rfile.insert() |
|---|
| 290 | return id_ |
|---|
| 291 | |
|---|
| 292 | def _hash_from_file_node(self, node): |
|---|
| 293 | return hash_from_file_node(node) |
|---|
| 294 | |
|---|
| 295 | def save_changes(self, req): |
|---|
| 296 | def file_is_commented(author): |
|---|
| 297 | rfiles = ReviewFileModel.select_by_review(self.env, review['review_id']) |
|---|
| 298 | for f in rfiles: |
|---|
| 299 | comments = [c for c in Comment.select_by_file_id(self.env, f['file_id']) if c.author == author] |
|---|
| 300 | if comments: |
|---|
| 301 | return True |
|---|
| 302 | return False |
|---|
| 303 | |
|---|
| 304 | review = PeerReviewModel(self.env, req.args.get('oldid')) |
|---|
| 305 | review['name'] = req.args.get('Name') |
|---|
| 306 | review['notes'] = req.args.get('Notes') |
|---|
| 307 | review['project'] = req.args.get('project') |
|---|
| 308 | review.save_changes(req.authname) |
|---|
| 309 | |
|---|
| 310 | user = req.args.get('user') |
|---|
| 311 | if not type(user) is list: |
|---|
| 312 | user = [user] |
|---|
| 313 | data = {} |
|---|
| 314 | add_users_to_data(self.env,review['review_id'], data) |
|---|
| 315 | # Handle new users if any |
|---|
| 316 | new_users = list(set(user) - set(data['assigned_users'])) |
|---|
| 317 | for name in new_users: |
|---|
| 318 | if name != "": |
|---|
| 319 | reviewer = PeerReviewerModel(self.env) |
|---|
| 320 | reviewer['review_id'] = review['review_id'] |
|---|
| 321 | reviewer['reviewer'] = name |
|---|
| 322 | reviewer['vote'] = -1 |
|---|
| 323 | reviewer.insert() |
|---|
| 324 | # Handle removed users if any |
|---|
| 325 | rem_users = list(set(data['assigned_users']) - set(user)) |
|---|
| 326 | for name in rem_users: |
|---|
| 327 | if name != "": |
|---|
| 328 | reviewer = Reviewer(self.env, review['review_id'], name) |
|---|
| 329 | if file_is_commented(name): |
|---|
| 330 | add_warning(req, "User '%s' already commented a file. Not removed from review '#%s'", |
|---|
| 331 | name, review['review_id']) |
|---|
| 332 | continue |
|---|
| 333 | reviewer.delete() |
|---|
| 334 | |
|---|
| 335 | # Handle file removal |
|---|
| 336 | new_files = req.args.get('file') |
|---|
| 337 | if not type(new_files) is list: |
|---|
| 338 | new_files = [new_files] |
|---|
| 339 | old_files = [] |
|---|
| 340 | rfiles = {} |
|---|
| 341 | for f in ReviewFileModel.select_by_review(self.env, review['review_id']): |
|---|
| 342 | fid = u"%s,%s,%s,%s,%s" % (f['path'], f['revision'], f['line_start'], f['line_end'], f['repo']) |
|---|
| 343 | old_files.append(fid) |
|---|
| 344 | rfiles[fid] = f |
|---|
| 345 | |
|---|
| 346 | rem_files = list(set(old_files) - set(new_files)) |
|---|
| 347 | for fid in rem_files: |
|---|
| 348 | rfiles[fid].delete() |
|---|