1<?php
2// papercolumn.php -- HotCRP helper classes for paper list content
3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE.
4
5class PaperColumn extends Column {
6    const OVERRIDE_NONE = 0;
7    const OVERRIDE_FOLD_IFEMPTY = 1;
8    const OVERRIDE_FOLD_BOTH = 2;
9    const OVERRIDE_ALWAYS = 3;
10    public $override = 0;
11
12    const PREP_SORT = -1;
13    const PREP_FOLDED = 0; // value matters
14    const PREP_VISIBLE = 1; // value matters
15
16    function __construct(Conf $conf, $cj) {
17        parent::__construct($cj);
18    }
19
20    static function make(Conf $conf, $cj) {
21        if ($cj->callback[0] === "+") {
22            $class = substr($cj->callback, 1);
23            return new $class($conf, $cj);
24        } else
25            return call_user_func($cj->callback, $conf, $cj);
26    }
27
28
29    function mark_editable() {
30    }
31
32    function prepare(PaperList $pl, $visible) {
33        return true;
34    }
35    function realize(PaperList $pl) {
36        return $this;
37    }
38    function annotate_field_js(PaperList $pl, &$fjs) {
39    }
40
41    function analyze(PaperList $pl, &$rows, $fields) {
42    }
43    function analyze_sort(PaperList $pl, &$rows, ListSorter $sorter) {
44    }
45    function compare(PaperInfo $a, PaperInfo $b, ListSorter $sorter) {
46        error_log("unexpected compare " . json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)));
47        return $a->paperId - $b->paperId;
48    }
49
50    function header(PaperList $pl, $is_text) {
51        if ($is_text)
52            return "<" . $this->name . ">";
53        else
54            return "&lt;" . htmlspecialchars($this->name) . "&gt;";
55    }
56    function completion_name() {
57        if (!$this->completion)
58            return false;
59        else if (is_string($this->completion))
60            return $this->completion;
61        else
62            return $this->name;
63    }
64    function sort_name($score_sort) {
65        return $this->name;
66    }
67
68    function content_empty(PaperList $pl, PaperInfo $row) {
69        return false;
70    }
71
72    function content(PaperList $pl, PaperInfo $row) {
73        return "";
74    }
75    function text(PaperList $pl, PaperInfo $row) {
76        return "";
77    }
78
79    function has_statistics() {
80        return false;
81    }
82}
83
84class IdPaperColumn extends PaperColumn {
85    function __construct(Conf $conf, $cj) {
86        parent::__construct($conf, $cj);
87    }
88    function header(PaperList $pl, $is_text) {
89        return "ID";
90    }
91    function compare(PaperInfo $a, PaperInfo $b, ListSorter $sorter) {
92        return $a->paperId - $b->paperId;
93    }
94    function content(PaperList $pl, PaperInfo $row) {
95        $href = $pl->_paperLink($row);
96        return "<a href=\"$href\" class=\"pnum taghl\">#$row->paperId</a>";
97    }
98    function text(PaperList $pl, PaperInfo $row) {
99        return $row->paperId;
100    }
101}
102
103class SelectorPaperColumn extends PaperColumn {
104    function __construct(Conf $conf, $cj) {
105        parent::__construct($conf, $cj);
106    }
107    function header(PaperList $pl, $is_text) {
108        return $is_text ? "Selected" : "";
109    }
110    protected function checked(PaperList $pl, PaperInfo $row) {
111        return $pl->is_selected($row->paperId, $this->name == "selon");
112    }
113    function content(PaperList $pl, PaperInfo $row) {
114        $pl->mark_has("sel");
115        $c = "";
116        if ($this->checked($pl, $row))
117            $c .= ' checked="checked"';
118        return '<span class="pl_rownum fx6">' . $pl->count . '. </span>'
119            . '<input type="checkbox" class="uix js-range-click" name="pap[]" value="' . $row->paperId . '"' . $c . ' />';
120    }
121    function text(PaperList $pl, PaperInfo $row) {
122        return $this->checked($pl, $row) ? "Y" : "N";
123    }
124}
125
126class TitlePaperColumn extends PaperColumn {
127    private $has_decoration = false;
128    private $highlight = false;
129    function __construct(Conf $conf, $cj) {
130        parent::__construct($conf, $cj);
131    }
132    function prepare(PaperList $pl, $visible) {
133        $this->has_decoration = $pl->user->can_view_tags(null)
134            && $pl->conf->tags()->has_decoration;
135        if ($this->has_decoration)
136            $pl->qopts["tags"] = 1;
137        $this->highlight = $pl->search->field_highlighter("title");
138        return true;
139    }
140    function compare(PaperInfo $a, PaperInfo $b, ListSorter $sorter) {
141        $cmp = strcasecmp($a->unaccented_title(), $b->unaccented_title());
142        if (!$cmp)
143            $cmp = strcasecmp($a->title, $b->title);
144        return $cmp;
145    }
146    function header(PaperList $pl, $is_text) {
147        return "Title";
148    }
149    function content(PaperList $pl, PaperInfo $row) {
150        $t = '<a href="' . $pl->_paperLink($row) . '" class="ptitle taghl';
151
152        if ($row->title !== "")
153            $highlight_text = Text::highlight($row->title, $this->highlight, $highlight_count);
154        else {
155            $highlight_text = "[No title]";
156            $highlight_count = 0;
157        }
158
159        if (!$highlight_count && ($format = $row->title_format())) {
160            $pl->need_render = true;
161            $t .= ' need-format" data-format="' . $format
162                . '" data-title="' . htmlspecialchars($row->title);
163        }
164
165        $t .= '">' . $highlight_text . '</a>'
166            . $pl->_contentDownload($row);
167
168        if ($this->has_decoration && (string) $row->paperTags !== "") {
169            if ($pl->row_tags_overridable
170                && ($deco = $pl->tagger->unparse_decoration_html($pl->row_tags_overridable))) {
171                $decx = $pl->tagger->unparse_decoration_html($pl->row_tags);
172                if ($deco !== $decx) {
173                    if ($decx)
174                        $t .= '<span class="fn5">' . $decx . '</span>';
175                    $t .= '<span class="fx5">' . $deco . '</span>';
176                } else
177                    $t .= $deco;
178            } else if ($pl->row_tags)
179                $t .= $pl->tagger->unparse_decoration_html($pl->row_tags);
180        }
181
182        return $t;
183    }
184    function text(PaperList $pl, PaperInfo $row) {
185        return $row->title;
186    }
187}
188
189class StatusPaperColumn extends PaperColumn {
190    private $is_long;
191    function __construct(Conf $conf, $cj) {
192        parent::__construct($conf, $cj);
193        $this->is_long = $cj->name === "statusfull";
194        $this->override = PaperColumn::OVERRIDE_FOLD_BOTH;
195    }
196    function analyze_sort(PaperList $pl, &$rows, ListSorter $sorter) {
197        foreach ($rows as $row) {
198            if ($row->outcome && $pl->user->can_view_decision($row))
199                $row->_status_sort_info = $row->outcome;
200            else
201                $row->_status_sort_info = -10000;
202        }
203    }
204    function compare(PaperInfo $a, PaperInfo $b, ListSorter $sorter) {
205        $x = $b->_status_sort_info - $a->_status_sort_info;
206        $x = $x ? : ($a->timeWithdrawn > 0) - ($b->timeWithdrawn > 0);
207        $x = $x ? : ($b->timeSubmitted > 0) - ($a->timeSubmitted > 0);
208        return $x ? : ($b->paperStorageId > 1) - ($a->paperStorageId > 1);
209    }
210    function header(PaperList $pl, $is_text) {
211        return "Status";
212    }
213    function content(PaperList $pl, PaperInfo $row) {
214        $status_info = $pl->user->paper_status_info($row, !$pl->search->limit_author() && $pl->user->can_administer($row));
215        if (!$this->is_long && $status_info[0] == "pstat_sub")
216            return "";
217        return "<span class=\"pstat $status_info[0]\">" . htmlspecialchars($status_info[1]) . "</span>";
218    }
219    function text(PaperList $pl, PaperInfo $row) {
220        $status_info = $pl->user->paper_status_info($row, !$pl->search->limit_author() && $pl->user->allow_administer($row));
221        return $status_info[1];
222    }
223}
224
225class ReviewStatus_PaperColumn extends PaperColumn {
226    private $round;
227    function __construct(Conf $conf, $cj) {
228        parent::__construct($conf, $cj);
229        $this->override = PaperColumn::OVERRIDE_FOLD_BOTH;
230        $this->round = get($cj, "round", null);
231    }
232    function prepare(PaperList $pl, $visible) {
233        if ($pl->user->privChair || $pl->user->is_reviewer() || $pl->conf->can_some_author_view_review()) {
234            $pl->qopts["reviewSignatures"] = true;
235            return true;
236        } else
237            return false;
238    }
239    private function data(PaperInfo $row, Contact $user) {
240        $want_assigned = !$row->conflict_type($user) || $user->can_administer($row);
241        $done = $started = 0;
242        foreach ($row->reviews_by_id() as $rrow)
243            if ($user->can_view_review_assignment($row, $rrow)
244                && ($this->round === null || $this->round === $rrow->reviewRound)) {
245                if ($rrow->reviewSubmitted > 0) {
246                    ++$done;
247                    ++$started;
248                } else if ($want_assigned ? $rrow->reviewNeedsSubmit > 0 : $rrow->reviewModified > 0)
249                    ++$started;
250            }
251        return [$done, $started];
252    }
253    function analyze_sort(PaperList $pl, &$rows, ListSorter $sorter) {
254        foreach ($rows as $row) {
255            if (!$pl->user->can_view_review_assignment($row, null))
256                $row->_review_status_sort_info = -2147483647;
257            else {
258                list($done, $started) = $this->data($row, $pl->user);
259                $row->_review_status_sort_info = $done + $started / 1000.0;
260            }
261        }
262    }
263    function compare(PaperInfo $a, PaperInfo $b, ListSorter $sorter) {
264        $av = $a->_review_status_sort_info;
265        $bv = $b->_review_status_sort_info;
266        return ($av < $bv ? 1 : ($av == $bv ? 0 : -1));
267    }
268    function header(PaperList $pl, $is_text) {
269        $round_name = "";
270        if ($this->round !== null)
271            $round_name = ($pl->conf->round_name($this->round) ? : "unnamed") . " ";
272        if ($is_text)
273            return "# {$round_name}Reviews";
274        else
275            return '<span class="need-tooltip" data-tooltip="# completed reviews / # assigned reviews" data-tooltip-dir="b">#&nbsp;' . $round_name . 'Reviews</span>';
276    }
277    function content_empty(PaperList $pl, PaperInfo $row) {
278        return !$pl->user->can_view_review_assignment($row, null);
279    }
280    function content(PaperList $pl, PaperInfo $row) {
281        list($done, $started) = $this->data($row, $pl->user);
282        return "<b>$done</b>" . ($done == $started ? "" : "/$started");
283    }
284    function text(PaperList $pl, PaperInfo $row) {
285        list($done, $started) = $this->data($row, $pl->user);
286        return $done . ($done == $started ? "" : "/$started");
287    }
288}
289
290class Authors_PaperColumn extends PaperColumn {
291    private $aufull;
292    private $anonau;
293    private $highlight;
294    function __construct(Conf $conf, $cj) {
295        parent::__construct($conf, $cj);
296    }
297    function header(PaperList $pl, $is_text) {
298        return "Authors";
299    }
300    function prepare(PaperList $pl, $visible) {
301        $this->aufull = !$pl->is_folded("aufull");
302        $this->anonau = !$pl->is_folded("anonau");
303        $this->highlight = $pl->search->field_highlighter("authorInformation");
304        return $pl->user->can_view_some_authors();
305    }
306    private function affiliation_map($row) {
307        $nonempty_count = 0;
308        $aff = [];
309        foreach ($row->author_list() as $i => $au) {
310            if ($i != 0 && $au->affiliation === $aff[$i - 1])
311                $aff[$i - 1] = null;
312            $aff[] = $au->affiliation;
313            $nonempty_count += ($au->affiliation !== "");
314        }
315        if ($nonempty_count != 0 && $nonempty_count != count($aff)) {
316            foreach ($aff as &$affx)
317                if ($affx === "")
318                    $affx = "unaffiliated";
319        }
320        return $aff;
321    }
322    function content_empty(PaperList $pl, PaperInfo $row) {
323        return !$pl->user->allow_view_authors($row);
324    }
325    function content(PaperList $pl, PaperInfo $row) {
326        $out = [];
327        if (!$this->highlight && !$this->aufull) {
328            foreach ($row->author_list() as $au)
329                $out[] = $au->abbrevname_html();
330            $t = join(", ", $out);
331        } else {
332            $affmap = $this->affiliation_map($row);
333            $aus = $affout = [];
334            $any_affhl = false;
335            foreach ($row->author_list() as $i => $au) {
336                $name = Text::highlight($au->name(), $this->highlight, $didhl);
337                if (!$this->aufull
338                    && ($first = htmlspecialchars($au->firstName))
339                    && (!$didhl || substr($name, 0, strlen($first)) === $first)
340                    && ($initial = Text::initial($first)) !== "")
341                    $name = $initial . substr($name, strlen($first));
342                $auy[] = $name;
343                if ($affmap[$i] !== null) {
344                    $out[] = join(", ", $auy);
345                    $affout[] = Text::highlight($affmap[$i], $this->highlight, $didhl);
346                    $any_affhl = $any_affhl || $didhl;
347                    $auy = [];
348                }
349            }
350            // $affout[0] === "" iff there are no nonempty affiliations
351            if (($any_affhl || $this->aufull)
352                && !empty($out)
353                && $affout[0] !== "") {
354                foreach ($out as $i => &$x)
355                    $x .= ' <span class="auaff">(' . $affout[$i] . ')</span>';
356            }
357            $t = join($any_affhl || $this->aufull ? "; " : ", ", $out);
358        }
359        if ($pl->conf->submission_blindness() !== Conf::BLIND_NEVER
360            && !$pl->user->can_view_authors($row))
361            $t = '<div class="fx2">' . $t . '</div>';
362        return $t;
363    }
364    function text(PaperList $pl, PaperInfo $row) {
365        if (!$pl->user->can_view_authors($row) && !$this->anonau)
366            return "";
367        $out = [];
368        if (!$this->aufull) {
369            foreach ($row->author_list() as $au)
370                $out[] = $au->abbrevname_text();
371            return join("; ", $out);
372        } else {
373            $affmap = $this->affiliation_map($row);
374            $aus = [];
375            foreach ($row->author_list() as $i => $au) {
376                $aus[] = $au->name();
377                if ($affmap[$i] !== null) {
378                    $aff = ($affmap[$i] !== "" ? " ($affmap[$i])" : "");
379                    $out[] = commajoin($aus) . $aff;
380                    $aus = [];
381                }
382            }
383            return join("; ", $out);
384        }
385    }
386}
387
388class Collab_PaperColumn extends PaperColumn {
389    function __construct(Conf $conf, $cj) {
390        parent::__construct($conf, $cj);
391        $this->override = PaperColumn::OVERRIDE_FOLD_IFEMPTY;
392    }
393    function prepare(PaperList $pl, $visible) {
394        return !!$pl->conf->setting("sub_collab") && $pl->user->can_view_some_authors();
395    }
396    function header(PaperList $pl, $is_text) {
397        return "Collaborators";
398    }
399    function content_empty(PaperList $pl, PaperInfo $row) {
400        return $row->collaborators == ""
401            || strcasecmp($row->collaborators, "None") == 0
402            || !$pl->user->allow_view_authors($row);
403    }
404    function content(PaperList $pl, PaperInfo $row) {
405        $x = "";
406        foreach (explode("\n", $row->collaborators) as $c)
407            $x .= ($x === "" ? "" : ", ") . trim($c);
408        return Text::highlight($x, $pl->search->field_highlighter("collaborators"));
409    }
410    function text(PaperList $pl, PaperInfo $row) {
411        $x = "";
412        foreach (explode("\n", $row->collaborators) as $c)
413            $x .= ($x === "" ? "" : ", ") . trim($c);
414        return $x;
415    }
416}
417
418class Abstract_PaperColumn extends PaperColumn {
419    function __construct(Conf $conf, $cj) {
420        parent::__construct($conf, $cj);
421    }
422    function header(PaperList $pl, $is_text) {
423        return "Abstract";
424    }
425    function content_empty(PaperList $pl, PaperInfo $row) {
426        return $row->abstract == "";
427    }
428    function content(PaperList $pl, PaperInfo $row) {
429        $t = Text::highlight($row->abstract, $pl->search->field_highlighter("abstract"), $highlight_count);
430        $klass = strlen($t) > 190 ? "pl_longtext" : "pl_shorttext";
431        if (!$highlight_count && ($format = $row->format_of($row->abstract))) {
432            $pl->need_render = true;
433            $t = '<div class="' . $klass . ' need-format" data-format="'
434                . $format . '.abs.plx">' . $t . '</div>';
435        } else
436            $t = '<div class="' . $klass . ' format0">' . Ht::format0($t) . '</div>';
437        return $t;
438    }
439    function text(PaperList $pl, PaperInfo $row) {
440        return $row->abstract;
441    }
442}
443
444class ReviewerType_PaperColumn extends PaperColumn {
445    protected $contact;
446    private $not_me;
447    private $rrow_key;
448    function __construct(Conf $conf, $cj) {
449        parent::__construct($conf, $cj);
450        if ($conf && isset($cj->user))
451            $this->contact = $conf->pc_member_by_email($cj->user);
452    }
453    function contact() {
454        return $this->contact;
455    }
456    function prepare(PaperList $pl, $visible) {
457        $this->contact = $this->contact ? : $pl->reviewer_user();
458        $this->not_me = $this->contact->contactId !== $pl->user->contactId;
459        return true;
460    }
461    const F_CONFLICT = 1;
462    const F_LEAD = 2;
463    const F_SHEPHERD = 4;
464    private function analysis(PaperList $pl, PaperInfo $row) {
465        $rrow = $row->review_of_user($this->contact);
466        if ($rrow && (!$this->not_me || $pl->user->can_view_review_identity($row, $rrow)))
467            $ranal = $pl->make_review_analysis($rrow, $row);
468        else
469            $ranal = null;
470        if ($ranal && !$ranal->rrow->reviewSubmitted)
471            $pl->mark_has("need_review");
472        $flags = 0;
473        if ($row->conflict_type($this->contact)
474            && (!$this->not_me || $pl->user->can_view_conflicts($row)))
475            $flags |= self::F_CONFLICT;
476        if ($row->leadContactId == $this->contact->contactId
477            && (!$this->not_me || $pl->user->can_view_lead($row)))
478            $flags |= self::F_LEAD;
479        if ($row->shepherdContactId == $this->contact->contactId
480            && (!$this->not_me || $pl->user->can_view_shepherd($row)))
481            $flags |= self::F_SHEPHERD;
482        return [$ranal, $flags];
483    }
484    function analyze_sort(PaperList $pl, &$rows, ListSorter $sorter) {
485        $k = $sorter->uid;
486        foreach ($rows as $row) {
487            list($ranal, $flags) = $this->analysis($pl, $row);
488            if ($ranal && $ranal->rrow->reviewType) {
489                $row->$k = 2 * $ranal->rrow->reviewType;
490                if ($ranal->rrow->reviewSubmitted)
491                    $row->$k += 1;
492            } else
493                $row->$k = ($flags & self::F_CONFLICT ? -2 : 0);
494            if ($flags & self::F_LEAD)
495                $row->$k += 30;
496            if ($flags & self::F_SHEPHERD)
497                $row->$k += 60;
498        }
499    }
500    function compare(PaperInfo $a, PaperInfo $b, ListSorter $sorter) {
501        $k = $sorter->uid;
502        return $b->$k - $a->$k;
503    }
504    function header(PaperList $pl, $is_text) {
505        if (!$this->not_me || $pl->report_id() === "conflictassign")
506            return "Review";
507        else if ($is_text)
508            return $pl->user->name_text_for($this->contact) . " review";
509        else
510            return $pl->user->name_html_for($this->contact) . "<br />review";
511    }
512    function content(PaperList $pl, PaperInfo $row) {
513        list($ranal, $flags) = $this->analysis($pl, $row);
514        $t = "";
515        if ($ranal)
516            $t = $ranal->icon_html(true);
517        else if ($flags & self::F_CONFLICT)
518            $t = review_type_icon(-1);
519        $x = null;
520        if ($flags & self::F_LEAD)
521            $x[] = review_lead_icon();
522        if ($flags & self::F_SHEPHERD)
523            $x[] = review_shepherd_icon();
524        if ($x || ($ranal && $ranal->round)) {
525            $c = ["pl_revtype"];
526            $t && ($c[] = "hasrev");
527            ($flags & (self::F_LEAD | self::F_SHEPHERD)) && ($c[] = "haslead");
528            $ranal && $ranal->round && ($c[] = "hasround");
529            $t && ($x[] = $t);
530            return '<div class="' . join(" ", $c) . '">' . join('&nbsp;', $x) . '</div>';
531        } else
532            return $t;
533    }
534    function text(PaperList $pl, PaperInfo $row) {
535        list($ranal, $flags) = $this->analysis($pl, $row);
536        $t = null;
537        if ($flags & self::F_LEAD)
538            $t[] = "Lead";
539        if ($flags & self::F_SHEPHERD)
540            $t[] = "Shepherd";
541        if ($ranal)
542            $t[] = $ranal->icon_text();
543        if ($flags & self::F_CONFLICT)
544            $t[] = "Conflict";
545        return $t ? join("; ", $t) : "";
546    }
547}
548
549class AssignReview_PaperColumn extends ReviewerType_PaperColumn {
550    function __construct(Conf $conf, $cj) {
551        parent::__construct($conf, $cj);
552    }
553    function prepare(PaperList $pl, $visible) {
554        return parent::prepare($pl, $visible) && $pl->user->is_manager();
555    }
556    function header(PaperList $pl, $is_text) {
557        if ($is_text)
558            return $pl->user->name_text_for($this->contact) . " assignment";
559        else
560            return $pl->user->name_html_for($this->contact) . "<br />assignment";
561    }
562    function content_empty(PaperList $pl, PaperInfo $row) {
563        return !$pl->user->allow_administer($row);
564    }
565    function content(PaperList $pl, PaperInfo $row) {
566        $ci = $row->contact_info($this->contact);
567        if ($ci->conflictType >= CONFLICT_AUTHOR)
568            return '<span class="author">Author</span>';
569        if ($ci->conflictType > 0)
570            $rt = -1;
571        else
572            $rt = min(max($ci->reviewType, 0), REVIEW_META);
573        if ($this->contact->can_accept_review_assignment_ignore_conflict($row)
574            || $rt > 0)
575            $options = array(0 => "None",
576                             REVIEW_PRIMARY => "Primary",
577                             REVIEW_SECONDARY => "Secondary",
578                             REVIEW_PC => "Optional",
579                             REVIEW_META => "Metareview",
580                             -1 => "Conflict");
581        else
582            $options = array(0 => "None", -1 => "Conflict");
583        return Ht::select("assrev{$row->paperId}u{$this->contact->contactId}",
584                          $options, $rt, ["class" => "uich js-assign-review", "tabindex" => 2]);
585    }
586}
587
588class PreferenceList_PaperColumn extends PaperColumn {
589    private $topics;
590    function __construct(Conf $conf, $cj) {
591        parent::__construct($conf, $cj);
592        $this->topics = get($cj, "topics");
593    }
594    function prepare(PaperList $pl, $visible) {
595        if ($this->topics && !$pl->conf->has_topics())
596            $this->topics = false;
597        if (!$pl->user->is_manager())
598            return false;
599        if ($visible) {
600            $pl->qopts["allReviewerPreference"] = true;
601            if ($this->topics)
602                $pl->qopts["topics"] = true;
603        }
604        $pl->conf->stash_hotcrp_pc($pl->user);
605        return true;
606    }
607    function header(PaperList $pl, $is_text) {
608        return "Preferences";
609    }
610    function content_empty(PaperList $pl, PaperInfo $row) {
611        return !$pl->user->allow_administer($row);
612    }
613    function content(PaperList $pl, PaperInfo $row) {
614        $prefs = $row->reviewer_preferences();
615        $ts = array();
616        if ($prefs || $this->topics)
617            foreach ($row->conf->pc_members() as $pcid => $pc) {
618                if (($pref = get($prefs, $pcid))
619                    && ($pref[0] !== 0 || $pref[1] !== null)) {
620                    $t = "P" . $pref[0];
621                    if ($pref[1] !== null)
622                        $t .= unparse_expertise($pref[1]);
623                    $ts[] = $pcid . $t;
624                } else if ($this->topics
625                           && ($tscore = $row->topic_interest_score($pc)))
626                    $ts[] = $pcid . "T" . $tscore;
627            }
628        $pl->row_attr["data-allpref"] = join(" ", $ts);
629        if (!empty($ts)) {
630            $t = '<span class="need-allpref">Loading</span>';
631            $pl->need_render = true;
632            return $t;
633        } else
634            return '';
635    }
636}
637
638class ReviewerList_PaperColumn extends PaperColumn {
639    private $topics;
640    function __construct(Conf $conf, $cj) {
641        parent::__construct($conf, $cj);
642    }
643    function prepare(PaperList $pl, $visible) {
644        if (!$pl->user->can_view_some_review_identity())
645            return false;
646        $this->topics = $pl->conf->has_topics();
647        $pl->qopts["reviewSignatures"] = true;
648        if ($pl->conf->review_blindness() === Conf::BLIND_OPTIONAL)
649            $this->override = PaperColumn::OVERRIDE_FOLD_BOTH;
650        else
651            $this->override = PaperColumn::OVERRIDE_FOLD_IFEMPTY;
652        return true;
653    }
654    function header(PaperList $pl, $is_text) {
655        return "Reviewers";
656    }
657    function content_empty(PaperList $pl, PaperInfo $row) {
658        return !$pl->user->can_view_review_identity($row, null);
659    }
660    function content(PaperList $pl, PaperInfo $row) {
661        // see also search.php > getaction == "reviewers"
662        $x = [];
663        foreach ($row->reviews_by_display() as $xrow)
664            if ($pl->user->can_view_review_identity($row, $xrow)) {
665                $ranal = $pl->make_review_analysis($xrow, $row);
666                $x[] = $pl->user->reviewer_html_for($xrow) . " " . $ranal->icon_html(false);
667            }
668        if ($x)
669            return '<span class="nb">' . join(',</span> <span class="nb">', $x) . '</span>';
670        else
671            return "";
672    }
673    function text(PaperList $pl, PaperInfo $row) {
674        $x = [];
675        foreach ($row->reviews_by_display() as $xrow)
676            if ($pl->user->can_view_review_identity($row, $xrow))
677                $x[] = $pl->user->name_text_for($xrow);
678        return join("; ", $x);
679    }
680}
681
682class TagList_PaperColumn extends PaperColumn {
683    private $editable;
684    function __construct(Conf $conf, $cj, $editable = false) {
685        parent::__construct($conf, $cj);
686        $this->override = PaperColumn::OVERRIDE_ALWAYS;
687        $this->editable = $editable;
688    }
689    function mark_editable() {
690        $this->editable = true;
691    }
692    function prepare(PaperList $pl, $visible) {
693        if (!$pl->user->can_view_tags(null))
694            return false;
695        if ($visible)
696            $pl->qopts["tags"] = 1;
697        if ($visible && $this->editable)
698            $pl->has_editable_tags = true;
699        $pl->need_tag_attr = true;
700        return true;
701    }
702    function annotate_field_js(PaperList $pl, &$fjs) {
703        $fjs["highlight_tags"] = $pl->search->highlight_tags();
704        if ($pl->conf->tags()->has_votish)
705            $fjs["votish_tags"] = array_values(array_map(function ($t) { return $t->tag; }, $pl->conf->tags()->filter("votish")));
706    }
707    function header(PaperList $pl, $is_text) {
708        return "Tags";
709    }
710    function content_empty(PaperList $pl, PaperInfo $row) {
711        return !$pl->user->can_view_tags($row);
712    }
713    function content(PaperList $pl, PaperInfo $row) {
714        if ($this->editable)
715            $pl->row_attr["data-tags-editable"] = 1;
716        if ($this->editable || $pl->row_tags || $pl->row_tags_overridable) {
717            $pl->need_render = true;
718            return '<span class="need-tags"></span>';
719        } else
720            return "";
721    }
722    function text(PaperList $pl, PaperInfo $row) {
723        return $pl->tagger->unparse_hashed($row->viewable_tags($pl->user));
724    }
725}
726
727class Tag_PaperColumn extends PaperColumn {
728    private $is_value;
729    private $dtag;
730    private $ltag;
731    private $ctag;
732    private $editable = false;
733    private $emoji = false;
734    private $editsort;
735    function __construct(Conf $conf, $cj) {
736        parent::__construct($conf, $cj);
737        $this->override = PaperColumn::OVERRIDE_FOLD_IFEMPTY;
738        $this->dtag = $cj->tag;
739        $this->is_value = get($cj, "tagvalue");
740    }
741    function mark_editable() {
742        $this->editable = true;
743        if ($this->is_value === null)
744            $this->is_value = true;
745    }
746    function sorts_my_tag($sorter, Contact $user) {
747        return strcasecmp(Tagger::check_tag_keyword($sorter->type, $user, Tagger::NOVALUE | Tagger::ALLOWCONTACTID), $this->ltag) == 0;
748    }
749    function prepare(PaperList $pl, $visible) {
750        if (!$pl->user->can_view_tags(null))
751            return false;
752        $tagger = new Tagger($pl->user);
753        if (!($ctag = $tagger->check($this->dtag, Tagger::NOVALUE | Tagger::ALLOWCONTACTID)))
754            return false;
755        $this->ltag = strtolower($ctag);
756        $this->ctag = " {$this->ltag}#";
757        if ($visible)
758            $pl->qopts["tags"] = 1;
759        if ($this->ltag[0] == ":"
760            && !$this->is_value
761            && ($dt = $pl->user->conf->tags()->check($this->dtag))
762            && count($dt->emoji) == 1)
763            $this->emoji = $dt->emoji[0];
764        if ($this->editable && $visible > 0 && ($tid = $pl->table_id())) {
765            $sorter = get($pl->sorters, 0);
766            if ($this->sorts_my_tag($sorter, $pl->user)
767                && !$sorter->reverse
768                && (!$pl->search->thenmap || $pl->search->is_order_anno)
769                && $this->is_value) {
770                $this->editsort = true;
771                $pl->table_attr["data-drag-tag"] = $this->dtag;
772            }
773            $pl->has_editable_tags = true;
774        }
775        $this->className = ($this->editable ? "pl_edit" : "pl_")
776            . ($this->is_value ? "tagval" : "tag");
777        $pl->need_tag_attr = true;
778        return true;
779    }
780    function completion_name() {
781        return "#$this->dtag";
782    }
783    function sort_name($score_sort) {
784        return "#$this->dtag";
785    }
786    function analyze_sort(PaperList $pl, &$rows, ListSorter $sorter) {
787        $k = $sorter->uid;
788        $unviewable = $empty = TAG_INDEXBOUND * ($sorter->reverse ? -1 : 1);
789        if ($this->editable)
790            $empty = (TAG_INDEXBOUND - 1) * ($sorter->reverse ? -1 : 1);
791        foreach ($rows as $row) {
792            if (!$pl->user->can_view_tag($row, $this->ltag))
793                $row->$k = $unviewable;
794            else if (($row->$k = $row->tag_value($this->ltag)) === false)
795                $row->$k = $empty;
796        }
797    }
798    function compare(PaperInfo $a, PaperInfo $b, ListSorter $sorter) {
799        $k = $sorter->uid;
800        return $a->$k < $b->$k ? -1 : ($a->$k == $b->$k ? 0 : 1);
801    }
802    function header(PaperList $pl, $is_text) {
803        if (($twiddle = strpos($this->dtag, "~")) > 0) {
804            $cid = (int) substr($this->dtag, 0, $twiddle);
805            if ($cid == $pl->user->contactId)
806                return "#" . substr($this->dtag, $twiddle);
807            else if (($p = $pl->conf->cached_user_by_id($cid))) {
808                if ($is_text)
809                    return $pl->user->name_text_for($p) . " #" . substr($this->dtag, $twiddle);
810                else
811                    return $pl->user->name_html_for($p) . "<br />#" . substr($this->dtag, $twiddle);
812            }
813        }
814        return "#$this->dtag";
815    }
816    function content_empty(PaperList $pl, PaperInfo $row) {
817        return !$pl->user->can_view_tag($row, $this->ltag);
818    }
819    function content(PaperList $pl, PaperInfo $row) {
820        $v = $row->tag_value($this->ltag);
821        if ($this->editable
822            && ($t = $this->edit_content($pl, $row, $v)))
823            return $t;
824        else if ($v === false)
825            return "";
826        else if ($v >= 0.0 && $this->emoji)
827            return Tagger::unparse_emoji_html($this->emoji, $v);
828        else if ($v === 0.0 && !$this->is_value)
829            return "✓";
830        else
831            return $v;
832    }
833    private function edit_content($pl, $row, $v) {
834        if (!$pl->user->can_change_tag($row, $this->dtag, 0, 0))
835            return false;
836        if (!$this->is_value) {
837            return "<input type=\"checkbox\" class=\"uix js-range-click edittag\" data-range-type=\"tag:{$this->dtag}\" name=\"tag:{$this->dtag} {$row->paperId}\" value=\"x\" tabindex=\"2\""
838                . ($v !== false ? ' checked="checked"' : '') . " />";
839        }
840        $t = '<input type="text" class="edittagval';
841        if ($this->editsort) {
842            $t .= " need-draghandle";
843            $pl->need_render = true;
844        }
845        return $t . '" size="4" name="tag:' . "$this->dtag $row->paperId" . '" value="'
846            . ($v !== false ? htmlspecialchars($v) : "") . '" tabindex="2" />';
847    }
848    function text(PaperList $pl, PaperInfo $row) {
849        if (($v = $row->tag_value($this->ltag)) === false)
850            return "";
851        else if ($v === 0.0 && !$this->is_value)
852            return "Y";
853        else
854            return $v;
855    }
856}
857
858class Tag_PaperColumnFactory {
859    static function expand($name, Conf $conf, $xfj, $m) {
860        $tagger = new Tagger($conf->xt_user);
861        $ts = [];
862        if (($twiddle = strpos($m[2], "~")) > 0
863            && !ctype_digit(substr($m[2], 0, $twiddle))) {
864            $utext = substr($m[2], 0, $twiddle);
865            foreach (ContactSearch::make_pc($utext, $conf->xt_user)->ids as $cid) {
866                $ts[] = $cid . substr($m[2], $twiddle);
867            }
868            if (!$ts) {
869                $conf->xt_factory_error("No PC member matches “" . htmlspecialchars($utext) . "”.");
870            }
871        } else {
872            $ts[] = $m[2];
873        }
874        $flags = Tagger::NOVALUE | ($conf->xt_user->is_manager() ? Tagger::ALLOWCONTACTID : 0);
875        $rs = [];
876        foreach ($ts as $t) {
877            if ($tagger->check($t, $flags)) {
878                $fj = (array) $xfj;
879                $fj["name"] = $m[1] . $t;
880                $fj["tag"] = $t;
881                $rs[] = (object) $fj;
882            } else {
883                $conf->xt_factory_error($tagger->error_html);
884            }
885        }
886        return $rs;
887    }
888}
889
890class ScoreGraph_PaperColumn extends PaperColumn {
891    protected $contact;
892    protected $not_me;
893    protected $format_field;
894    function __construct(Conf $conf, $cj) {
895        parent::__construct($conf, $cj);
896    }
897    function sort_name($score_sort) {
898        $score_sort = ListSorter::canonical_long_score_sort($score_sort);
899        return $this->name . ($score_sort ? " $score_sort" : "");
900    }
901    function prepare(PaperList $pl, $visible) {
902        $this->contact = $pl->reviewer_user();
903        $this->not_me = $this->contact->contactId !== $pl->user->contactId;
904        if ($visible && $this->not_me
905            && (!$pl->user->privChair || $pl->conf->has_any_manager()))
906            $pl->qopts["reviewSignatures"] = true;
907    }
908    function score_values(PaperList $pl, PaperInfo $row) {
909        return null;
910    }
911    protected function set_sort_fields(PaperList $pl, PaperInfo $row, ListSorter $sorter) {
912        $k = $sorter->uid;
913        $avgk = $k . "avg";
914        $s = $this->score_values($pl, $row);
915        if ($s !== null) {
916            $scoreinfo = new ScoreInfo($s, true);
917            $cid = $this->contact->contactId;
918            if ($this->not_me
919                && !$row->can_view_review_identity_of($cid, $pl->user))
920                $cid = 0;
921            $row->$k = $scoreinfo->sort_data($sorter->score, $cid);
922            $row->$avgk = $scoreinfo->mean();
923        } else
924            $row->$k = $row->$avgk = null;
925    }
926    function analyze_sort(PaperList $pl, &$rows, ListSorter $sorter) {
927        foreach ($rows as $row)
928            self::set_sort_fields($pl, $row, $sorter);
929    }
930    function compare(PaperInfo $a, PaperInfo $b, ListSorter $sorter) {
931        $k = $sorter->uid;
932        if (!($x = ScoreInfo::compare($b->$k, $a->$k, -1))) {
933            $k .= "avg";
934            $x = ScoreInfo::compare($b->$k, $a->$k);
935        }
936        return $x;
937    }
938    function content(PaperList $pl, PaperInfo $row) {
939        $values = $this->score_values($pl, $row);
940        if (empty($values))
941            return "";
942        $pl->need_render = true;
943        $cid = $this->contact->contactId;
944        if ($this->not_me && !$row->can_view_review_identity_of($cid, $pl->user))
945            $cid = 0;
946        return $this->format_field->unparse_graph($values, 1, get($values, $cid));
947    }
948    function text(PaperList $pl, PaperInfo $row) {
949        $values = array_map([$this->format_field, "unparse_value"],
950            $this->score_values($pl, $row));
951        return join(" ", $values);
952    }
953}
954
955class Score_PaperColumn extends ScoreGraph_PaperColumn {
956    public $score;
957    function __construct(Conf $conf, $cj) {
958        parent::__construct($conf, $cj);
959        $this->override = PaperColumn::OVERRIDE_FOLD_IFEMPTY;
960        $this->format_field = $conf->review_field($cj->review_field_id);
961        $this->score = $this->format_field->id;
962    }
963    function prepare(PaperList $pl, $visible) {
964        $bound = $pl->user->permissive_view_score_bound($pl->search->limit_author());
965        if ($this->format_field->view_score <= $bound)
966            return false;
967        if ($visible)
968            $pl->qopts["scores"][$this->score] = true;
969        parent::prepare($pl, $visible);
970        return true;
971    }
972    function score_values(PaperList $pl, PaperInfo $row) {
973        $fid = $this->format_field->id;
974        $row->ensure_review_score($this->format_field);
975        $scores = [];
976        foreach ($row->viewable_submitted_reviews_by_user($pl->user) as $rrow)
977            if (isset($rrow->$fid) && $rrow->$fid)
978                $scores[$rrow->contactId] = $rrow->$fid;
979        return $scores;
980    }
981    function header(PaperList $pl, $is_text) {
982        return $is_text ? $this->format_field->search_keyword() : $this->format_field->web_abbreviation();
983    }
984    function content_empty(PaperList $pl, PaperInfo $row) {
985        // Do not use score_values to determine content emptiness, since
986        // that would load the scores from the DB -- even for folded score
987        // columns.
988        return !$row->may_have_viewable_scores($this->format_field, $pl->user);
989    }
990}
991
992class Score_PaperColumnFactory {
993    static function xt_user_visible_fields($name, Conf $conf = null) {
994        if ($name === "scores") {
995            $fs = $conf->all_review_fields();
996            $conf->xt_factory_mark_matched();
997        } else
998            $fs = [$conf->find_review_field($name)];
999        $vsbound = $conf->xt_user->permissive_view_score_bound();
1000        return array_filter($fs, function ($f) use ($vsbound) {
1001            return $f && $f->has_options && $f->displayed && $f->view_score > $vsbound;
1002        });
1003    }
1004    static function expand($name, Conf $conf, $xfj, $m) {
1005        return array_map(function ($f) use ($xfj) {
1006            $cj = (array) $xfj;
1007            $cj["name"] = $f->search_keyword();
1008            $cj["review_field_id"] = $f->id;
1009            return (object) $cj;
1010        }, self::xt_user_visible_fields($name, $conf));
1011    }
1012    static function completions(Contact $user, $fxt) {
1013        if (!$user->can_view_some_review())
1014            return [];
1015        $vsbound = $user->permissive_view_score_bound();
1016        $cs = array_map(function ($f) {
1017            return $f->search_keyword();
1018        }, array_filter($user->conf->all_review_fields(), function ($f) use ($vsbound) {
1019            return $f->has_options && $f->displayed && $f->view_score > $vsbound;
1020        }));
1021        if (!empty($cs))
1022            array_unshift($cs, "scores");
1023        return $cs;
1024    }
1025}
1026
1027class NumericOrderPaperColumn extends PaperColumn {
1028    private $order;
1029    function __construct(Conf $conf, $order) {
1030        parent::__construct($conf, ["name" => "numericorder", "sort" => true]);
1031        $this->order = $order;
1032    }
1033    function compare(PaperInfo $a, PaperInfo $b, ListSorter $sorter) {
1034        return +get($this->order, $a->paperId) - +get($this->order, $b->paperId);
1035    }
1036}
1037