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 .= " &#x2691;";
107                else if ($all & 1)
108                    $t .= " &#x2690;";
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 .= '&nbsp;<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) . "&nbsp;<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&amp;m=edit") . '" class="xx revlink">'
364            . Ht::img("edit48.png", "[Edit]", $dlimgjs) . "&nbsp;<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) . "&nbsp;<u><b>Edit your review</b></u></a>";
375        else
376            $x = $a . Ht::img("review48.png", "[Your review]", $dlimgjs) . "&nbsp;<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&amp;m=re") . '" class="xx revlink">'
380            . Ht::img("review48.png", "[Write review]", $dlimgjs) . "&nbsp;<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) . "&nbsp;<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) . "&nbsp;<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) . "&nbsp;"
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") . "&nbsp;<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