1<?php 2// assign.php -- HotCRP per-paper assignment/conflict management page 3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE. 4 5require_once("src/initweb.php"); 6require_once("src/papertable.php"); 7require_once("src/reviewtable.php"); 8if (!$Me->email) 9 $Me->escape(); 10$Me->add_overrides(Contact::OVERRIDE_CONFLICT); 11// ensure site contact exists before locking tables 12$Conf->site_contact(); 13 14// header 15function confHeader() { 16 global $paperTable, $Qreq; 17 PaperTable::do_header($paperTable, "assign", "assign", $Qreq); 18} 19 20function errorMsgExit($msg) { 21 confHeader(); 22 $msg && Conf::msg_error($msg); 23 Conf::$g->footer(); 24 exit; 25} 26 27 28// grab paper row 29function loadRows() { 30 global $prow, $Conf, $Me, $Qreq; 31 $Conf->paper = $prow = PaperTable::paperRow($Qreq, $whyNot); 32 if (!$prow) 33 errorMsgExit(whyNotText($whyNot + ["listViewable" => true])); 34 if (($whyNot = $Me->perm_request_review($prow, false))) { 35 $wnt = whyNotText($whyNot); 36 error_go(hoturl("paper", ["p" => $prow->paperId]), $wnt); 37 } 38} 39 40 41loadRows(); 42 43 44 45// retract review request 46function retractRequest($email, $prow, $confirm = true) { 47 global $Conf, $Me; 48 49 $Conf->qe("lock tables PaperReview write, ReviewRequest write, ContactInfo read, PaperConflict read"); 50 $email = trim($email); 51 // NB caller unlocks tables 52 53 // check for outstanding review 54 $contact_fields = "firstName, lastName, ContactInfo.email, password, roles, preferredEmail, disabled"; 55 $result = $Conf->qe("select reviewId, reviewType, reviewModified, reviewSubmitted, reviewToken, requestedBy, $contact_fields 56 from ContactInfo 57 join PaperReview on (PaperReview.paperId=$prow->paperId and PaperReview.contactId=ContactInfo.contactId) 58 where ContactInfo.email=?", $email); 59 $row = edb_orow($result); 60 61 // check for outstanding review request 62 $result2 = Dbl::qe("select * from ReviewRequest where paperId=? and email=?", $prow->paperId, $email); 63 $row2 = edb_orow($result2); 64 65 // act 66 if (!$row && !$row2) 67 return Conf::msg_error("No such review request."); 68 if ($row && $row->reviewModified > 0) 69 return Conf::msg_error("You can’t retract that review request since the reviewer has already started their review."); 70 if (!$Me->allow_administer($prow) 71 && (($row && $row->requestedBy && $Me->contactId != $row->requestedBy) 72 || ($row2 && $row2->requestedBy && $Me->contactId != $row2->requestedBy))) 73 return Conf::msg_error("You can’t retract that review request since you didn’t make the request in the first place."); 74 75 // at this point, success; remove the review request 76 if ($row) { 77 $Conf->qe("delete from PaperReview where paperId=? and reviewId=?", $prow->paperId, $row->reviewId); 78 $Me->update_review_delegation($prow->paperId, $row->requestedBy, -1); 79 } 80 if ($row2) 81 $Conf->qe("delete from ReviewRequest where paperId=? and email=?", $prow->paperId, $email); 82 83 if (get($row, "reviewToken", 0) != 0) 84 $Conf->settings["rev_tokens"] = -1; 85 // send confirmation email, if the review site is open 86 if ($Conf->time_review_open() && $row) { 87 $Reviewer = new Contact($row); 88 HotCRPMailer::send_to($Reviewer, "@retractrequest", $prow, 89 array("requester_contact" => $Me, 90 "cc" => Text::user_email_to($Me))); 91 } 92 93 // confirmation message 94 if ($confirm) 95 $Conf->confirmMsg("Retracted request that " . Text::user_html($row ? $row : $row2) . " review paper #$prow->paperId."); 96} 97 98if (isset($Qreq->retract) && $Qreq->post_ok()) { 99 retractRequest($Qreq->retract, $prow); 100 $Conf->qe("unlock tables"); 101 if ($Conf->setting("rev_tokens") === -1) 102 $Conf->update_rev_tokens_setting(0); 103 SelfHref::redirect($Qreq); 104 loadRows(); 105} 106 107 108// change PC assignments 109function pcAssignments($qreq) { 110 global $Conf, $Me, $prow; 111 112 $reviewer = $qreq->reviewer; 113 if (($rname = $Conf->sanitize_round_name($qreq->rev_round)) === "") 114 $rname = "unnamed"; 115 $round = CsvGenerator::quote(":" . (string) $rname); 116 117 $t = ["paper,action,email,round\n"]; 118 foreach ($Conf->pc_members() as $cid => $p) { 119 if ($reviewer 120 && strcasecmp($p->email, $reviewer) != 0 121 && (string) $p->contactId !== $reviewer) 122 continue; 123 124 if (isset($qreq["assrev{$prow->paperId}u{$cid}"])) 125 $revtype = $qreq["assrev{$prow->paperId}u{$cid}"]; 126 else if (isset($qreq["pcs{$cid}"])) 127 $revtype = $qreq["pcs{$cid}"]; 128 else 129 continue; 130 $revtype = cvtint($revtype, null); 131 if ($revtype === null) 132 continue; 133 134 $myround = $round; 135 if (isset($qreq["rev_round{$prow->paperId}u{$cid}"])) { 136 $x = $Conf->sanitize_round_name($qreq["rev_round{$prow->paperId}u{$cid}"]); 137 if ($x !== false) 138 $myround = $x === "" ? "unnamed" : CsvGenerator::quote($x); 139 } 140 141 $user = CsvGenerator::quote($p->email); 142 if ($revtype >= 0) 143 $t[] = "{$prow->paperId},clearconflict,$user\n"; 144 if ($revtype <= 0) 145 $t[] = "{$prow->paperId},clearreview,$user\n"; 146 if ($revtype == REVIEW_META) 147 $t[] = "{$prow->paperId},metareview,$user,$myround\n"; 148 else if ($revtype == REVIEW_PRIMARY) 149 $t[] = "{$prow->paperId},primary,$user,$myround\n"; 150 else if ($revtype == REVIEW_SECONDARY) 151 $t[] = "{$prow->paperId},secondary,$user,$myround\n"; 152 else if ($revtype == REVIEW_PC || $revtype == REVIEW_EXTERNAL) 153 $t[] = "{$prow->paperId},pcreview,$user,$myround\n"; 154 else if ($revtype < 0) 155 $t[] = "{$prow->paperId},conflict,$user\n"; 156 } 157 158 $aset = new AssignmentSet($Me, true); 159 $aset->enable_papers($prow); 160 $aset->parse(join("", $t)); 161 if ($aset->execute()) { 162 if ($qreq->ajax) 163 json_exit(["ok" => true]); 164 else { 165 $Conf->confirmMsg("Assignments saved." . $aset->errors_div_html()); 166 SelfHref::redirect($qreq); 167 // NB normally SelfHref::redirect() does not return 168 loadRows(); 169 } 170 } else { 171 if ($qreq->ajax) 172 json_exit(["ok" => false, "error" => join("<br />", $aset->errors_html())]); 173 else 174 $Conf->errorMsg(join("<br />", $aset->errors_html())); 175 } 176} 177 178if (isset($Qreq->update) && $Me->allow_administer($prow) && $Qreq->post_ok()) 179 pcAssignments($Qreq); 180else if (isset($Qreq->update) && $Qreq->ajax) 181 json_exit(["ok" => false, "error" => "Only administrators can assign papers."]); 182 183 184// add review requests 185if ((isset($Qreq->requestreview) || isset($Qreq->approvereview)) 186 && $Qreq->post_ok()) { 187 $result = RequestReview_API::requestreview($Me, $Qreq, $prow); 188 $result = JsonResult::make($result); 189 if ($result->content["ok"]) { 190 if ($result->content["action"] === "token") 191 $Conf->confirmMsg("Created a new anonymous review. The review token is " . $result->content["review_token"] . "."); 192 else if ($result->content["action"] === "propose") 193 $Conf->warnMsg($result->content["response"]); 194 else 195 $Conf->confirmMsg($result->content["response"]); 196 unset($Qreq->email, $Qreq->firstName, $Qreq->lastName, $Qreq->affiliation, $Qreq->round, $Qreq->reason, $Qreq->override); 197 SelfHref::redirect($Qreq); 198 } else { 199 $error = $result->content["error"]; 200 if (isset($result->content["errf"]) && isset($result->content["errf"]["override"])) 201 $error .= "<div>To request a review anyway, submit again with the “Override” checkbox checked.</div>"; 202 Conf::msg_error($error); 203 if (isset($result->content["errf"])) { 204 foreach ($result->content["errf"] as $f => $x) 205 Ht::error_at($f); 206 } 207 Ht::error_at("need-override-requestreview-" . $Qreq->email); 208 loadRows(); 209 } 210} 211 212 213// deny review request 214if ((isset($Qreq->deny) || isset($Qreq->denyreview)) 215 && $Me->allow_administer($prow) 216 && $Qreq->post_ok() 217 && ($email = trim($Qreq->email))) { 218 $Conf->qe("lock tables ReviewRequest write, ContactInfo read, PaperConflict read, PaperReview read, PaperReviewRefused write"); 219 // Need to be careful and not expose inappropriate information: 220 // this email comes from the chair, who can see all, but goes to a PC 221 // member, who can see less. 222 $result = $Conf->qe("select requestedBy from ReviewRequest where paperId=$prow->paperId and email=?", $email); 223 if (($row = edb_row($result))) { 224 $Requester = $Conf->user_by_id($row[0]); 225 $Conf->qe("delete from ReviewRequest where paperId=$prow->paperId and email=?", $email); 226 if (($reqId = $Conf->user_id_by_email($email)) > 0) 227 $Conf->qe("insert into PaperReviewRefused (paperId, contactId, requestedBy, reason) values ($prow->paperId, $reqId, $Requester->contactId, 'request denied by chair')"); 228 229 // send anticonfirmation email 230 HotCRPMailer::send_to($Requester, "@denyreviewrequest", $prow, 231 array("reviewer_contact" => (object) array("fullName" => trim($Qreq->name), "email" => $email))); 232 233 $Conf->confirmMsg("Proposed reviewer denied."); 234 } else 235 Conf::msg_error("No one has proposed that " . htmlspecialchars($email) . " review this paper."); 236 Dbl::qx_raw("unlock tables"); 237 unset($Qreq->email, $Qreq->name); 238} 239 240 241// paper table 242$paperTable = new PaperTable($prow, $Qreq, "assign"); 243$paperTable->initialize(false, false); 244$paperTable->resolveReview(false); 245 246confHeader(); 247 248 249// begin form and table 250$paperTable->paptabBegin(); 251 252 253// reviewer information 254$proposals = null; 255if ($Conf->setting("extrev_chairreq")) { 256 $qv = [$prow->paperId]; 257 $q = ""; 258 if (!$Me->allow_administer($prow)) { 259 $q = " and requestedBy=?"; 260 $qv[] = $Me->contactId; 261 } 262 $result = $Conf->qe_apply("select * from ReviewRequest where paperId=?$q", $qv); 263 $proposals = edb_orows($result); 264} 265$t = reviewTable($prow, $paperTable->all_reviews(), null, null, "assign", $proposals); 266$t .= reviewLinks($prow, $paperTable->all_reviews(), null, null, "assign", $allreviewslink); 267if ($t !== "") 268 echo '<hr class="papcard_sep" />', $t; 269 270 271// PC assignments 272if ($Me->can_administer($prow)) { 273 $result = $Conf->qe("select ContactInfo.contactId, allReviews, 274 exists(select paperId from PaperReviewRefused where paperId=? and contactId=ContactInfo.contactId) refused 275 from ContactInfo 276 left join (select contactId, group_concat(reviewType separator '') allReviews 277 from PaperReview join Paper using (paperId) 278 where reviewType>=" . REVIEW_PC . " and timeSubmitted>=0 279 group by contactId) A using (contactId) 280 where ContactInfo.roles!=0 and (ContactInfo.roles&" . Contact::ROLE_PC . ")!=0", 281 $prow->paperId); 282 $pcx = array(); 283 while (($row = edb_orow($result))) 284 $pcx[$row->contactId] = $row; 285 286 // PC conflicts row 287 echo '<hr class="papcard_sep" />', 288 "<h3 style=\"margin-top:0\">PC review assignments</h3>", 289 Ht::form(hoturl_post("assign", "p=$prow->paperId"), array("id" => "ass")), 290 '<p>'; 291 Ht::stash_script('hiliter_children("#ass", true)'); 292 293 if ($Conf->has_topics()) 294 echo "<p>Review preferences display as “P#”, topic scores as “T#”.</p>"; 295 else 296 echo "<p>Review preferences display as “P#”.</p>"; 297 298 echo '<div class="pc_ctable has-assignment-set need-assignment-change"'; 299 $rev_rounds = array_keys($Conf->round_selector_options(false)); 300 echo ' data-review-rounds="', htmlspecialchars(json_encode($rev_rounds)), '"', 301 ' data-default-review-round="', htmlspecialchars($Conf->assignment_round_name(false)), '">'; 302 $tagger = new Tagger($Me); 303 $show_possible_conflicts = $Me->allow_view_authors($prow); 304 305 foreach ($Conf->full_pc_members() as $pc) { 306 $p = $pcx[$pc->contactId]; 307 if (!$pc->can_accept_review_assignment_ignore_conflict($prow)) 308 continue; 309 310 // first, name and assignment 311 $conflict_type = $prow->conflict_type($pc); 312 $rrow = $prow->review_of_user($pc); 313 if ($conflict_type >= CONFLICT_AUTHOR) 314 $revtype = -2; 315 else 316 $revtype = $rrow ? $rrow->reviewType : 0; 317 $pcconfmatch = null; 318 if ($show_possible_conflicts && $revtype != -2) 319 $pcconfmatch = $prow->potential_conflict_html($pc, $conflict_type <= 0); 320 321 $color = $pc->viewable_color_classes($Me); 322 echo '<div class="ctelt">', 323 '<div class="ctelti', ($color ? " $color" : ""), ' has-assignment has-fold foldc" data-pid="', $prow->paperId, 324 '" data-uid="', $pc->contactId, 325 '" data-review-type="', $revtype; 326 if ($conflict_type) 327 echo '" data-conflict-type="1'; 328 if (!$revtype && $p->refused) 329 echo '" data-assignment-refused="', htmlspecialchars($p->refused); 330 if ($rrow && $rrow->reviewRound && ($rn = $rrow->round_name())) 331 echo '" data-review-round="', htmlspecialchars($rn); 332 if ($rrow && $rrow->reviewModified > 1) 333 echo '" data-review-in-progress="'; 334 echo '"><div class="pctbname pctbname', $revtype, ' ui js-assignment-fold">', 335 '<a class="qq taghl ui js-assignment-fold" href="">', expander(null, 0), 336 $Me->name_html_for($pc), '</a>'; 337 if ($revtype != 0) { 338 echo ' ', review_type_icon($revtype, $rrow && !$rrow->reviewSubmitted); 339 if ($rrow && $rrow->reviewRound > 0) 340 echo ' <span class="revround" title="Review round">', 341 htmlspecialchars($Conf->round_name($rrow->reviewRound)), 342 '</span>'; 343 } 344 if ($revtype >= 0) 345 echo unparse_preference_span($prow->reviewer_preference($pc, true)); 346 echo '</div>'; // .pctbname 347 if ($pcconfmatch) 348 echo '<div class="need-tooltip" data-tooltip-class="gray" data-tooltip="', str_replace('"', '"', $pcconfmatch[1]), '">', $pcconfmatch[0], '</div>'; 349 350 // then, number of reviews 351 echo '<div class="pctbnrev">'; 352 $numReviews = strlen($p->allReviews); 353 $numPrimary = substr_count($p->allReviews, REVIEW_PRIMARY); 354 if (!$numReviews) 355 echo "0 reviews"; 356 else { 357 echo "<a class='q' href=\"" 358 . hoturl("search", "q=re:" . urlencode($pc->email)) . "\">" 359 . plural($numReviews, "review") . "</a>"; 360 if ($numPrimary && $numPrimary < $numReviews) 361 echo " (<a class='q' href=\"" 362 . hoturl("search", "q=pri:" . urlencode($pc->email)) 363 . "\">$numPrimary primary</a>)"; 364 } 365 echo "</div></div></div>\n"; // .pctbnrev .ctelti .ctelt 366 } 367 368 echo "</div>\n", 369 '<div class="aab aabr aabig">', 370 '<div class="aabut">', Ht::submit("update", "Save assignments", ["class" => "btn btn-primary"]), '</div>', 371 '<div class="aabut">', Ht::submit("cancel", "Cancel"), '</div>', 372 '<div id="assresult" class="aabut"></div>', 373 '</div></form>'; 374} 375 376 377echo "</div></div>\n"; 378 379// add external reviewers 380$req = "Request an external review"; 381if (!$Me->allow_administer($prow) && $Conf->setting("extrev_chairreq")) 382 $req = "Propose an external review"; 383echo Ht::form(hoturl_post("assign", "p=$prow->paperId"), ["novalidate" => true]), 384 '<div class="revcard"><div class="revcard_head">', 385 "<h3>", $req, "</h3></div><div class=\"revcard_body\">"; 386 387echo '<p class="papertext f-h">External reviewers may view their assigned papers, including '; 388if ($Conf->setting("extrev_view") >= 2) 389 echo "the other reviewers’ identities and "; 390echo "any eventual decision. Before requesting an external review, 391 you should generally check personally whether they are interested."; 392if ($Me->allow_administer($prow)) 393 echo "\nTo create an anonymous review with a review token, leave Name and Email blank."; 394echo '</p>'; 395 396if (($rrow = $prow->review_of_user($Me)) 397 && $rrow->reviewType == REVIEW_SECONDARY 398 && ($round_name = $Conf->round_name($rrow->reviewRound))) 399 echo Ht::hidden("round", $round_name); 400echo '<div class="papertext g">', 401 '<div class="', Ht::control_class("email", "f-i"), '">', 402 Ht::label("Email", "revreq_email"), 403 Ht::entry("email", (string) $Qreq->email, ["id" => "revreq_email", "size" => 52, "class" => "fullw", "autocomplete" => "off", "type" => "email"]), 404 '</div>', 405 '<div class="f-2col">', 406 '<div class="', Ht::control_class("firstName", "f-i"), '">', 407 Ht::label("First name (given name)", "revreq_firstName"), 408 Ht::entry("firstName", (string) $Qreq->firstName, ["id" => "revreq_firstName", "size" => 24, "class" => "fullw", "autocomplete" => "off"]), 409 '</div><div class="', Ht::control_class("lastName", "f-i"), '">', 410 Ht::label("Last name (family name)", "revreq_lastName"), 411 Ht::entry("lastName", (string) $Qreq->lastName, ["id" => "revreq_lastName", "size" => 24, "class" => "fullw", "autocomplete" => "off"]), 412 '</div></div>', 413 '<div class="', Ht::control_class("affiliation", "f-i"), '">', 414 Ht::label("Affiliation", "revreq_affiliation"), 415 Ht::entry("affiliation", (string) $Qreq->affiliation, ["id" => "revreq_affiliation", "size" => 52, "class" => "fullw", "autocomplete" => "off"]), 416 '</div>'; 417 418// reason area 419$null_mailer = new HotCRPMailer($Conf); 420$reqbody = $null_mailer->expand_template("requestreview", false); 421if (strpos($reqbody["body"], "%REASON%") !== false) { 422 echo '<div class="f-i">', 423 Ht::label('Note to reviewer <span class="n">(optional)</span>', "revreq_reason"), 424 Ht::textarea("reason", $Qreq->reason, 425 ["class" => "need-autogrow fullw", "rows" => 2, "cols" => 60, "spellcheck" => "true", "id" => "revreq_reason"]), 426 "</div>\n\n"; 427} 428 429if ($Me->can_administer($prow)) 430 echo '<div class="', Ht::control_class("override", "checki"), '"><label><span class="checkc">', 431 Ht::checkbox("override"), 432 ' </span>Override deadlines, declined requests, and potential conflicts</label></div>'; 433 434echo "<div class='f-i'>\n", 435 Ht::submit("requestreview", "Request review", ["class" => "btn btn-primary"]), 436 "</div>\n\n"; 437Ht::stash_script("\$(\"#revreq_email\").on(\"input\",revreq_email_input)"); 438 439echo "</div></div></div></form>\n"; 440 441$Conf->footer(); 442