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 "<" . htmlspecialchars($this->name) . ">"; 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"># ' . $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(' ', $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