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

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

PeerReviewPlugin: record timestamp when finishing a review. The status considered as a terminal status are closed, approved, disapproved (unless configuration was changed). The finishing date is shown in the UI.

File size: 14.3 KB
Line 
1#
2# Copyright (C) 2005-2006 Team5
3# All rights reserved.
4#
5# This software is licensed as described in the file COPYING.txt, which
6# you should have received as part of this distribution.
7#
8# Author: Team5
9#
10
11# Provides functionality for view code review page
12# Works with peerReviewView.html
13
14import itertools
15from string import Template
16from trac.config import BoolOption, ListOption
17from trac.core import Component, implements, TracError
18from trac.mimeview import Context
19from trac.mimeview.api import Mimeview
20from trac.resource import Resource
21from trac.util import format_date
22from trac.util.html import html as tag
23from trac.util.text import CRLF, obfuscate_email_address
24from trac.web.chrome import add_link, add_stylesheet, Chrome, INavigationContributor
25from trac.web.main import IRequestHandler
26from trac.wiki.formatter import format_to, format_to_html
27from model import Comment, get_users, \
28    PeerReviewerModel, PeerReviewModel, ReviewFileModel, ReviewDataModel
29from peerReviewMain import add_ctxt_nav_items, web_context_compat
30from tracgenericworkflow.api import IWorkflowOperationProvider, IWorkflowTransitionListener, ResourceWorkflowSystem
31from util import review_is_finished, review_is_locked
32
33try:
34    from trac.web.chrome import web_context
35except ImportError:
36    web_context = web_context_compat
37
38
39class PeerReviewView(Component):
40    """Displays a summary page for a review.
41
42    === The following configuration options may be set:
43
44    [[TracIni(peerreview)]]
45    """
46    implements(IRequestHandler, INavigationContributor, IWorkflowOperationProvider, IWorkflowTransitionListener)
47
48    ListOption("peerreview", "terminal_review_states", ['closed', 'approved', 'disapproved'],
49               doc="Ending states for a review. Only an administrator may force a review to leave these states. "
50                   "Reviews in one of these states may not be modified.")
51
52    ListOption("peerreview", "reviewer_locked_states", ['reviewed'],
53               doc="A reviewer may no longer comment on reviews in one of the given states. The review owner still "
54                   "may comment. Used to lock a review against modification after all reviewing persons have "
55                   "finished their task.")
56
57    show_ticket = BoolOption("peerreview", "show_ticket", False,
58                             doc="A ticket may be created with information about "
59                                 "a review. If set to {{{True}}} a ticket preview on "
60                                 "the view page of a review will be shown and a button "
61                                 "for filling the '''New Ticket''' page with data. "
62                                 "The review must have status ''reviewed''. Only the "
63                                 "author or a manager have the necessary permisisons.")
64
65    # IWorkflowOperationProvider methods
66
67    def get_implemented_operations(self):
68        yield 'set_review_owner'
69
70    def get_operation_control(self, req, action, operation, res_wf_state, resource):
71        """Get markup for workflow operation 'set_review_owner."""
72
73        id_ = 'action_%s_operation_%s' % (action, operation)
74
75        rws = ResourceWorkflowSystem(self.env)
76        this_action = rws.actions[resource.realm][action]  # We need the full action data for custom label
77
78        if operation == 'set_review_owner':
79            self.log.debug("Creating control for setting review owner.")
80            review = PeerReviewModel(self.env, resource.id)
81
82            if not (Chrome(self.env).show_email_addresses
83                    or 'EMAIL_VIEW' in req.perm(resource)):
84                format_user = obfuscate_email_address
85            else:
86                format_user = lambda address: address
87            current_owner = format_user(review['owner'])
88
89            self.log.debug("Current owner is %s." % current_owner)
90
91            selected_owner = req.args.get(id_, req.authname)
92
93            hint = "The owner will be changed from %s" % current_owner
94
95            owners = get_users(self.env)
96            if not owners:
97                owner = req.args.get(id_, req.authname)
98                control = tag(u'%s ' % this_action['name'],
99                              tag.input(type='text', id=id_,
100                                        name=id_, value=owner))
101            elif len(owners) == 1:
102                owner = tag.input(type='hidden', id=id_, name=id_,
103                                  value=owners[0])
104                formatted_owner = format_user(owners[0])
105                control = tag(u'%s ' % this_action['name'],
106                              tag(formatted_owner, owner))
107                if res_wf_state['owner'] != owners[0]:
108                    hint = "The owner will be changed from %s to %s" % (current_owner, formatted_owner)
109            else:
110                control = tag(u'%s ' % this_action['name'], tag.select(
111                        [tag.option(format_user(x), value=x,
112                                    selected=(x == selected_owner or None))
113                         for x in owners],
114                        id=id_, name=id_))
115
116            return control, hint
117
118        return None, ''
119
120    def perform_operation(self, req, action, operation, old_state, new_state, res_wf_state, resource):
121        """Perform 'set_review_owner' operation on reviews."""
122
123        self.log.debug("---> Performing operation %s while transitioning from %s to %s."
124                       % (operation, old_state, new_state))
125        new_owner = req.args.get('action_%s_operation_%s' % (action, operation), None)
126        review = PeerReviewModel(self.env, int(resource.id))
127        if review:
128            review['owner'] = new_owner
129            review.save_changes(author=req.authname)
130
131    # IWorkflowTransitionListener
132
133    def object_transition(self, res_wf_state, resource, action, old_state, new_state):
134        if resource.realm == 'peerreviewer':
135            reviewer = PeerReviewerModel(self.env, resource.id)
136            reviewer['status'] = new_state
137            # This is for updating the PeerReviewer object. The info is used for displaying the review state to the
138            # review author. Note that this change is recorded in 'peerreviewer_change' by this.
139            # The change is also recorded in the generic change table 'resourceworkflow_change' because of the
140            # Resource object 'resource'.
141            reviewer.save_changes(author=res_wf_state.authname,
142                                  comment="State change to %s  for %s from object_transition() of %s"
143                                                                        % (new_state, reviewer, resource))
144        elif resource.realm == 'peerreview':
145            review = PeerReviewModel(self.env, resource.id)
146            review.change_status(new_state, res_wf_state.authname)
147
148    # INavigationContributor
149
150    def get_active_navigation_item(self, req):
151        return 'peerReviewMain'
152
153    def get_navigation_items(self, req):
154        return []
155
156    # IRequestHandler methods
157    def match_request(self, req):
158        return req.path_info == '/peerReviewView'
159
160    def process_request(self, req):
161        def get_review_by_id(review_id):
162            """Get a PeerReviewModel for the given review id and prepare some additional data used by the template"""
163            review = PeerReviewModel(self.env, review_id)
164            review.html_notes = format_to_html(self.env, Context.from_request(req), review['notes'])
165            review.date = format_date(review['created'])
166            if review['closed']:
167                review.finish_date = format_date(review['closed'])
168            else:
169                review.finish_date = ''
170            return review
171        def get_files_for_review_id(review_id, comments=False):
172            """Get all files belonging to the given review id. Provide the number of comments if asked for."""
173            rfm = ReviewFileModel(self.env)
174            rfm.clear_props()
175            rfm['review_id'] = review_id
176            rev_files = list(rfm.list_matching_objects())
177            if comments:
178                for f in rev_files:
179                    f.num_comments = len(Comment.select_by_file_id(self.env, f['file_id']))
180                    my_comment_data = ReviewDataModel.comments_for_file_and_owner(self.env, f['file_id'], req.authname)
181                    f.num_notread = f.num_comments - len([c_id for c_id, t, dat in my_comment_data if t == 'read'])
182            return rev_files
183
184        req.perm.require('CODE_REVIEW_DEV')
185
186        data = {}
187        # check to see if the user is a manager of this page or not
188        if 'CODE_REVIEW_MGR' in req.perm:
189            data['manager'] = True
190
191        # review_id argument checking
192        review_id = req.args.get('Review')
193        if review_id is None or not review_id.isdigit():
194            raise TracError(u"Invalid review ID supplied - unable to load page.")
195
196        if req.method == 'POST':
197            if req.args.get('resubmit'):
198                req.redirect(self.env.href.peerReviewNew(resubmit=review_id))
199            elif req.args.get('followup'):
200                req.redirect(self.env.href.peerReviewNew(resubmit=review_id, followup=1))
201            elif req.args.get('modify'):
202                req.redirect(self.env.href.peerReviewNew(resubmit=review_id, modify=1))
203
204        data['review_files'] = get_files_for_review_id(review_id, True)
205        data['users'] = get_users(self.env)
206        data['show_ticket'] = self.show_ticket
207
208        review = get_review_by_id(review_id)
209        data['review'] = review
210
211        # A finished review can't be changed anymore except by a manager
212        data['is_finished'] = review_is_finished(self.env.config, review)
213        # A user can't change his voting for a reviewed review
214        data['review_locked'] = review_is_locked(self.env.config, review, req.authname)
215        # Used to indicate that a review is done. 'review_locked' is not suitable because it is false for the
216        # author of a review even when the review is done.
217        data['review_done'] = review_is_locked(self.env.config, review)
218        data['finished_states_str'] = ', '.join(self.env.config.getlist("peerreview", "terminal_review_states"))
219        # Parent review if any
220        if review['parent_id'] != 0:
221            data['parent_review'] = get_review_by_id(review['parent_id'])
222            par_files = get_files_for_review_id(review['parent_id'], False)
223            data['parent_files'] = par_files
224
225            # Map files to parent files. Key is current file id, value is parent file object
226
227            def get_parent_file(rfile, par_files):
228                fid = u"%s%s%s" % (rfile['path'], rfile['line_start'], rfile['line_end'])
229                for f in par_files:
230                    tmp = u"%s%s%s" % (f['path'], f['line_start'], f['line_end'])
231                    if tmp == fid:
232                        return f
233                return None
234            file_map = {}
235            for f in data['review_files']:
236                file_map[f['file_id']] = get_parent_file(f, par_files)
237
238            data['file_map'] = file_map
239
240        self.create_ticket_data(req, data)
241        url = '.'
242        # Actions for a reviewer. Each reviewer marks his progress on a review. The author
243        # can see this progress in the user list. The possible actions are defined in trac.ini
244        # as a workflow in [peerreviewer-resource_workflow]
245        realm = 'peerreviewer'
246        res = None
247
248        rm = PeerReviewerModel(self.env)
249        rm.clear_props()
250        rm['review_id'] = review_id
251        reviewers = list(rm.list_matching_objects())
252        data['reviewer'] = reviewers
253
254        for reviewer in reviewers:
255            if reviewer['reviewer'] == req.authname:
256                res = Resource(realm, str(reviewer['reviewer_id']))  # id must be a string
257                if not data['is_finished'] and not data['review_done']:  # even author isn't allowed to change
258                    data['canivote'] = True
259                break
260        if res:
261            data['reviewer_workflow'] = ResourceWorkflowSystem(self.env).\
262                get_workflow_markup(req, url, realm, res, {'redirect': req.href.peerReviewView(Review=review_id)})
263
264        # Actions for the author of a review. The author may approve, disapprove or close a review.
265        # The possible actions are defined in trac.ini as a workflow in [peerreview-resource_workflow]
266        realm = 'peerreview'
267        res = Resource(realm, str(review['review_id']))  # Must be a string
268        data['workflow'] = ResourceWorkflowSystem(self.env).\
269            get_workflow_markup(req, url, realm, res, {'redirect': req.href.peerReviewView(Review=review_id)})
270
271        data['cycle'] = itertools.cycle
272
273        add_stylesheet(req, 'common/css/code.css')
274        add_stylesheet(req, 'common/css/browser.css')
275        add_stylesheet(req, 'common/css/ticket.css')
276        add_stylesheet(req, 'hw/css/peerreview.css')
277        add_ctxt_nav_items(req)
278
279        # For downloading in docx format
280        conversions = Mimeview(self.env).get_supported_conversions('text/x-trac-peerreview')
281        for key, name, ext, mime_in, mime_out, q, c in conversions:
282            conversion_href = req.href("peerreview", format=key, reviewid=review_id)
283            add_link(req, 'alternate', conversion_href, name, mime_out)
284
285        return 'peerReviewView.html', data, None
286
287    desc = u"""
288Review [/peerReviewView?Review=${review_id} ${review_name}] is finished.
289=== Review
290
291||= Name =|| ${review_name} ||
292||= ID =|| ${review_id} ||
293[[br]]
294**Review Notes:**
295${review_notes}
296
297=== Files
298||= File name =||= Comments =||
299"""
300
301    def create_ticket_data(self, req, data):
302        """Create the ticket description for tickets created from the review page"""
303        txt = u""
304        review = data['review']
305        tmpl = Template(self.desc)
306        txt = tmpl.substitute(review_name=review['name'], review_id=review['review_id'],
307                                   review_notes="")  #review['notes'])
308
309        try:
310            for f in data['review_files']:
311                txt += u"||[/peerReviewPerform?IDFile=%s %s]|| %s ||%s" % \
312                       (f['file_id'], f['path'], f.num_comments, CRLF)
313        except KeyError:
314            pass
315
316        data['ticket_desc_wiki'] = self.create_preview(req, txt)
317        data['ticket_desc'] = txt
318        data['ticket_summary'] = u'Problems with Review "%s"'% review['name']
319
320    def create_preview(self, req, text):
321        resource = Resource('peerreview')
322        context = web_context(req, resource)
323        return format_to_html(self.env, context, text)
Note: See TracBrowser for help on using the repository browser.