1<?php 2// reviewtable.php -- HotCRP helper class for table of all reviews 3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE. 4 5function _review_table_actas($rr) { 6 global $Me; 7 if (!get($rr, "contactId") || $rr->contactId == $Me->contactId) 8 return ""; 9 return ' <a href="' . selfHref(array("actas" => $rr->email)) . '">' 10 . Ht::img("viewas.png", "[Act as]", array("title" => "Act as " . Text::name_text($rr))) 11 . "</a>"; 12} 13 14function _retract_review_request_form(PaperInfo $prow, ReviewInfo $rr) { 15 return '<small>' 16 . Ht::form(hoturl_post("assign", "p=$prow->paperId")) 17 . '<div class="inline">' 18 . Ht::hidden("retract", $rr->email) 19 . Ht::submit("Retract review", array("title" => "Retract this review request", "style" => "font-size:smaller")) 20 . '</div></form></small>'; 21} 22 23// reviewer information 24function reviewTable(PaperInfo $prow, $rrows, $crows, $rrow, $mode, $proposals = null) { 25 global $Me; 26 $conf = $prow->conf; 27 $subrev = array(); 28 $nonsubrev = array(); 29 $foundRrow = $foundMyReview = $notShown = 0; 30 $cflttype = $Me->view_conflict_type($prow); 31 $allow_admin = $Me->allow_administer($prow); 32 $admin = $Me->can_administer($prow); 33 $hideUnviewable = ($cflttype > 0 && !$admin) 34 || (!$Me->act_pc($prow) && !$conf->setting("extrev_view")); 35 $show_colors = $Me->can_view_reviewer_tags($prow); 36 $show_ratings = $Me->can_view_review_ratings($prow); 37 $tagger = $show_colors ? new Tagger($Me) : null; 38 $xsep = ' <span class="barsep">·</span> '; 39 $want_scores = $mode !== "assign" && $mode !== "edit" && $mode !== "re"; 40 $want_requested_by = false; 41 $want_retract = false; 42 $score_header = array_map(function ($x) { return ""; }, $conf->review_form()->forder); 43 44 // actual rows 45 foreach ($rrows as $rr) { 46 $highlight = ($rrow && $rr->reviewId == $rrow->reviewId); 47 $foundRrow += $highlight; 48 $want_my_scores = $want_scores; 49 if ($Me->is_owned_review($rr) && $mode === "re") { 50 $want_my_scores = true; 51 $foundMyReview++; 52 } 53 $canView = $Me->can_view_review($prow, $rr); 54 55 // skip unsubmitted reviews 56 if (!$canView && $hideUnviewable) { 57 if ($rr->reviewNeedsSubmit == 1 && $rr->reviewModified) 58 $notShown++; 59 continue; 60 } 61 62 $t = ""; 63 $tclass = ($rrow && $highlight ? "reviewers-highlight" : ""); 64 65 // review ID 66 $id = "Review"; 67 if ($rr->reviewOrdinal) 68 $id .= " #" . $prow->paperId . unparseReviewOrdinal($rr->reviewOrdinal); 69 else if ($rr->reviewSubmitted) 70 /* OK */; 71 else if ($rr->reviewType == REVIEW_SECONDARY && $rr->reviewNeedsSubmit <= 0) 72 $id .= " (delegated)"; 73 else if ($rr->reviewModified > 1 && $rr->timeApprovalRequested > 0) 74 $id .= " (awaiting approval)"; 75 else if ($rr->reviewModified > 1) 76 $id .= " (in progress)"; 77 else if ($rr->reviewModified > 0) 78 $id .= " (accepted)"; 79 else 80 $id .= " (not started)"; 81 $rlink = unparseReviewOrdinal($rr); 82 $t .= '<td class="rl nw">'; 83 if ($rrow && $rrow->reviewId == $rr->reviewId) { 84 if ($Me->contactId == $rr->contactId && !$rr->reviewSubmitted) 85 $id = "Your $id"; 86 $t .= '<a href="' . hoturl("review", "p=$prow->paperId&r=$rlink") . '" class="q"><b>' . $id . '</b></a>'; 87 } else if (!$canView 88 || ($rr->reviewModified <= 1 && !$Me->can_review($prow, $rr))) { 89 $t .= $id; 90 } else if ($rrow 91 || $rr->reviewModified <= 1 92 || (($mode === "re" || $mode === "assign") 93 && $Me->can_review($prow, $rr))) { 94 $t .= '<a href="' . hoturl("review", "p=$prow->paperId&r=$rlink") . '">' . $id . '</a>'; 95 } else if (Navigation::page() !== "paper") { 96 $t .= '<a href="' . hoturl("paper", "p=$prow->paperId#r$rlink") . '">' . $id . '</a>'; 97 } else { 98 $t .= '<a href="#r' . $rlink . '">' . $id . '</a>'; 99 if ($show_ratings 100 && $Me->can_view_review_ratings($prow, $rr) 101 && ($ratings = $rr->ratings())) { 102 $all = 0; 103 foreach ($ratings as $r) 104 $all |= $r; 105 if ($all & 126) 106 $t .= " ⚑"; 107 else if ($all & 1) 108 $t .= " ⚐"; 109 } 110 } 111 $t .= '</td>'; 112 113 // primary/secondary glyph 114 if ($cflttype > 0 && !$admin) 115 $rtype = ""; 116 else if ($rr->reviewType > 0) { 117 $rtype = review_type_icon($rr->reviewType, $rr->reviewNeedsSubmit != 0); 118 if ($rr->reviewRound > 0 && $Me->can_view_review_round($prow, $rr)) 119 $rtype .= ' <span class="revround" title="Review round">' 120 . htmlspecialchars($conf->round_name($rr->reviewRound)) 121 . "</span>"; 122 } else 123 $rtype = ""; 124 125 // reviewer identity 126 $showtoken = $rr->reviewToken && $Me->can_review($prow, $rr); 127 if (!$Me->can_view_review_identity($prow, $rr)) { 128 $t .= ($rtype ? '<td class="rl">' . $rtype . '</td>' : '<td></td>'); 129 } else { 130 if (!$showtoken || !Contact::is_anonymous_email($rr->email)) 131 $n = $Me->name_html_for($rr); 132 else 133 $n = "[Token " . encode_token((int) $rr->reviewToken) . "]"; 134 if ($allow_admin) 135 $n .= _review_table_actas($rr); 136 $t .= '<td class="rl"><span class="taghl">' . $n . '</span>' 137 . ($rtype ? " $rtype" : "") . "</td>"; 138 if ($show_colors 139 && ($p = $conf->pc_member_by_id($rr->contactId)) 140 && ($color = $p->viewable_color_classes($Me))) 141 $tclass .= ($tclass ? " " : "") . $color; 142 } 143 144 // requester 145 if ($mode === "assign") { 146 if ($rr->reviewType < REVIEW_SECONDARY 147 && !$showtoken 148 && $rr->requestedBy 149 && $rr->requestedBy != $rr->contactId 150 && $Me->can_view_review_requester($prow, $rr)) { 151 $t .= '<td class="rl" style="font-size:smaller">'; 152 if ($rr->requestedBy == $Me->contactId) 153 $t .= "you"; 154 else 155 $t .= $Me->reviewer_html_for($rr->requestedBy); 156 $t .= '</td>'; 157 $want_requested_by = true; 158 159 if ($rr->reviewModified <= 0 160 && ($rr->requestedBy == $Me->contactId || $admin)) 161 $t .= '<td class="rl">' . _retract_review_request_form($prow, $rr) . '</td>'; 162 } else 163 $t .= '<td></td>'; 164 } 165 166 // scores 167 $scores = array(); 168 if ($want_my_scores && $canView) { 169 $view_score = $Me->view_score_bound($prow, $rr); 170 foreach ($conf->review_form()->forder as $fid => $f) 171 if ($f->has_options && $f->view_score > $view_score 172 && (!$f->round_mask || $f->is_round_visible($rr)) 173 && isset($rr->$fid) && $rr->$fid) { 174 if ($score_header[$fid] === "") 175 $score_header[$fid] = '<th class="revscore">' . $f->web_abbreviation() . "</th>"; 176 $scores[$fid] = '<td class="revscore need-tooltip" data-rf="' . $f->uid() . '" data-tooltip-info="rf-score">' 177 . $f->unparse_value($rr->$fid, ReviewField::VALUE_SC) 178 . '</td>'; 179 } 180 } 181 182 // affix 183 if (!$rr->reviewSubmitted) 184 $nonsubrev[] = array($tclass, $t, $scores); 185 else 186 $subrev[] = array($tclass, $t, $scores); 187 } 188 189 // proposed review rows 190 if ($proposals) 191 foreach ($proposals as $rr) { 192 $t = ""; 193 194 // review ID 195 $t = '<td class="rl">Proposed review</td>'; 196 197 // reviewer identity 198 $t .= '<td class="rl">' . Text::user_html($rr); 199 if ($allow_admin) 200 $t .= _review_table_actas($rr); 201 $t .= "</td>"; 202 203 // requester 204 if ($cflttype <= 0 || $admin) { 205 $t .= '<td class="rl" style="font-size:smaller">'; 206 if ($rr->requestedBy) { 207 if ($rr->requestedBy == $Me->contactId) 208 $t .= "you"; 209 else 210 $t .= $Me->reviewer_html_for($rr->requestedBy); 211 } 212 $t .= '</td>'; 213 $want_requested_by = true; 214 } 215 216 $t .= '<td class="rl">'; 217 if ($admin) { 218 $t .= '<small>' 219 . Ht::form(hoturl_post("assign", "p=$prow->paperId")) 220 . Ht::hidden("firstName", $rr->firstName) 221 . Ht::hidden("lastName", $rr->lastName) 222 . Ht::hidden("email", $rr->email) 223 . Ht::hidden("affiliation", $rr->affiliation) 224 . Ht::hidden("reason", $rr->reason); 225 if ($rr->reviewRound !== null) { 226 if ($rr->reviewRound == 0) 227 $rname = "unnamed"; 228 else 229 $rname = $conf->round_name($rr->reviewRound); 230 if ($rname) 231 $t .= Ht::hidden("round", $rname); 232 } 233 $apptext = "Approve review"; 234 if (Ht::control_class("need-override-requestreview-" . $rr->email)) { 235 $t .= Ht::hidden("override", 1); 236 $apptext = "Override conflict and approve review"; 237 } 238 $t .= Ht::submit("approvereview", $apptext, array("style" => "font-size:smaller")) 239 . ' ' 240 . Ht::submit("denyreview", "Deny request", array("style" => "font-size:smaller")) 241 . '</form>'; 242 } else if ($Me->contactId && $rr->requestedBy === $Me->contactId) 243 $t .= _retract_review_request_form($prow, $rr); 244 $t .= '</td>'; 245 246 // affix 247 $nonsubrev[] = array("", $t); 248 } 249 250 // unfinished review notification 251 $notetxt = ""; 252 if ($cflttype >= CONFLICT_AUTHOR && !$admin && $notShown 253 && $Me->can_view_review($prow, null)) { 254 if ($notShown == 1) 255 $t = "1 review remains outstanding."; 256 else 257 $t = "$notShown reviews remain outstanding."; 258 $t .= '<br /><span class="hint">You will be emailed if new reviews are submitted or existing reviews are changed.</span>'; 259 $notetxt = '<div class="revnotes">' . $t . "</div>"; 260 } 261 262 // completion 263 if (count($nonsubrev) + count($subrev)) { 264 if ($want_requested_by) 265 array_unshift($score_header, '<th class="rl">Requester</th>'); 266 $score_header_text = join("", $score_header); 267 $t = "<div class=\"reviewersdiv\"><table class=\"reviewers"; 268 if ($score_header_text) 269 $t .= " has-scores"; 270 $t .= "\">\n"; 271 $nscores = 0; 272 if ($score_header_text) { 273 foreach ($score_header as $x) 274 $nscores += $x !== "" ? 1 : 0; 275 $t .= '<tr><td colspan="2"></td>'; 276 if ($mode === "assign" && !$want_requested_by) 277 $t .= '<td></td>'; 278 $t .= $score_header_text . "</tr>\n"; 279 } 280 foreach (array_merge($subrev, $nonsubrev) as $r) { 281 $t .= '<tr class="rl' . ($r[0] ? " $r[0]" : "") . '">' . $r[1]; 282 if (get($r, 2)) { 283 foreach ($score_header as $fid => $header_needed) 284 if ($header_needed !== "") { 285 $x = get($r[2], $fid); 286 $t .= $x ? : "<td class=\"revscore rs_$fid\"></td>"; 287 } 288 } else if ($nscores > 0) 289 $t .= '<td colspan="' . $nscores . '"></td>'; 290 $t .= "</tr>\n"; 291 } 292 return $t . "</table></div>\n" . $notetxt; 293 } else 294 return $notetxt; 295} 296 297 298// links below review table 299function reviewLinks(PaperInfo $prow, $rrows, $crows, $rrow, $mode, &$allreviewslink) { 300 global $Me; 301 $conf = $prow->conf; 302 $cflttype = $Me->view_conflict_type($prow); 303 $allow_admin = $Me->allow_administer($prow); 304 $any_comments = false; 305 $admin = $Me->can_administer($prow); 306 $xsep = ' <span class="barsep">·</span> '; 307 308 $nvisible = 0; 309 $myrr = null; 310 if ($rrows) 311 foreach ($rrows as $rr) { 312 if ($Me->can_view_review($prow, $rr)) 313 $nvisible++; 314 if ($rr->contactId == $Me->contactId 315 || (!$myrr && $Me->is_my_review($rr))) 316 $myrr = $rr; 317 } 318 319 // comments 320 $pret = ""; 321 if ($crows && !empty($crows) && !$rrow && $mode !== "edit") { 322 $tagger = new Tagger($Me); 323 $viewable_crows = array_filter($crows, function ($cr) use ($Me) { return $Me->can_view_comment($cr->prow, $cr); }); 324 $cxs = CommentInfo::group_by_identity($viewable_crows, $Me, true); 325 if (!empty($cxs)) { 326 $count = array_reduce($cxs, function ($n, $cx) { return $n + $cx[1]; }, 0); 327 $cnames = array_map(function ($cx) use ($Me, $conf) { 328 $cid = CommentInfo::unparse_html_id($cx[0], $conf); 329 $tclass = "cmtlink"; 330 if (($tags = $cx[0]->viewable_tags($Me)) 331 && ($color = $cx[0]->conf->tags()->color_classes($tags))) 332 $tclass .= " $color taghh"; 333 return "<span class=\"nb\"><a class=\"{$tclass}\" href=\"#{$cid}\">" 334 . $cx[0]->unparse_user_html($Me, null) 335 . "</a>" 336 . ($cx[1] > 1 ? " ({$cx[1]})" : "") 337 . $cx[2] . "</span>"; 338 }, $cxs); 339 $first_cid = CommentInfo::unparse_html_id($cxs[0][0], $conf); 340 $pret = '<div class="revnotes"><a href="#' . $first_cid . '"><strong>' 341 . plural($count, "Comment") . '</strong></a>: ' 342 . join(" ", $cnames) . '</div>'; 343 $any_comments = true; 344 } 345 } 346 347 $t = []; 348 $dlimgjs = ["class" => "dlimg", "width" => 24, "height" => 24]; 349 350 // see all reviews 351 $allreviewslink = false; 352 if (($nvisible > 1 || ($nvisible > 0 && !$myrr)) 353 && ($mode !== "p" || $rrow)) { 354 $allreviewslink = true; 355 $t[] = '<a href="' . hoturl("paper", "p=$prow->paperId") . '" class="xx revlink">' 356 . Ht::img("view48.png", "[All reviews]", $dlimgjs) . " <u>All reviews</u></a>"; 357 } 358 359 // edit paper 360 if ($mode !== "edit" 361 && $prow->has_author($Me) 362 && !$Me->can_administer($prow)) { 363 $t[] = '<a href="' . hoturl("paper", "p=$prow->paperId&m=edit") . '" class="xx revlink">' 364 . Ht::img("edit48.png", "[Edit]", $dlimgjs) . " <u><strong>Edit submission</strong></u></a>"; 365 } 366 367 // edit review 368 if ($mode === "re" || ($mode === "assign" && $t !== "") || !$prow) 369 /* no link */; 370 else if ($myrr && $rrow != $myrr) { 371 $myrlink = unparseReviewOrdinal($myrr); 372 $a = '<a href="' . hoturl("review", "p=$prow->paperId&r=$myrlink") . '" class="xx revlink">'; 373 if ($Me->can_review($prow, $myrr)) 374 $x = $a . Ht::img("review48.png", "[Edit review]", $dlimgjs) . " <u><b>Edit your review</b></u></a>"; 375 else 376 $x = $a . Ht::img("review48.png", "[Your review]", $dlimgjs) . " <u><b>Your review</b></u></a>"; 377 $t[] = $x; 378 } else if (!$myrr && !$rrow && $Me->can_review($prow, null)) { 379 $t[] = '<a href="' . hoturl("review", "p=$prow->paperId&m=re") . '" class="xx revlink">' 380 . Ht::img("review48.png", "[Write review]", $dlimgjs) . " <u><b>Write review</b></u></a>"; 381 } 382 383 // review assignments 384 if ($mode !== "assign" && $mode !== "edit" 385 && $Me->can_request_review($prow, true)) { 386 $t[] = '<a href="' . hoturl("assign", "p=$prow->paperId") . '" class="xx revlink">' 387 . Ht::img("assign48.png", "[Assign]", $dlimgjs) . " <u>" . ($admin ? "Assign reviews" : "External reviews") . "</u></a>"; 388 } 389 390 // new comment 391 $nocmt = preg_match('/\A(?:assign|contact|edit|re)\z/', $mode); 392 if (!$allreviewslink && !$nocmt && $Me->can_comment($prow, null)) { 393 $t[] = '<a class="ui js-edit-comment xx revlink" href="#cnew">' 394 . Ht::img("comment48.png", "[Add comment]", $dlimgjs) . " <u>Add comment</u></a>"; 395 $any_comments = true; 396 } 397 398 // new response 399 if (!$nocmt 400 && ($prow->has_author($Me) || $allow_admin) 401 && $conf->any_response_open) { 402 foreach ($conf->resp_rounds() as $rrd) { 403 $cr = null; 404 foreach ($crows ? : [] as $crow) 405 if (($crow->commentType & COMMENTTYPE_RESPONSE) 406 && $crow->commentRound == $rrd->number) 407 $cr = $crow; 408 $cr = $cr ? : CommentInfo::make_response_template($rrd->number, $prow); 409 if ($Me->can_respond($prow, $cr)) { 410 $cid = $conf->resp_round_text($rrd->number) . "response"; 411 $what = "Add"; 412 if ($cr->commentId) 413 $what = $cr->commentType & COMMENTTYPE_DRAFT ? "Edit draft" : "Edit"; 414 $t[] = '<a class="ui js-edit-comment xx revlink" href="#' . $cid . '">' 415 . Ht::img("comment48.png", "[$what response]", $dlimgjs) . " " 416 . ($cflttype >= CONFLICT_AUTHOR ? '<u style="font-weight:bold">' : '<u>') 417 . $what . ($rrd->name == "1" ? "" : " $rrd->name") . ' response</u></a>'; 418 $any_comments = true; 419 } 420 } 421 } 422 423 // override conflict 424 if ($allow_admin && !$admin) { 425 $t[] = '<span class="revlink"><a href="' . selfHref(array("forceShow" => 1)) . '" class="xx">' 426 . Ht::img("override24.png", "[Override]", "dlimg") . " <u>Override conflict</u></a> to show reviewers and allow editing</span>"; 427 } else if ($Me->privChair && !$allow_admin) { 428 $x = '<span class="revlink">You can’t override your conflict because this submission has an administrator.</span>'; 429 } 430 431 if ($any_comments) 432 CommentInfo::echo_script($prow); 433 434 $t = empty($t) ? "" : '<p class="sd">' . join("", $t) . '</p>'; 435 if ($prow->has_author($Me)) 436 $t = '<p class="sd">' . $conf->_('You are an <span class="author">author</span> of this submission.') . '</p>' . $t; 437 else if ($prow->has_conflict($Me)) 438 $t = '<p class="sd">' . $conf->_('You have a <span class="conflict">conflict</span> with this submission.') . '</p>' . $t; 439 return $pret . $t; 440} 441