source: peerreviewplugin/tags/0.12/3.1/codereview/peerReviewNew.py

Last change on this file was 17265, checked in by Cinc-th, 5 years ago

PeerReviewPlugin: added some documentation to plugins, added comments and fixed typos.

File size: 15.0 KB
Line 
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
14import itertools
15import time
16from pkg_resources import get_distribution, parse_version
17from trac import util
18from trac.core import Component, implements, TracError
19from trac.util.text import CRLF
20from trac.web.chrome import INavigationContributor, add_script, add_script_data, \
21    add_warning, add_notice, add_stylesheet, Chrome
22from trac.web.main import IRequestHandler
23from trac.versioncontrol.api import RepositoryManager
24from CodeReviewStruct import *
25from model import Comment, get_users, \
26    PeerReviewerModel, PeerReviewModel, Reviewer, ReviewFileModel
27from peerReviewMain import add_ctxt_nav_items
28from repobrowser import get_node_from_repo
29from .repo import hash_from_file_node
30
31
32def 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
40def 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
47def create_file_hash_id(f):
48    return 'id%s' % java_string_hashcode(create_id_string(f))
49
50
51def 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
89class 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()
Note: See TracBrowser for help on using the repository browser.