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('"', '&quot;', $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 "&nbsp; (<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