source: peerreviewplugin/trunk/codereview/peerReviewNew.py

Last change on this file was 18278, checked in by Cinc-th, 2 years ago

PeerReviewPlugin: use display_rev() to render short revisions in more places. Esp. useful when using git repositories.

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