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
Line 
1#
2# Copyright (C) 2005-2006 Team5
3# Copyright (C) 2016-2021 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 peerreview_new.html
13
14import itertools
15from trac.core import Component, implements, TracError
16from trac.util.text import CRLF, to_unicode
17from trac.web.chrome import INavigationContributor, add_script, add_script_data, \
18    add_warning, add_notice, add_stylesheet, Chrome
19from trac.web.main import IRequestHandler
20from trac.versioncontrol.api import RepositoryManager
21from .model import Comment, get_users, \
22    PeerReviewerModel, PeerReviewModel, ReviewFileModel
23from .peerReviewMain import add_ctxt_nav_items
24from .repobrowser import get_node_from_repo
25from .repo import hash_from_file_node
26
27
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
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']
39    return "%s,%s,%s,%s,%s" %\
40           (f['path'], f_rev, f['line_start'], f['line_end'], f['repo'])
41
42
43def create_file_hash_id(f):
44    return 'id%s' % java_string_hashcode(create_id_string(f))
45
46
47def add_users_to_data(env, reviewID, data):
48    """Add user, assigned and unassigned users to dict data.
49
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
56    :param data:
57
58    :return: None. Data is added to dict data using keys 'users', 'assigned_users', 'unassigned_users', 'emptyList'
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
65    reviewers = PeerReviewerModel.select_by_review_id(env, reviewID)
66    popUsers = []
67    for reviewer in reviewers:
68        popUsers.append(reviewer['reviewer'])
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
85class NewReviewModule(Component):
86    """Component handling the creation of code reviews.
87
88    [[BR]]
89    This component handles the creation of a new review and creation of followup reviews.
90    """
91
92    implements(IRequestHandler, INavigationContributor)
93
94    # INavigationContributor methods
95
96    def get_active_navigation_item(self, req):
97        return 'peerreviewmain'
98
99    def get_navigation_items(self, req):
100        return []
101
102    # IRequestHandler methods
103
104    def match_request(self, req):
105
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
114    def process_request(self, req):
115        req.perm.require('CODE_REVIEW_DEV')
116
117        if req.method == 'POST':
118            oldid = req.args.get('oldid')
119            if req.args.get('create'):
120                returnid = self.createCodeReview(req, 'create')
121                if oldid:
122                    # Automatically close the review we resubmitted from
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))
129                # If no errors then redirect to the viewCodeReview page
130                req.redirect(req.href.peerreviewview(returnid))
131            if req.args.get('createfollowup'):
132                returnid = self.createCodeReview(req, 'followup')
133                # If no errors then redirect to the viewCodeReview page of the new review
134                req.redirect(req.href.peerreviewview(returnid))
135            if req.args.get('save'):
136                self.save_changes(req)
137                req.redirect(req.href.peerreviewview(oldid))
138            if req.args.get('cancel'):
139                req.redirect(req.href.peerreviewview(oldid))
140
141        # Handling of GET request
142
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)
150        review_id = req.args.get('resubmit')
151        review = PeerReviewModel(self.env, review_id)
152
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):
155            raise TracError("Invalid resubmit ID supplied - unable to load page correctly.", "Resubmit ID error")
156
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'],
159                            "Modify Review error")
160
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
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
169
170            add_users_to_data(self.env, review_id, data)
171
172            rfiles = ReviewFileModel.select_by_review(self.env, review_id)
173            popFiles = []
174            # Set up the file information
175            for f in rfiles:
176                repo = RepositoryManager(self.env).get_repository(f['repo'])
177                f['display_rev'] = repo.display_rev
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)
181                    f.curchangerevision = to_unicode(node.created_rev)
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)
190                # This id is used by the javascript code to find duplicate entries.
191                f.element_id = create_file_hash_id(f)
192                if req.args.get('modify'):
193                    comments = Comment.select_by_file_id(self.env, f['file_id'])
194                    f.num_comments = len(comments) or 0
195                popFiles.append(f)
196
197            data['name'] = review['name']
198            if req.args.get('modify'):
199                data['notes'] = review['notes']
200            elif  req.args.get('followup'):
201                data['notes'] = "%sReview is followup to review ''%s''." % \
202                                (review['notes']+ CRLF, review['name'])
203            else:
204                data['notes'] = "%sReview based on ''%s'' (resubmitted)." %\
205                                (review['notes']+ CRLF, review['name'])
206            data['prevFiles'] = popFiles
207        # If we are not resubmitting
208        else:
209            data['new'] = "yes"
210
211        prj = self.env.config.getlist("peerreview", "projects", default=[])
212        if not prj:
213            prj = self.env.config.getlist("ticket-custom", "project.options", default=[], sep='|')
214
215        data['projects'] = prj
216        data['curproj'] = review['project']
217
218        Chrome(self.env).add_jquery_ui(req)
219        add_stylesheet(req, 'common/css/browser.css')
220        add_stylesheet(req, 'common/css/code.css')
221        add_stylesheet(req, 'hw/css/peerreview.css')
222        add_script(req, 'common/js/auto_preview.js')
223        add_script_data(req, {'repo_browser': req.href.peerReviewBrowser(),
224                              'auto_preview_timeout': self.env.config.get('trac', 'auto_preview_timeout', '2.0'),
225                              'form_token': req.form_token,
226                              'peer_is_modify': req.args.get('modify', '0'),
227                              'peer_is_followup': req.args.get('followup', '0')})
228        add_script(req, "hw/js/peer_review_new.js")
229        add_script(req, 'hw/js/peer_user_list.js')
230        add_ctxt_nav_items(req)
231        if hasattr(Chrome, 'jenv'):
232            return 'peerreview_new_jinja.html', data
233        else:
234            return 'peerreview_new.html', data, None
235
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        """
244        oldid = req.args.get('oldid', 0)
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'):
250            review['project'] = req.args.get('project')
251        if oldid:
252            # Resubmit or follow up
253            if action == 'followup':
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']
259        review.insert()
260        id_ = review['review_id']
261        self.log.debug('New review created: %s', id_)
262
263        # loop here through all the reviewers
264        # and create new reviewer structs based on them
265        user = req.args.getlist('user')
266        if not type(user) is list:
267            user = [user]
268        for name in user:
269            if name != "":
270                reviewer = PeerReviewerModel(self.env)
271                reviewer['review_id'] = id_
272                reviewer['reviewer'] = name
273                reviewer['vote'] = -1
274                reviewer.insert()
275
276        # loop here through all included files
277        # and create new file structs based on them
278        files = req.args.getlist('file')
279        if not type(files) is list:
280            files = [files]
281        for item in files:
282            segment = item.split(',')
283            rfile = ReviewFileModel(self.env)
284            rfile['review_id'] = id_
285            rfile['path'] = '/' + segment[0].lstrip('/')  # We save the path with leading '/' for historical reasons
286            rfile['revision'] = segment[1]  # If we create a followup review this is the current repo revision
287            rfile['line_start'] = segment[2]
288            rfile['line_end'] = segment[3]
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'])
292            rfile['changerevision'] = to_unicode(node.created_rev)
293            rfile['hash'] = self._hash_from_file_node(node)
294            rfile.insert()
295        return id_
296
297    def _hash_from_file_node(self, node):
298        return hash_from_file_node(node)
299
300    def save_changes(self, req):
301        def file_is_commented(author):
302            rfiles = ReviewFileModel.select_by_review(self.env, review['review_id'])
303            for f in rfiles:
304                comments = [c for c in Comment.select_by_file_id(self.env, f['file_id']) if c.author == author]
305                if comments:
306                    return True
307            return False
308
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)
314
315        user = req.args.getlist('user')
316        data = {}
317        add_users_to_data(self.env,review['review_id'], data)
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 != "":
322                reviewer = PeerReviewerModel(self.env)
323                reviewer['review_id'] = review['review_id']
324                reviewer['reviewer'] = name
325                reviewer['vote'] = -1
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'",
333                                name, review['review_id'])
334                else:
335                    PeerReviewerModel.delete_by_review_id_and_name(self.env, review['review_id'], name)
336
337        # Handle file removal
338        new_files = req.args.getlist('file')
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()
Note: See TracBrowser for help on using the repository browser.