| 1 | # |
|---|
| 2 | # Copyright (C) 2005-2006 Team5 |
|---|
| 3 | # Copyright (C) 2016-2021 Cinc |
|---|
| 4 | # All rights reserved. |
|---|
| 5 | # |
|---|
| 6 | # This software is licensed as described in the file COPYING.txt, which |
|---|
| 7 | # you should have received as part of this distribution. |
|---|
| 8 | # |
|---|
| 9 | # Author: Team5 |
|---|
| 10 | # |
|---|
| 11 | |
|---|
| 12 | # Provides functionality for view code review page |
|---|
| 13 | # Works with peerreview_view.html |
|---|
| 14 | |
|---|
| 15 | import itertools |
|---|
| 16 | import re |
|---|
| 17 | from codereview.changeset import get_changeset_data |
|---|
| 18 | from codereview.model import Comment, get_users, \ |
|---|
| 19 | PeerReviewerModel, PeerReviewModel, ReviewCommentModel, ReviewDataModel, ReviewFileModel |
|---|
| 20 | from codereview.peerReviewMain import add_ctxt_nav_items |
|---|
| 21 | from codereview.tracgenericworkflow.api import IWorkflowOperationProvider, IWorkflowTransitionListener, \ |
|---|
| 22 | ResourceWorkflowSystem |
|---|
| 23 | from codereview.util import get_changeset_html, get_files_for_review_id, review_is_finished, review_is_locked |
|---|
| 24 | from string import Template |
|---|
| 25 | from trac.config import BoolOption, ListOption |
|---|
| 26 | from trac.core import Component, implements, TracError |
|---|
| 27 | from trac.mimeview.api import Mimeview |
|---|
| 28 | from trac.resource import Resource |
|---|
| 29 | from trac.util.datefmt import format_date, to_datetime, user_time |
|---|
| 30 | from trac.util.html import html as tag |
|---|
| 31 | from trac.util.text import CRLF, obfuscate_email_address |
|---|
| 32 | from trac.util.translation import _ |
|---|
| 33 | from trac.versioncontrol.api import RepositoryManager |
|---|
| 34 | from trac.web.chrome import add_link, add_script, add_stylesheet, Chrome, INavigationContributor, web_context |
|---|
| 35 | from trac.web.main import IRequestHandler |
|---|
| 36 | from trac.wiki.model import WikiPage |
|---|
| 37 | from trac.wiki.formatter import format_to_html |
|---|
| 38 | |
|---|
| 39 | |
|---|
| 40 | class PeerReviewView(Component): |
|---|
| 41 | """Displays a summary page for a review. |
|---|
| 42 | |
|---|
| 43 | === Configuration |
|---|
| 44 | |
|---|
| 45 | [[TracIni(peerreview)]] |
|---|
| 46 | |
|---|
| 47 | === Ticket template |
|---|
| 48 | |
|---|
| 49 | If the review status is in the list defined by {{{show_ticket}}} |
|---|
| 50 | you may create a ticket |
|---|
| 51 | prefilled with review details from within the review page. This can be |
|---|
| 52 | used to inform team members about pending work or a succesful review. |
|---|
| 53 | |
|---|
| 54 | The ticket description is defined in the wiki page {{{CodeReview/TicketTemplate}}}. |
|---|
| 55 | If this page doesn't exists a default description is used. The following |
|---|
| 56 | variables can be used in the ticket template: |
|---|
| 57 | |
|---|
| 58 | || **${review_id}** || will be replaced with the review id || |
|---|
| 59 | || **${review_name}** || name of the current review || |
|---|
| 60 | || **${review_notes}** || notes of the current review || |
|---|
| 61 | || **${review_files}** || a table with files belonging to the review || |
|---|
| 62 | |
|---|
| 63 | The inserted file table is a wiki table with two columns like this: |
|---|
| 64 | {{{ |
|---|
| 65 | || path/to/file || number of comments || |
|---|
| 66 | || ... || ... || |
|---|
| 67 | }}} |
|---|
| 68 | |
|---|
| 69 | You may use the following wiki formatting for a file table with custom headers: |
|---|
| 70 | {{{ |
|---|
| 71 | ||= File path =||= Comments =|| |
|---|
| 72 | ${review_files} |
|---|
| 73 | }}} |
|---|
| 74 | """ |
|---|
| 75 | implements(IRequestHandler, INavigationContributor, IWorkflowOperationProvider, IWorkflowTransitionListener) |
|---|
| 76 | |
|---|
| 77 | ListOption("peerreview", "terminal_review_states", ['closed', 'approved', 'disapproved'], |
|---|
| 78 | doc="Ending states for a review. Only an administrator may force a review to leave these states. " |
|---|
| 79 | "Reviews in one of these states may not be modified.") |
|---|
| 80 | |
|---|
| 81 | ListOption("peerreview", "reviewer_locked_states", ['reviewed'], |
|---|
| 82 | doc="A reviewer may no longer comment on reviews in one of the given states. The review owner still " |
|---|
| 83 | "may comment. Used to lock a review against modification after all reviewing persons have " |
|---|
| 84 | "finished their task.") |
|---|
| 85 | |
|---|
| 86 | show_ticket = ListOption("peerreview", "show_ticket", ['reviewed', 'in-review', 'approved', 'disapproved'], |
|---|
| 87 | doc="A ticket may be created from within the review page with information about " |
|---|
| 88 | "a review. A ticket preview " |
|---|
| 89 | "and a button for filling the '''New Ticket''' page with data will be shown " |
|---|
| 90 | "on the view page of a review if the review status is in this list " |
|---|
| 91 | "of states. [[BR]][[BR]]" |
|---|
| 92 | "You must be the review author or a manager to use this feature.") |
|---|
| 93 | |
|---|
| 94 | peerreview_view_re = re.compile(r'/peerreviewview/([0-9]+)$') |
|---|
| 95 | |
|---|
| 96 | # used to build the url inside a workflow. Must be changed, when the depth of the url of this page changes. |
|---|
| 97 | workflow_base_href = '..' # account for trailing review id in url |
|---|
| 98 | |
|---|
| 99 | # IWorkflowOperationProvider methods |
|---|
| 100 | |
|---|
| 101 | def get_implemented_operations(self): |
|---|
| 102 | yield 'set_review_owner' |
|---|
| 103 | |
|---|
| 104 | def get_operation_control(self, req, action, operation, res_wf_state, resource): |
|---|
| 105 | """Get markup for workflow operation 'set_review_owner.""" |
|---|
| 106 | |
|---|
| 107 | id_ = 'action_%s_operation_%s' % (action, operation) |
|---|
| 108 | |
|---|
| 109 | rws = ResourceWorkflowSystem(self.env) |
|---|
| 110 | this_action = rws.actions[resource.realm][action] # We need the full action data for custom label |
|---|
| 111 | |
|---|
| 112 | if operation == 'set_review_owner': |
|---|
| 113 | self.log.debug("Creating control for setting review owner.") |
|---|
| 114 | review = PeerReviewModel(self.env, resource.id) |
|---|
| 115 | |
|---|
| 116 | if not (Chrome(self.env).show_email_addresses |
|---|
| 117 | or 'EMAIL_VIEW' in req.perm(resource)): |
|---|
| 118 | format_user = obfuscate_email_address |
|---|
| 119 | else: |
|---|
| 120 | format_user = lambda address: address |
|---|
| 121 | current_owner = format_user(review['owner']) |
|---|
| 122 | |
|---|
| 123 | self.log.debug("Current owner is %s." % current_owner) |
|---|
| 124 | |
|---|
| 125 | selected_owner = req.args.get(id_, req.authname) |
|---|
| 126 | |
|---|
| 127 | hint = "The owner will be changed from %s" % current_owner |
|---|
| 128 | |
|---|
| 129 | owners = get_users(self.env) |
|---|
| 130 | if not owners: |
|---|
| 131 | owner = req.args.get(id_, req.authname) |
|---|
| 132 | control = tag(u'%s ' % this_action['name'], |
|---|
| 133 | tag.input(type='text', id=id_, |
|---|
| 134 | name=id_, value=owner)) |
|---|
| 135 | elif len(owners) == 1: |
|---|
| 136 | owner = tag.input(type='hidden', id=id_, name=id_, |
|---|
| 137 | value=owners[0]) |
|---|
| 138 | formatted_owner = format_user(owners[0]) |
|---|
| 139 | control = tag(u'%s ' % this_action['name'], |
|---|
| 140 | tag(formatted_owner, owner)) |
|---|
| 141 | if res_wf_state['owner'] != owners[0]: |
|---|
| 142 | hint = "The owner will be changed from %s to %s" % (current_owner, formatted_owner) |
|---|
| 143 | else: |
|---|
| 144 | control = tag(u'%s ' % this_action['name'], tag.select( |
|---|
| 145 | [tag.option(format_user(x), value=x, |
|---|
| 146 | selected=(x == selected_owner or None)) |
|---|
| 147 | for x in owners], |
|---|
| 148 | id=id_, name=id_)) |
|---|
| 149 | |
|---|
| 150 | return control, hint |
|---|
| 151 | |
|---|
| 152 | return None, '' |
|---|
| 153 | |
|---|
| 154 | def perform_operation(self, req, action, operation, old_state, new_state, res_wf_state, resource): |
|---|
| 155 | """Perform 'set_review_owner' operation on reviews.""" |
|---|
| 156 | |
|---|
| 157 | self.log.debug("---> Performing operation %s while transitioning from %s to %s." |
|---|
| 158 | % (operation, old_state, new_state)) |
|---|
| 159 | new_owner = req.args.get('action_%s_operation_%s' % (action, operation), None) |
|---|
| 160 | review = PeerReviewModel(self.env, int(resource.id)) |
|---|
| 161 | if review: |
|---|
| 162 | review['owner'] = new_owner |
|---|
| 163 | review.save_changes(author=req.authname) |
|---|
| 164 | |
|---|
| 165 | # IWorkflowTransitionListener |
|---|
| 166 | |
|---|
| 167 | def object_transition(self, res_wf_state, resource, action, old_state, new_state): |
|---|
| 168 | if resource.realm == 'peerreviewer': |
|---|
| 169 | reviewer = PeerReviewerModel(self.env, resource.id) |
|---|
| 170 | reviewer['status'] = new_state |
|---|
| 171 | # This is for updating the PeerReviewer object. The info is used for displaying the review state to the |
|---|
| 172 | # review author. Note that this change is recorded in 'peerreviewer_change' by this. |
|---|
| 173 | # The change is also recorded in the generic change table 'resourceworkflow_change' because of the |
|---|
| 174 | # Resource object 'resource'. |
|---|
| 175 | reviewer.save_changes(author=res_wf_state.authname, |
|---|
| 176 | comment="State change to %s for %s from object_transition() of %s" |
|---|
| 177 | % (new_state, reviewer, resource)) |
|---|
| 178 | elif resource.realm == 'peerreview': |
|---|
| 179 | review = PeerReviewModel(self.env, resource.id) |
|---|
| 180 | review.change_status(new_state, res_wf_state.authname) |
|---|
| 181 | |
|---|
| 182 | # INavigationContributor |
|---|
| 183 | |
|---|
| 184 | def get_active_navigation_item(self, req): |
|---|
| 185 | return 'peerreviewmain' |
|---|
| 186 | |
|---|
| 187 | def get_navigation_items(self, req): |
|---|
| 188 | return [] |
|---|
| 189 | |
|---|
| 190 | # IRequestHandler methods |
|---|
| 191 | def match_request(self, req): |
|---|
| 192 | match = self.peerreview_view_re.match(req.path_info) |
|---|
| 193 | if match: |
|---|
| 194 | req.args['Review'] = match.group(1) |
|---|
| 195 | return True |
|---|
| 196 | # Deprecated legacy URL. |
|---|
| 197 | if req.path_info == '/peerReviewView': |
|---|
| 198 | self.env.log.info("Legacy URL 'peerReviewView' called from: %s", req.get_header('Referer')) |
|---|
| 199 | return True |
|---|
| 200 | |
|---|
| 201 | def process_request(self, req): |
|---|
| 202 | req.perm.require('CODE_REVIEW_VIEW') |
|---|
| 203 | |
|---|
| 204 | review_id = req.args.get('Review') |
|---|
| 205 | if review_id is None or not review_id.isdigit(): |
|---|
| 206 | raise TracError(u"Invalid review ID supplied - unable to load page.") |
|---|
| 207 | |
|---|
| 208 | if req.method == 'POST': |
|---|
| 209 | req.perm.require('CODE_REVIEW_DEV') |
|---|
| 210 | if req.args.get('resubmit'): |
|---|
| 211 | req.redirect(req.href.peerreviewnew(resubmit=review_id)) |
|---|
| 212 | elif req.args.get('followup'): |
|---|
| 213 | req.redirect(req.href.peerreviewnew(resubmit=review_id, followup=1)) |
|---|
| 214 | elif req.args.get('modify'): |
|---|
| 215 | req.redirect(req.href.peerreviewnew(resubmit=review_id, modify=1)) |
|---|
| 216 | |
|---|
| 217 | # Add display_rev() function to files so we can properly display the revision |
|---|
| 218 | # during template processing |
|---|
| 219 | rm = RepositoryManager(self.env) |
|---|
| 220 | files = self.get_files_for_review_id(req, review_id, True) |
|---|
| 221 | for file in files: |
|---|
| 222 | repos = rm.get_repository(file['repo']) |
|---|
| 223 | file['display_rev'] = repos.display_rev |
|---|
| 224 | |
|---|
| 225 | # If this is not a changeset review 'reponame' and 'changeset' are empty strings. |
|---|
| 226 | reponame, changeset = get_changeset_data(self.env, review_id) |
|---|
| 227 | repos = rm.get_repository(reponame) |
|---|
| 228 | short_rev = repos.display_rev(changeset) if repos else changeset |
|---|
| 229 | data = {'review_files': files, |
|---|
| 230 | 'users': get_users(self.env), |
|---|
| 231 | 'show_ticket': self.show_ticket, |
|---|
| 232 | 'cycle': itertools.cycle, |
|---|
| 233 | 'review': self.get_review_by_id(req, review_id), |
|---|
| 234 | 'reviewer': list(PeerReviewerModel.select_by_review_id(self.env, review_id)), |
|---|
| 235 | 'repo': reponame, |
|---|
| 236 | 'display_rev': repos.display_rev if repos else lambda x: x, |
|---|
| 237 | 'changeset': changeset, |
|---|
| 238 | 'changeset_html': get_changeset_html(self.env, req, short_rev, repos) |
|---|
| 239 | } |
|---|
| 240 | |
|---|
| 241 | # check to see if the user is a manager of this page or not |
|---|
| 242 | if 'CODE_REVIEW_MGR' in req.perm: |
|---|
| 243 | data['manager'] = True |
|---|
| 244 | |
|---|
| 245 | review = data['review'] |
|---|
| 246 | |
|---|
| 247 | # A finished reviews can't be changed anymore except by a manager |
|---|
| 248 | data['is_finished'] = review_is_finished(self.env.config, review) |
|---|
| 249 | # A user can't change his voting for a reviewed review |
|---|
| 250 | data['review_locked'] = review_is_locked(self.env.config, review, req.authname) |
|---|
| 251 | # Used to indicate that a review is done. 'review_locked' is not suitable because it is false for the |
|---|
| 252 | # author of a review even when the review is done. |
|---|
| 253 | data['review_done'] = review_is_locked(self.env.config, review) |
|---|
| 254 | data['finished_states_str'] = ', '.join(self.env.config.getlist("peerreview", "terminal_review_states")) |
|---|
| 255 | |
|---|
| 256 | # Add data for parent review if any |
|---|
| 257 | self.add_parent_data(req, review, data) |
|---|
| 258 | |
|---|
| 259 | self.add_ticket_data(req, data) # This updates dict 'data' |
|---|
| 260 | |
|---|
| 261 | # Actions for a reviewer. Each reviewer marks his progress on a review. The author |
|---|
| 262 | # can see this progress in the user list. The possible actions are defined in trac.ini |
|---|
| 263 | # as a workflow in [peerreviewer-resource_workflow] |
|---|
| 264 | wflow = self.prepare_workflow_for_reviewer(req, review, data) |
|---|
| 265 | if wflow: |
|---|
| 266 | data['reviewer_workflow'] = wflow |
|---|
| 267 | # Actions for the author of a review. The author may approve, disapprove or close a review. |
|---|
| 268 | # The possible actions are defined in trac.ini as a workflow in [peerreview-resource_workflow] |
|---|
| 269 | data['workflow'] = self.prepare_workflow_for_author(req, review, data) |
|---|
| 270 | |
|---|
| 271 | # For downloading in docx format |
|---|
| 272 | self.add_docx_export_link(req, review_id) |
|---|
| 273 | |
|---|
| 274 | add_stylesheet(req, 'common/css/code.css') |
|---|
| 275 | add_stylesheet(req, 'common/css/browser.css') |
|---|
| 276 | add_stylesheet(req, 'common/css/ticket.css') |
|---|
| 277 | add_stylesheet(req, 'hw/css/peerreview.css') |
|---|
| 278 | Chrome(self.env).add_jquery_ui(req) # For user icons |
|---|
| 279 | add_ctxt_nav_items(req) |
|---|
| 280 | if hasattr(Chrome, 'jenv'): |
|---|
| 281 | return 'peerreview_view_jinja.html', data |
|---|
| 282 | else: |
|---|
| 283 | add_script(req, 'common/js/folding.js') |
|---|
| 284 | return 'peerreview_view.html', data, None |
|---|
| 285 | |
|---|
| 286 | def add_parent_data(self, req, review, data): |
|---|
| 287 | """Add inforamtion about parent review to dict 'data'. Do nothing if not parent.""" |
|---|
| 288 | def get_parent_file(curfile, par_files): |
|---|
| 289 | fid = u"%s%s%s" % (curfile['path'], curfile['line_start'], curfile['line_end']) |
|---|
| 290 | for parfile in par_files: |
|---|
| 291 | tmp = u"%s%s%s" % (parfile['path'], parfile['line_start'], parfile['line_end']) |
|---|
| 292 | if tmp == fid: |
|---|
| 293 | return parfile |
|---|
| 294 | return None |
|---|
| 295 | |
|---|
| 296 | if review['parent_id'] != 0: |
|---|
| 297 | data['parent_review'] = self.get_review_by_id(req, review['parent_id']) |
|---|
| 298 | |
|---|
| 299 | # Add display_rev() function to files so we can properly display the revision |
|---|
| 300 | # during template processing |
|---|
| 301 | rm = RepositoryManager(self.env) |
|---|
| 302 | files = self.get_files_for_review_id(req, review['parent_id'], False) |
|---|
| 303 | for file in files: |
|---|
| 304 | repos = rm.get_repository(file['repo']) |
|---|
| 305 | file['display_rev'] = repos.display_rev |
|---|
| 306 | data['parent_files'] = files |
|---|
| 307 | |
|---|
| 308 | # Map files to parent files. Key is current file id, value is parent file object |
|---|
| 309 | file_map = {} |
|---|
| 310 | for rfile in data['review_files']: |
|---|
| 311 | file_map[rfile['file_id']] = get_parent_file(rfile, data['parent_files']) |
|---|
| 312 | data['file_map'] = file_map |
|---|
| 313 | |
|---|
| 314 | def prepare_workflow_for_reviewer(self, req, review, data): |
|---|
| 315 | """ |
|---|
| 316 | :param req: Request object |
|---|
| 317 | :param review: PeerReviewModel object representing the current review |
|---|
| 318 | :return: a workflow suitable for directly inserting into the page. or None if we are not a reviewer. |
|---|
| 319 | """ |
|---|
| 320 | # Actions for a reviewer. Each reviewer marks his progress on a review. The author |
|---|
| 321 | # can see this progress in the user list. The possible actions are defined in trac.ini |
|---|
| 322 | # as a workflow in [peerreviewer-resource_workflow] |
|---|
| 323 | realm = 'peerreviewer' |
|---|
| 324 | for reviewer in data['reviewer']: |
|---|
| 325 | if reviewer['reviewer'] == req.authname: |
|---|
| 326 | # Todo: 'canivote' should be obsolete by now |
|---|
| 327 | if not data['is_finished'] and not data['review_done']: # even author isn't allowed to change |
|---|
| 328 | data['canivote'] = True |
|---|
| 329 | res = Resource(realm, str(reviewer['reviewer_id'])) # id must be a string |
|---|
| 330 | wf_data = {'redirect': req.href.peerreviewview(review['review_id']), |
|---|
| 331 | 'legend': _("Set review progress"), |
|---|
| 332 | 'help': _("Setting the current state helps the review author to track progress.")} |
|---|
| 333 | return ResourceWorkflowSystem(self.env).get_workflow_markup(req, self.workflow_base_href, |
|---|
| 334 | realm, res, wf_data) |
|---|
| 335 | return |
|---|
| 336 | |
|---|
| 337 | def prepare_workflow_for_author(self, req, review, data): |
|---|
| 338 | """ |
|---|
| 339 | :param req: Request object |
|---|
| 340 | :param review: PeerReviewModel object representing the current review |
|---|
| 341 | :return: a workflow suitable for directly inserting into the page. |
|---|
| 342 | """ |
|---|
| 343 | # Actions for the author of a review. The author may approve, disapprove or close a review. |
|---|
| 344 | # The possible actions are defined in trac.ini as a workflow in [peerreview-resource_workflow] |
|---|
| 345 | realm = 'peerreview' |
|---|
| 346 | res = Resource(realm, str(review['review_id'])) # Must be a string (?) |
|---|
| 347 | |
|---|
| 348 | wf_data = {'redirect': req.href.peerreviewview(review['review_id']), |
|---|
| 349 | 'legend': _("Set state of review (author or manager only)"), |
|---|
| 350 | 'help': _("Closing a review means it is marked as obsolete and <strong>all associated data will be unavailable</strong>.")} |
|---|
| 351 | return ResourceWorkflowSystem(self.env).get_workflow_markup(req, self.workflow_base_href, realm, res, wf_data) |
|---|
| 352 | |
|---|
| 353 | def add_docx_export_link(self, req, review_id): |
|---|
| 354 | """Ad a download link for docx format if conversion is available. |
|---|
| 355 | |
|---|
| 356 | Note that python-docx must be installed for the link to show up. |
|---|
| 357 | """ |
|---|
| 358 | conversions = Mimeview(self.env).get_supported_conversions('text/x-trac-peerreview') |
|---|
| 359 | for key, name, ext, mime_in, mime_out, q, c in conversions: |
|---|
| 360 | conversion_href = req.href("peerreview", format=key, reviewid=review_id) |
|---|
| 361 | add_link(req, 'alternate', conversion_href, name, mime_out) |
|---|
| 362 | |
|---|
| 363 | |
|---|
| 364 | def get_files_for_review_id(self, req, review_id, comments=False): |
|---|
| 365 | """Get all file objects belonging to the given review id. Provide the number of comments if asked for. |
|---|
| 366 | |
|---|
| 367 | :param review_id: id of review as an int |
|---|
| 368 | :param req: Request object |
|---|
| 369 | :param comments: if True add information about comments as attributes to the file objects |
|---|
| 370 | :return: list of ReviewFileModels |
|---|
| 371 | """ |
|---|
| 372 | return get_files_for_review_id(self.env, req, review_id, comments) |
|---|
| 373 | |
|---|
| 374 | rev_files = list(ReviewFileModel.select_by_review(self.env, review_id)) |
|---|
| 375 | if comments: |
|---|
| 376 | for file_ in rev_files: |
|---|
| 377 | file_.num_comments = len(list(ReviewCommentModel.select_by_file_id(self.env, file_['file_id']))) |
|---|
| 378 | my_comment_data = ReviewDataModel.comments_for_file_and_owner(self.env, file_['file_id'], req.authname) |
|---|
| 379 | file_.num_notread = file_.num_comments - len([c_id for c_id, t, dat in my_comment_data if t == 'read']) |
|---|
| 380 | return rev_files |
|---|
| 381 | |
|---|
| 382 | def get_review_by_id(self, req, review_id): |
|---|
| 383 | """Get a PeerReviewModel for the given review id and prepare some additional data used by the template""" |
|---|
| 384 | review = PeerReviewModel(self.env, review_id) |
|---|
| 385 | review.html_notes = format_to_html(self.env, web_context(req), review['notes']) |
|---|
| 386 | review.date = user_time(req, format_date, to_datetime(review['created'])) |
|---|
| 387 | if review['closed']: |
|---|
| 388 | review.finish_date = user_time(req, format_date, to_datetime(review['closed'])) |
|---|
| 389 | else: |
|---|
| 390 | review.finish_date = '' |
|---|
| 391 | return review |
|---|
| 392 | |
|---|
| 393 | desc = u""" |
|---|
| 394 | Review [review:${review_id} ${review_name}]. |
|---|
| 395 | === Review |
|---|
| 396 | |
|---|
| 397 | ||= Name =|| ${review_name} || |
|---|
| 398 | ||= ID =|| [review:${review_id}] || |
|---|
| 399 | |
|---|
| 400 | === Review Notes |
|---|
| 401 | |
|---|
| 402 | ${review_notes} |
|---|
| 403 | |
|---|
| 404 | === Files |
|---|
| 405 | ||= File name =||= Comments =|| |
|---|
| 406 | ${review_files} |
|---|
| 407 | """ |
|---|
| 408 | |
|---|
| 409 | def add_ticket_data(self, req, data): |
|---|
| 410 | """Create the ticket description for tickets created from the review page""" |
|---|
| 411 | review = data['review'] |
|---|
| 412 | |
|---|
| 413 | wikipage = WikiPage(self.env, 'CodeReview/TicketTemplate') |
|---|
| 414 | if wikipage.exists: |
|---|
| 415 | tmpl = Template(wikipage.text) |
|---|
| 416 | else: |
|---|
| 417 | tmpl = Template(self.desc) |
|---|
| 418 | |
|---|
| 419 | try: |
|---|
| 420 | file_table = '' |
|---|
| 421 | for f in data['review_files']: |
|---|
| 422 | file_table += u"||[rfile:%s %s]|| %s ||%s" % \ |
|---|
| 423 | (f['file_id'], f['path'], f.num_comments, CRLF) |
|---|
| 424 | except KeyError: |
|---|
| 425 | pass |
|---|
| 426 | |
|---|
| 427 | txt = tmpl.substitute(review_name=review['name'], review_id=review['review_id'], |
|---|
| 428 | review_notes=review['notes'] if review['notes'] else '', |
|---|
| 429 | review_files=file_table) |
|---|
| 430 | |
|---|
| 431 | data['ticket_desc_wiki'] = self.create_preview(req, txt) |
|---|
| 432 | data['ticket_desc'] = txt |
|---|
| 433 | data['ticket_summary'] = u'Review "%s"' % review['name'] |
|---|
| 434 | |
|---|
| 435 | def create_preview(self, req, text): |
|---|
| 436 | resource = Resource('peerreview') |
|---|
| 437 | context = web_context(req, resource) |
|---|
| 438 | return format_to_html(self.env, context, text) |
|---|