1<?php 2// paperlist.php -- HotCRP helper class for producing paper lists 3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE. 4 5class PaperListTableRender { 6 public $table_start; 7 public $thead; 8 public $tbody_class; 9 public $body_rows; 10 public $tfoot; 11 public $table_end; 12 public $error; 13 14 public $ncol; 15 public $titlecol; 16 17 public $colorindex = 0; 18 public $hascolors = false; 19 public $skipcallout; 20 public $last_trclass = ""; 21 public $groupstart = [0]; 22 23 function __construct($ncol, $titlecol, $skipcallout) { 24 $this->ncol = $ncol; 25 $this->titlecol = $titlecol; 26 $this->skipcallout = $skipcallout; 27 } 28 static function make_error($error) { 29 $tr = new PaperListTableRender(0, 0, 0); 30 $tr->error = $error; 31 return $tr; 32 } 33 function tbody_start() { 34 return " <tbody class=\"{$this->tbody_class}\">\n"; 35 } 36 function heading_row($heading, $attr = []) { 37 if (!$heading) { 38 return " <tr class=\"plheading\"><td class=\"plheading-blank\" colspan=\"{$this->ncol}\"></td></tr>\n"; 39 } else { 40 $x = " <tr class=\"plheading\""; 41 foreach ($attr as $k => $v) 42 if ($k !== "no_titlecol" && $k !== "tdclass") 43 $x .= " $k=\"" . str_replace("\"", """, $v) . "\""; 44 $x .= ">"; 45 $titlecol = get($attr, "no_titlecol") ? 0 : $this->titlecol; 46 if ($titlecol) 47 $x .= "<td class=\"plheading-spacer\" colspan=\"{$titlecol}\"></td>"; 48 $tdclass = get($attr, "tdclass"); 49 $x .= "<td class=\"plheading" . ($tdclass ? " $tdclass" : "") . "\" colspan=\"" . ($this->ncol - $titlecol) . "\">"; 50 return $x . $heading . "</td></tr>\n"; 51 } 52 } 53 function heading_separator_row() { 54 return " <tr class=\"plheading\"><td class=\"plheading-separator\" colspan=\"{$this->ncol}\"></td></tr>\n"; 55 } 56} 57 58class PaperListReviewAnalysis { 59 private $prow; 60 public $rrow = null; 61 public $round = ""; 62 function __construct($rrow, PaperInfo $prow) { 63 $this->prow = $prow; 64 if ($rrow->reviewId) { 65 $this->rrow = $rrow; 66 if ($rrow->reviewRound) 67 $this->round = htmlspecialchars($prow->conf->round_name($rrow->reviewRound)); 68 } 69 } 70 function icon_html($includeLink) { 71 $rrow = $this->rrow; 72 if (($title = get(ReviewForm::$revtype_names, $rrow->reviewType))) 73 $title .= " review"; 74 else 75 $title = "Review"; 76 if (!$rrow->reviewSubmitted) 77 $title .= " (" . $this->description_text() . ")"; 78 $t = review_type_icon($rrow->reviewType, !$rrow->reviewSubmitted, $title); 79 if ($includeLink) 80 $t = $this->wrap_link($t); 81 if ($this->round) 82 $t .= '<span class="revround" title="Review round"> ' . $this->round . "</span>"; 83 return $t; 84 } 85 function icon_text() { 86 $x = ""; 87 if ($this->rrow->reviewType) 88 $x = get_s(ReviewForm::$revtype_names, $this->rrow->reviewType); 89 if ($x !== "" && $this->round) 90 $x .= ":" . $this->round; 91 return $x; 92 } 93 function description_text() { 94 if (!$this->rrow) 95 return ""; 96 else if ($this->rrow->reviewSubmitted) 97 return "complete"; 98 else if ($this->rrow->reviewType == REVIEW_SECONDARY 99 && $this->rrow->reviewNeedsSubmit <= 0) 100 return "delegated"; 101 else if ($this->rrow->reviewType == REVIEW_EXTERNAL 102 && $this->rrow->timeApprovalRequested) 103 return "awaiting approval"; 104 else if ($this->rrow->reviewModified > 1) 105 return "in progress"; 106 else if ($this->rrow->reviewModified > 0) 107 return "accepted"; 108 else 109 return "not started"; 110 } 111 function wrap_link($html, $klass = null) { 112 if (!$this->rrow) 113 return $html; 114 if (!$this->rrow->reviewSubmitted) 115 $href = $this->prow->conf->hoturl("review", "r=" . unparseReviewOrdinal($this->rrow)); 116 else 117 $href = $this->prow->conf->hoturl("paper", "p=" . $this->rrow->paperId . "#r" . unparseReviewOrdinal($this->rrow)); 118 $t = $klass ? "<a class=\"$klass\"" : "<a"; 119 return $t . ' href="' . $href . '">' . $html . '</a>'; 120 } 121} 122 123class PaperList { 124 public $conf; 125 public $user; 126 public $qreq; 127 public $search; 128 private $_reviewer_user; 129 130 private $sortable; 131 private $foldable; 132 private $_unfold_all = false; 133 private $_paper_link_page; 134 private $_paper_link_mode; 135 private $_view_columns = false; 136 private $_view_compact_columns = false; 137 private $_view_row_numbers = false; 138 private $_view_statistics = false; 139 private $_view_force = false; 140 private $_view_fields = []; 141 private $atab; 142 143 private $_table_id; 144 private $_table_class; 145 private $report_id; 146 private $_row_id_pattern; 147 private $_selection; 148 149 private $_rowset; 150 private $_row_filter; 151 private $_columns_by_name; 152 private $_column_errors_by_name = []; 153 154 private $_header_script = ""; 155 private $_header_script_map = []; 156 157 // columns access 158 public $qopts; // set by PaperColumn::prepare 159 public $sorters = []; 160 public $tagger; 161 public $need_tag_attr; 162 public $table_attr; 163 public $row_attr; 164 public $row_overridable; 165 public $row_tags; 166 public $row_tags_overridable; 167 public $need_render; 168 public $has_editable_tags = false; 169 public $check_format; 170 171 // collected during render and exported to caller 172 public $count; // also exported to columns access: 1 more than row index 173 public $ids; 174 public $groups; 175 public $any; 176 private $_has; 177 public $error_html = array(); 178 179 static public $include_stash = true; 180 181 static private $stats = [ScoreInfo::SUM, ScoreInfo::MEAN, ScoreInfo::MEDIAN, ScoreInfo::STDDEV_P]; 182 183 function __construct(PaperSearch $search, $args = array(), $qreq = null) { 184 $this->search = $search; 185 $this->conf = $this->search->conf; 186 $this->user = $this->search->user; 187 if (!$qreq || !($qreq instanceof Qrequest)) 188 $qreq = new Qrequest("GET", $qreq); 189 $this->qreq = $qreq; 190 $this->_reviewer_user = $search->reviewer_user(); 191 192 $this->sortable = isset($args["sort"]) && $args["sort"]; 193 $this->foldable = $this->sortable || !!get($args, "foldable") 194 || $this->user->is_manager() /* “Override conflicts” fold */; 195 196 $this->_paper_link_page = ""; 197 if ($qreq->linkto === "paper" || $qreq->linkto === "review" || $qreq->linkto === "assign") 198 $this->_paper_link_page = $qreq->linkto; 199 else if ($qreq->linkto === "paperedit") { 200 $this->_paper_link_page = "paper"; 201 $this->_paper_link_mode = "edit"; 202 } 203 $this->atab = $qreq->atab; 204 205 $this->tagger = new Tagger($this->user); 206 207 $this->qopts = $this->search->simple_search_options(); 208 if ($this->qopts === false) 209 $this->qopts = ["paperId" => $this->search->paper_ids()]; 210 $this->qopts["scores"] = []; 211 // NB that actually processed the search, setting PaperSearch::viewmap 212 213 foreach ($this->search->sorters ? : [] as $sorter) 214 ListSorter::push($this->sorters, $sorter); 215 if ($this->sortable && is_string($args["sort"])) 216 array_unshift($this->sorters, PaperSearch::parse_sorter($args["sort"])); 217 else if ($this->sortable && $qreq->sort) 218 array_unshift($this->sorters, PaperSearch::parse_sorter($qreq->sort)); 219 220 if (($report = get($args, "report"))) { 221 $display = null; 222 if (!get($args, "no_session_display")) 223 $display = $this->conf->session("{$report}display", null); 224 if ($display === null) 225 $display = $this->conf->setting_data("{$report}display_default", null); 226 if ($display === null && $report === "pl") 227 $display = $this->conf->review_form()->default_display(); 228 $this->set_view_display($display); 229 } 230 if (is_string(get($args, "display"))) 231 $this->set_view_display($args["display"]); 232 foreach ($this->search->viewmap ? : [] as $k => $v) 233 $this->set_view($k, $v); 234 if ($this->conf->submission_blindness() != Conf::BLIND_OPTIONAL 235 && get($this->_view_fields, "au") 236 && get($this->_view_fields, "anonau") === null) 237 $this->_view_fields["anonau"] = true; 238 239 $this->_columns_by_name = ["anonau" => [], "aufull" => [], "rownum" => [], "statistics" => []]; 240 241 $this->_rowset = get($args, "rowset"); 242 } 243 244 function __get($name) { 245 error_log("PaperList::$name " . json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS))); 246 return $name === "contact" ? $this->user : null; 247 } 248 249 function table_id() { 250 return $this->_table_id; 251 } 252 function set_table_id_class($table_id, $table_class, $row_id_pattern = null) { 253 $this->_table_id = $table_id; 254 $this->_table_class = $table_class; 255 $this->_row_id_pattern = $row_id_pattern; 256 } 257 258 function report_id() { 259 return $this->report_id; 260 } 261 function set_report($report) { 262 $this->report_id = $report; 263 } 264 265 function set_row_filter($filter) { 266 $this->_row_filter = $filter; 267 } 268 269 function add_column($name, PaperColumn $col) { 270 $this->_columns_by_name[$name][] = $col; 271 } 272 273 function set_view($k, $v) { 274 if ($k !== "" && $k[0] === "\"" && $k[strlen($k) - 1] === "\"") 275 $k = substr($k, 1, -1); 276 if (in_array($k, ["compact", "cc", "compactcolumn", "ccol", "compactcolumns"])) 277 $this->_view_compact_columns = $this->_view_columns = $v; 278 else if (in_array($k, ["columns", "column", "col"])) 279 $this->_view_columns = $v; 280 else if ($k === "force") 281 $this->_view_force = $v; 282 else if (in_array($k, ["statistics", "stat", "stats", "totals"])) 283 $this->_view_statistics = $v; 284 else if (in_array($k, ["rownum", "rownumbers"])) 285 $this->_view_row_numbers = $v; 286 else { 287 if ($k === "authors") 288 $k = "au"; 289 if ($v && in_array($k, ["aufull", "anonau"]) && !isset($this->_view_fields["au"])) 290 $this->_view_fields["au"] = $v; 291 $this->_view_fields[$k] = $v; 292 } 293 } 294 function set_view_display($str) { 295 $has_sorters = !!array_filter($this->sorters, function ($s) { 296 return $s->thenmap === null; 297 }); 298 $splitter = new SearchSplitter($str); 299 while (($w = $splitter->shift()) !== "") { 300 if (($colon = strpos($w, ":")) !== false) { 301 $action = substr($w, 0, $colon); 302 $w = substr($w, $colon + 1); 303 } else 304 $action = "show"; 305 if ($action === "sort") { 306 if (!$has_sorters && $w !== "sort:id") 307 ListSorter::push($this->sorters, PaperSearch::parse_sorter($w)); 308 } else if ($action === "edit") 309 $this->set_view($w, "edit"); 310 else 311 $this->set_view($w, $action !== "hide"); 312 } 313 } 314 315 function set_selection(SearchSelection $ssel) { 316 $this->_selection = $ssel; 317 } 318 function is_selected($paperId, $default = false) { 319 return $this->_selection ? $this->_selection->is_selected($paperId) : $default; 320 } 321 322 function unfold_all() { 323 $this->_unfold_all = true; 324 } 325 326 function mark_has($key, $value = true) { 327 if ($value) 328 $this->_has[$key] = true; 329 else if (!isset($this->_has[$key])) 330 $this->_has[$key] = false; 331 } 332 function has($key) { 333 if (!isset($this->_has[$key])) 334 $this->_has[$key] = $this->_compute_has($key); 335 return $this->_has[$key]; 336 } 337 private function _compute_has($key) { 338 // paper options 339 if ($key === "paper" || $key === "final") { 340 $opt = $this->conf->paper_opts->find($key); 341 return $this->user->can_view_some_paper_option($opt) 342 && $this->_rowset->any(function ($row) use ($opt) { 343 return ($opt->id == DTYPE_SUBMISSION ? $row->paperStorageId : $row->finalPaperStorageId) > 1 344 && $this->user->can_view_paper_option($row, $opt); 345 }); 346 } 347 if (str_starts_with($key, "opt") 348 && ($opt = $this->conf->paper_opts->find($key))) { 349 return $this->user->can_view_some_paper_option($opt) 350 && $this->_rowset->any(function ($row) use ($opt) { 351 return ($ov = $row->option($opt->id)) 352 && (!$opt->has_document() || $ov->value > 1) 353 && $this->user->can_view_paper_option($row, $opt); 354 }); 355 } 356 // other features 357 if ($key === "abstract") 358 return $this->_rowset->any(function ($row) { 359 return (string) $row->abstract !== ""; 360 }); 361 if ($key === "openau") 362 return $this->has("authors") 363 && (!$this->user->is_manager() 364 || $this->_rowset->any(function ($row) { 365 return $this->user->can_view_authors($row); 366 })); 367 if ($key === "anonau") 368 return $this->has("authors") 369 && $this->user->is_manager() 370 && $this->_rowset->any(function ($row) { 371 return $this->user->allow_view_authors($row) 372 && !$this->user->can_view_authors($row); 373 }); 374 if ($key === "need_submit") 375 return $this->_rowset->any(function ($row) { 376 return $row->timeSubmitted <= 0 && $row->timeWithdrawn <= 0; 377 }); 378 if ($key === "accepted") 379 return $this->_rowset->any(function ($row) { 380 return $row->outcome > 0 && $this->user->can_view_decision($row); 381 }); 382 if ($key === "need_final") 383 return $this->has("accepted") 384 && $this->_rowset->any(function ($row) { 385 return $row->outcome > 0 386 && $this->user->can_view_decision($row) 387 && $row->timeFinalSubmitted <= 0; 388 }); 389 if (!in_array($key, ["collab", "lead", "shepherd", "topics", "sel", "need_review", "authors", "tags"])) 390 error_log("unexpected PaperList::_compute_has({$key})"); 391 return false; 392 } 393 394 395 private function find_columns($name) { 396 if (!array_key_exists($name, $this->_columns_by_name)) { 397 $fs = $this->conf->paper_columns($name, $this->user); 398 if (!$fs) { 399 $errors = $this->conf->xt_factory_errors(); 400 if (empty($errors)) { 401 if ($this->conf->paper_columns($name, $this->conf->site_contact())) 402 $errors[] = "Permission error."; 403 else 404 $errors[] = "No such column."; 405 } 406 $this->_column_errors_by_name[$name] = $errors; 407 } 408 $nfs = []; 409 foreach ($fs as $fdef) { 410 if ($fdef->name === $name) 411 $nfs[] = PaperColumn::make($this->conf, $fdef); 412 else { 413 if (!array_key_exists($fdef->name, $this->_columns_by_name)) 414 $this->_columns_by_name[$fdef->name][] = PaperColumn::make($this->conf, $fdef); 415 $nfs = array_merge($nfs, $this->_columns_by_name[$fdef->name]); 416 } 417 } 418 $this->_columns_by_name[$name] = $nfs; 419 } 420 return $this->_columns_by_name[$name]; 421 } 422 private function find_column($name) { 423 return get($this->find_columns($name), 0); 424 } 425 426 function _sort_compare($a, $b) { 427 foreach ($this->sorters as $s) { 428 if (($x = $s->field->compare($a, $b, $s))) { 429 return $x < 0 === $s->reverse ? 1 : -1; 430 } 431 } 432 if ($a->paperId != $b->paperId) { 433 return $a->paperId < $b->paperId ? -1 : 1; 434 } else { 435 return 0; 436 } 437 } 438 function _then_sort_compare($a, $b) { 439 if (($x = $a->_then_sort_info - $b->_then_sort_info)) { 440 return $x < 0 ? -1 : 1; 441 } 442 foreach ($this->sorters as $s) { 443 if (($s->thenmap === null || $s->thenmap === $a->_then_sort_info) 444 && ($x = $s->field->compare($a, $b, $s))) { 445 return $x < 0 === $s->reverse ? 1 : -1; 446 } 447 } 448 if ($a->paperId != $b->paperId) { 449 return $a->paperId < $b->paperId ? -1 : 1; 450 } else { 451 return 0; 452 } 453 } 454 private function _sort($rows) { 455 $overrides = $this->user->add_overrides($this->_view_force ? Contact::OVERRIDE_CONFLICT : 0); 456 457 if (($thenmap = $this->search->thenmap)) { 458 foreach ($rows as $row) 459 $row->_then_sort_info = $thenmap[$row->paperId]; 460 } 461 foreach ($this->sorters as $s) { 462 $s->assign_uid(); 463 $s->list = $this; 464 } 465 foreach ($this->sorters as $s) { 466 $s->field->analyze_sort($this, $rows, $s); 467 } 468 469 usort($rows, [$this, $thenmap ? "_then_sort_compare" : "_sort_compare"]); 470 471 foreach ($this->sorters as $s) 472 $s->list = null; // break circular ref 473 $this->user->set_overrides($overrides); 474 return $rows; 475 } 476 477 function sortdef($always = false) { 478 if (!empty($this->sorters) 479 && $this->sorters[0]->type 480 && $this->sorters[0]->thenmap === null 481 && ($always || (string) $this->qreq->sort != "") 482 && ($this->sorters[0]->type != "id" || $this->sorters[0]->reverse)) { 483 $x = ($this->sorters[0]->reverse ? "r" : ""); 484 if (($fdef = $this->find_column($this->sorters[0]->type)) 485 && isset($fdef->score)) 486 $x .= $this->sorters[0]->score; 487 return ($fdef ? $fdef->name : $this->sorters[0]->type) 488 . ($x ? ",$x" : ""); 489 } else 490 return ""; 491 } 492 493 494 function _contentDownload($row) { 495 if ($row->size == 0 || !$this->user->can_view_pdf($row)) 496 return ""; 497 $dtype = $row->finalPaperStorageId <= 0 ? DTYPE_SUBMISSION : DTYPE_FINAL; 498 return " " . $row->document($dtype)->link_html("", DocumentInfo::L_SMALL | DocumentInfo::L_NOSIZE | DocumentInfo::L_FINALTITLE); 499 } 500 501 function _paperLink(PaperInfo $row) { 502 $pt = $this->_paper_link_page ? : "paper"; 503 $rrow = null; 504 if ($pt === "review" || $pt === "finishreview") { 505 $rrow = $row->review_of_user($this->user); 506 if (!$rrow || ($pt === "finishreview" && !$rrow->reviewNeedsSubmit)) 507 $pt = "paper"; 508 else 509 $pt = "review"; 510 } 511 $pl = "p=" . $row->paperId; 512 if ($pt === "paper" && $this->_paper_link_mode) 513 $pl .= "&m=" . $this->_paper_link_mode; 514 else if ($pt === "review") { 515 $pl .= "&r=" . unparseReviewOrdinal($rrow); 516 if ($rrow->reviewSubmitted > 0) 517 $pl .= "&m=r"; 518 } 519 return $row->conf->hoturl($pt, $pl); 520 } 521 522 // content downloaders 523 static function wrapChairConflict($text) { 524 return '<span class="fn5"><em>Hidden for conflict</em> <span class="barsep">·</span> <a class="fn5" href="#">Override conflicts</a></span><span class="fx5">' . $text . "</span>"; 525 } 526 527 function reviewer_user() { 528 return $this->_reviewer_user; 529 } 530 function set_reviewer_user(Contact $user) { 531 $this->_reviewer_user = $user; 532 } 533 534 function _content_pc($contactId) { 535 $pc = $this->conf->pc_member_by_id($contactId); 536 return $pc ? $this->user->reviewer_html_for($pc) : ""; 537 } 538 539 function _text_pc($contactId) { 540 $pc = $this->conf->pc_member_by_id($contactId); 541 return $pc ? $this->user->reviewer_text_for($pc) : ""; 542 } 543 544 function _compare_pc($contactId1, $contactId2) { 545 $pc1 = $this->conf->pc_member_by_id($contactId1); 546 $pc2 = $this->conf->pc_member_by_id($contactId2); 547 if ($pc1 === $pc2) 548 return 0; 549 else if (!$pc1 || !$pc2) 550 return $pc1 ? -1 : 1; 551 else 552 return Contact::compare($pc1, $pc2); 553 } 554 555 function displayable_list_actions($prefix) { 556 $la = []; 557 foreach ($this->conf->list_action_map() as $name => $fjs) 558 if (str_starts_with($name, $prefix)) { 559 $uf = null; 560 foreach ($fjs as $fj) 561 if (Conf::xt_priority_compare($fj, $uf) <= 0 562 && $this->conf->xt_allowed($fj, $this->user) 563 && $this->action_xt_displayed($fj)) 564 $uf = $fj; 565 if ($uf) 566 $la[$name] = $uf; 567 } 568 return $la; 569 } 570 571 function action_xt_displayed($fj) { 572 if (isset($fj->display_if_report) 573 && (str_starts_with($fj->display_if_report, "!") 574 ? $this->report_id === substr($fj->display_if_report, 1) 575 : $this->report_id !== $fj->display_if_report)) 576 return false; 577 if (isset($fj->display_if) 578 && !$this->conf->xt_check($fj->display_if, $fj, $this->user)) 579 return false; 580 if (isset($fj->display_if_list_has)) { 581 $ifl = $fj->display_if_list_has; 582 foreach (is_array($ifl) ? $ifl : [$ifl] as $h) { 583 if (!is_bool($h)) { 584 if (str_starts_with($h, "!")) 585 $h = !$this->has(substr($h, 1)); 586 else 587 $h = $this->has($h); 588 } 589 if (!$h) 590 return false; 591 } 592 } 593 if (isset($fj->disabled) && $fj->disabled) 594 return false; 595 return true; 596 } 597 598 static function render_footer_row($arrow_ncol, $ncol, $header, 599 $lllgroups, $activegroup = -1, $extra = null) { 600 $foot = "<tr class=\"pl_footrow\">\n "; 601 if ($arrow_ncol) { 602 $foot .= '<td class="plf pl_footselector" colspan="' . $arrow_ncol . '">' 603 . Icons::ui_upperleft() . "</td>\n "; 604 } 605 $foot .= '<td id="plact" class="plf pl_footer linelinks" colspan="' . $ncol . '">'; 606 607 if ($header) { 608 $foot .= "<table class=\"pl_footerpart\"><tbody><tr>\n" 609 . ' <td class="pl_footer_desc">' . $header . "</td>\n" 610 . ' </tr></tbody></table>'; 611 } 612 613 foreach ($lllgroups as $i => $lllg) { 614 $attr = ["class" => "linelink pl_footerpart"]; 615 if ($i === $activegroup) 616 $attr["class"] .= " active"; 617 for ($j = 2; $j < count($lllg); ++$j) { 618 if (is_array($lllg[$j])) { 619 foreach ($lllg[$j] as $k => $v) 620 if (str_starts_with($k, "linelink-")) { 621 $k = substr($k, 9); 622 if ($k === "class") 623 $attr["class"] .= " " . $v; 624 else 625 $attr[$k] = $v; 626 } 627 } 628 } 629 $foot .= "<table"; 630 foreach ($attr as $k => $v) 631 $foot .= " $k=\"" . htmlspecialchars($v) . "\""; 632 $foot .= "><tbody><tr>\n" 633 . " <td class=\"pl_footer_desc lll\"><a class=\"ui tla\" href=\"" 634 . $lllg[0] . "\">" . $lllg[1] . "</a></td>\n"; 635 for ($j = 2; $j < count($lllg); ++$j) { 636 $cell = is_array($lllg[$j]) ? $lllg[$j] : ["content" => $lllg[$j]]; 637 $attr = []; 638 foreach ($cell as $k => $v) { 639 if ($k !== "content" && !str_starts_with($k, "linelink-")) 640 $attr[$k] = $v; 641 } 642 if ($attr || isset($cell["content"])) { 643 $attr["class"] = rtrim("lld " . get($attr, "class", "")); 644 $foot .= " <td"; 645 foreach ($attr as $k => $v) 646 $foot .= " $k=\"" . htmlspecialchars($v) . "\""; 647 $foot .= ">"; 648 if ($j === 2 && isset($cell["content"]) && !str_starts_with($cell["content"], "<b>")) 649 $foot .= "<b>: </b> "; 650 if (isset($cell["content"])) 651 $foot .= $cell["content"]; 652 $foot .= "</td>\n"; 653 } 654 } 655 if ($i < count($lllgroups) - 1) 656 $foot .= " <td> <span class='barsep'>·</span> </td>\n"; 657 $foot .= " </tr></tbody></table>"; 658 } 659 return $foot . (string) $extra . "<hr class=\"c\" /></td>\n </tr>"; 660 } 661 662 private function _footer($ncol, $extra) { 663 if ($this->count == 0) 664 return ""; 665 666 $renderers = []; 667 foreach ($this->conf->list_action_renderers() as $name => $fjs) { 668 $rf = null; 669 foreach ($fjs as $fj) 670 if (Conf::xt_priority_compare($fj, $rf) <= 0 671 && $this->conf->xt_allowed($fj, $this->user) 672 && $this->action_xt_displayed($fj)) 673 $rf = $fj; 674 if ($rf) { 675 Conf::xt_resolve_require($rf); 676 $renderers[] = $rf; 677 } 678 } 679 usort($renderers, "Conf::xt_position_compare"); 680 681 $lllgroups = []; 682 $whichlll = -1; 683 foreach ($renderers as $rf) { 684 if (($lllg = call_user_func($rf->render_callback, $this, $rf))) { 685 if (is_string($lllg)) 686 $lllg = [$lllg]; 687 array_unshift($lllg, $rf->name, $rf->title); 688 $lllg[0] = SelfHref::make($this->qreq, ["atab" => $lllg[0], "anchor" => "plact"]); 689 $lllgroups[] = $lllg; 690 if ($this->qreq->fn == $rf->name || $this->atab == $rf->name) 691 $whichlll = count($lllgroups) - 1; 692 } 693 } 694 695 $footsel_ncol = $this->_view_columns ? 0 : 1; 696 return self::render_footer_row($footsel_ncol, $ncol - $footsel_ncol, 697 "<b>Select papers</b> (or <a class=\"ui js-select-all\" href=\"" 698 . SelfHref::make($this->qreq, ["selectall" => 1, "anchor" => "plact"]) 699 . '">select all ' . $this->count . "</a>), then ", 700 $lllgroups, $whichlll, $extra); 701 } 702 703 private function _default_linkto($page) { 704 if (!$this->_paper_link_page) 705 $this->_paper_link_page = $page; 706 } 707 708 private function _list_columns() { 709 switch ($this->report_id) { 710 case "a": 711 return "id title revstat statusfull authors collab abstract topics reviewers shepherd scores formulas"; 712 case "authorHome": 713 return "id title statusfull"; 714 case "act": 715 case "all": 716 return "sel id title revtype revstat statusfull authors collab abstract topics pcconflicts allpref reviewers tags tagreports lead shepherd scores formulas"; 717 case "reviewerHome": 718 $this->_default_linkto("finishreview"); 719 return "id title revtype status"; 720 case "ar": 721 case "r": 722 case "rable": 723 $this->_default_linkto("finishreview"); 724 /* fallthrough */ 725 case "acc": 726 case "lead": 727 case "manager": 728 case "s": 729 case "vis": 730 return "sel id title revtype revstat status authors collab abstract topics pcconflicts allpref reviewers tags tagreports lead shepherd scores formulas"; 731 case "req": 732 case "rout": 733 $this->_default_linkto("review"); 734 return "sel id title revtype revstat status authors collab abstract topics pcconflicts allpref reviewers tags tagreports lead shepherd scores formulas"; 735 case "reqrevs": 736 $this->_default_linkto("review"); 737 return "id title revdelegation revstat status authors collab abstract topics pcconflicts allpref reviewers tags tagreports lead shepherd scores formulas"; 738 case "reviewAssignment": 739 $this->_default_linkto("assign"); 740 return "id title revpref topicscore desirability assrev authors potentialconflict topics allrevtopicpref reviewers tags scores formulas"; 741 case "conflictassign": 742 $this->_default_linkto("assign"); 743 return "id title abstract authors potentialconflict revtype editconf tags"; 744 case "editpref": 745 $this->_default_linkto("paper"); 746 return "sel id title topicscore revtype editpref authors abstract topics"; 747 case "reviewers": 748 $this->_default_linkto("assign"); 749 return "selon id title status"; 750 case "reviewersSel": 751 $this->_default_linkto("assign"); 752 return "sel id title status reviewers"; 753 default: 754 error_log($this->conf->dbname . ": No such report {$this->report_id}"); 755 return null; 756 } 757 } 758 759 function _canonicalize_columns($fields) { 760 if (is_string($fields)) 761 $fields = explode(" ", $fields); 762 $field_list = array(); 763 foreach ($fields as $fid) { 764 foreach ($this->find_columns($fid) as $fdef) 765 $field_list[] = $fdef; 766 } 767 if ($this->qreq->selectall > 0 && $field_list[0]->name == "sel") 768 $field_list[0] = $this->find_column("selon"); 769 return $field_list; 770 } 771 772 773 function rowset() { 774 if ($this->_rowset === null) { 775 $this->qopts["scores"] = array_keys($this->qopts["scores"]); 776 if (empty($this->qopts["scores"])) 777 unset($this->qopts["scores"]); 778 $result = $this->conf->paper_result($this->user, $this->qopts); 779 $this->_rowset = new PaperInfoSet; 780 while (($row = PaperInfo::fetch($result, $this->user))) { 781 assert(!$this->_rowset->get($row->paperId)); 782 $this->_rowset->add($row); 783 } 784 Dbl::free($result); 785 } 786 return $this->_rowset; 787 } 788 789 private function _rows($field_list) { 790 if (!$field_list) 791 return null; 792 793 // analyze rows (usually noop) 794 $rows = $this->rowset()->all(); 795 foreach ($field_list as $fdef) 796 $fdef->analyze($this, $rows, $field_list); 797 798 // sort rows 799 if (!empty($this->sorters)) 800 $rows = $this->_sort($rows); 801 802 // set `ids` 803 $this->ids = []; 804 foreach ($rows as $prow) 805 $this->ids[] = $prow->paperId; 806 807 // set `groups` 808 $this->groups = []; 809 if (!empty($this->search->groupmap)) 810 $this->_collect_groups($rows); 811 812 return $rows; 813 } 814 815 private function _collect_groups($srows) { 816 $groupmap = $this->search->groupmap ? : []; 817 $thenmap = $this->search->thenmap ? : []; 818 $rowpos = 0; 819 for ($grouppos = 0; 820 $rowpos < count($srows) || $grouppos < count($groupmap); 821 ++$grouppos) { 822 $first_rowpos = $rowpos; 823 while ($rowpos < count($srows) 824 && get_i($thenmap, $srows[$rowpos]->paperId) === $grouppos) 825 ++$rowpos; 826 $ginfo = get($groupmap, $grouppos); 827 if (($ginfo === null || $ginfo->is_empty()) && $first_rowpos === 0) 828 continue; 829 $ginfo = $ginfo ? clone $ginfo : TagInfo::make_empty(); 830 $ginfo->pos = $first_rowpos; 831 $ginfo->count = $rowpos - $first_rowpos; 832 // leave off an empty “Untagged” section unless editing 833 if ($ginfo->count === 0 && $ginfo->tag && !$ginfo->annoId 834 && !$this->has_editable_tags) 835 continue; 836 $this->groups[] = $ginfo; 837 } 838 } 839 840 function is_folded($fdef) { 841 $fname = $fdef; 842 if (is_object($fdef) || ($fdef = $this->find_column($fname))) 843 $fname = $fdef->fold ? $fdef->name : null; 844 else if ($fname === "force") 845 return !$this->_view_force; 846 else if ($fname === "rownum") 847 return !$this->_view_row_numbers; 848 else if ($fname === "statistics") 849 return !$this->_view_statistics; 850 if ($fname === "authors") 851 $fname = "au"; 852 if (!$fname || $this->_unfold_all || $this->qreq["show$fname"]) 853 return false; 854 return !get($this->_view_fields, $fname); 855 } 856 857 private function _wrap_conflict($main_content, $override_content, PaperColumn $fdef) { 858 if ($main_content === $override_content) 859 return $main_content; 860 $tag = $fdef->viewable_row() ? "div" : "span"; 861 if ((string) $main_content !== "") 862 $main_content = "<$tag class=\"fn5\">$main_content</$tag>"; 863 if ((string) $override_content !== "") 864 $override_content = "<$tag class=\"fx5\">$override_content</$tag>"; 865 return $main_content . $override_content; 866 } 867 868 private function _row_field_content(PaperColumn $fdef, PaperInfo $row) { 869 $content = ""; 870 if ($this->row_overridable && $fdef->override === PaperColumn::OVERRIDE_FOLD_IFEMPTY) { 871 $empty = $fdef->content_empty($this, $row); 872 if ($empty) { 873 $overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT); 874 $empty = $fdef->content_empty($this, $row); 875 if (!$empty && $fdef->is_visible) 876 $content = $this->_wrap_conflict("", $fdef->content($this, $row), $fdef); 877 $this->user->set_overrides($overrides); 878 } else if ($fdef->is_visible) 879 $content = $fdef->content($this, $row); 880 } else if ($this->row_overridable && $fdef->override === PaperColumn::OVERRIDE_FOLD_BOTH) { 881 $content1 = $content2 = ""; 882 $empty1 = $fdef->content_empty($this, $row); 883 if (!$empty1 && $fdef->is_visible) 884 $content1 = $fdef->content($this, $row); 885 $overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT); 886 $empty2 = $fdef->content_empty($this, $row); 887 if (!$empty2 && $fdef->is_visible) 888 $content2 = $fdef->content($this, $row); 889 $this->user->set_overrides($overrides); 890 $empty = $empty1 && $empty2; 891 $content = $this->_wrap_conflict($content1, $content2, $fdef); 892 } else if ($this->row_overridable && $fdef->override === PaperColumn::OVERRIDE_ALWAYS) { 893 $overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT); 894 $empty = $fdef->content_empty($this, $row); 895 if (!$empty && $fdef->is_visible) 896 $content = $fdef->content($this, $row); 897 $this->user->set_overrides($overrides); 898 } else { 899 $empty = $fdef->content_empty($this, $row); 900 if (!$empty && $fdef->is_visible) 901 $content = $fdef->content($this, $row); 902 } 903 return [$empty, $content]; 904 } 905 906 private function _row_setup(PaperInfo $row) { 907 ++$this->count; 908 $this->row_attr = []; 909 $this->row_overridable = $this->user->can_meaningfully_override($row); 910 911 $this->row_tags = $this->row_tags_overridable = null; 912 if (isset($row->paperTags) && $row->paperTags !== "") { 913 if ($this->row_overridable) { 914 $overrides = $this->user->add_overrides(Contact::OVERRIDE_CONFLICT); 915 $this->row_tags_overridable = $row->viewable_tags($this->user); 916 $this->user->remove_overrides(Contact::OVERRIDE_CONFLICT); 917 $this->row_tags = $row->viewable_tags($this->user); 918 $this->user->set_overrides($overrides); 919 } else 920 $this->row_tags = $row->viewable_tags($this->user); 921 } 922 } 923 924 private function _row_content($rstate, PaperInfo $row, $fieldDef) { 925 // main columns 926 $tm = ""; 927 foreach ($fieldDef as $fdef) { 928 if (!$fdef->viewable_column() 929 || (!$fdef->is_visible && $fdef->has_content)) 930 continue; 931 list($empty, $content) = $this->_row_field_content($fdef, $row); 932 if ($fdef->is_visible) { 933 if ($content !== "") { 934 $tm .= "<td class=\"pl " . $fdef->className; 935 if ($fdef->fold) 936 $tm .= " fx{$fdef->fold}"; 937 $tm .= "\">" . $content . "</td>"; 938 } else { 939 $tm .= "<td"; 940 if ($fdef->fold) 941 $tm .= " class=\"fx{$fdef->fold}\""; 942 $tm .= "></td>"; 943 } 944 } 945 if ($fdef->is_visible ? $content !== "" : !$empty) 946 $fdef->has_content = true; 947 } 948 949 // extension columns 950 $tt = ""; 951 foreach ($fieldDef as $fdef) { 952 if (!$fdef->viewable_row() 953 || (!$fdef->is_visible && $fdef->has_content)) 954 continue; 955 list($empty, $content) = $this->_row_field_content($fdef, $row); 956 if ($fdef->is_visible) { 957 if ($content !== "" && ($ch = $fdef->header($this, false))) { 958 if ($content[0] !== "<" 959 || !preg_match('/\A((?:<(?:div|p).*?>)*)([\s\S]*)\z/', $content, $cm)) 960 $cm = [null, "", $content]; 961 $content = $cm[1] . '<em class="plx">' . $ch . ':</em> ' . $cm[2]; 962 } 963 $tt .= "<div class=\"" . $fdef->className; 964 if ($fdef->fold) 965 $tt .= " fx" . $fdef->fold; 966 $tt .= "\">" . $content . "</div>"; 967 } 968 if ($fdef->is_visible ? $content !== "" : !$empty) 969 $fdef->has_content = !$empty; 970 } 971 972 // filter 973 if ($this->_row_filter 974 && !call_user_func($this->_row_filter, $this, $row, $fieldDef, $tm, $tt)) { 975 --$this->count; 976 return ""; 977 } 978 979 // tags 980 if ($this->need_tag_attr) { 981 if ($this->row_tags_overridable 982 && $this->row_tags_overridable !== $this->row_tags) { 983 $this->row_attr["data-tags"] = trim($this->row_tags_overridable); 984 $this->row_attr["data-tags-conflicted"] = trim($this->row_tags); 985 } else 986 $this->row_attr["data-tags"] = trim($this->row_tags); 987 } 988 989 // row classes 990 $trclass = []; 991 $cc = ""; 992 if (get($row, "paperTags")) { 993 if ($this->row_tags_overridable 994 && ($cco = $row->conf->tags()->color_classes($this->row_tags_overridable))) { 995 $ccx = $row->conf->tags()->color_classes($this->row_tags); 996 if ($cco !== $ccx) { 997 $this->row_attr["data-color-classes"] = $cco; 998 $this->row_attr["data-color-classes-conflicted"] = $ccx; 999 $trclass[] = "colorconflict"; 1000 } 1001 $cc = $this->_view_force ? $cco : $ccx; 1002 $rstate->hascolors = $rstate->hascolors || str_ends_with($cco, " tagbg"); 1003 } else if ($this->row_tags) 1004 $cc = $row->conf->tags()->color_classes($this->row_tags); 1005 } 1006 if ($cc) { 1007 $trclass[] = $cc; 1008 $rstate->hascolors = $rstate->hascolors || str_ends_with($cc, " tagbg"); 1009 } 1010 if (!$cc || !$rstate->hascolors) 1011 $trclass[] = "k" . $rstate->colorindex; 1012 if (($highlightclass = get($this->search->highlightmap, $row->paperId))) 1013 $trclass[] = $highlightclass[0] . "highlightmark"; 1014 $trclass = join(" ", $trclass); 1015 $rstate->colorindex = 1 - $rstate->colorindex; 1016 $rstate->last_trclass = $trclass; 1017 1018 $t = " <tr"; 1019 if ($this->_row_id_pattern) 1020 $t .= " id=\"" . str_replace("#", $row->paperId, $this->_row_id_pattern) . "\""; 1021 $t .= " class=\"pl $trclass\" data-pid=\"$row->paperId"; 1022 foreach ($this->row_attr as $k => $v) 1023 $t .= "\" $k=\"" . htmlspecialchars($v); 1024 $t .= "\">" . $tm . "</tr>\n"; 1025 1026 if ($tt !== "" || $this->table_id()) { 1027 $t .= " <tr class=\"plx $trclass\" data-pid=\"$row->paperId\">"; 1028 if ($rstate->skipcallout > 0) 1029 $t .= "<td colspan=\"$rstate->skipcallout\"></td>"; 1030 $t .= "<td class=\"plx\" colspan=\"" . ($rstate->ncol - $rstate->skipcallout) . "\">$tt</td></tr>\n"; 1031 } 1032 1033 return $t; 1034 } 1035 1036 private function _groups_for($grouppos, $rstate, &$body, $last) { 1037 for ($did_groupstart = false; 1038 $grouppos < count($this->groups) 1039 && ($last || $this->count > $this->groups[$grouppos]->pos); 1040 ++$grouppos) { 1041 if ($this->count !== 1 && $did_groupstart === false) 1042 $rstate->groupstart[] = $did_groupstart = count($body); 1043 $ginfo = $this->groups[$grouppos]; 1044 if ($ginfo->is_empty()) { 1045 $body[] = $rstate->heading_row(null); 1046 } else { 1047 $attr = []; 1048 if ($ginfo->tag) 1049 $attr["data-anno-tag"] = $ginfo->tag; 1050 if ($ginfo->annoId) { 1051 $attr["data-anno-id"] = $ginfo->annoId; 1052 $attr["data-tags"] = "{$ginfo->tag}#{$ginfo->tagIndex}"; 1053 if (get($this->table_attr, "data-drag-tag")) 1054 $attr["tdclass"] = "need-draghandle"; 1055 } 1056 $x = "<span class=\"plheading-group"; 1057 if ($ginfo->heading !== "" 1058 && ($format = $this->conf->check_format($ginfo->annoFormat, $ginfo->heading))) { 1059 $x .= " need-format\" data-format=\"$format"; 1060 $this->need_render = true; 1061 } 1062 $x .= "\" data-title=\"" . htmlspecialchars($ginfo->heading) 1063 . "\">" . htmlspecialchars($ginfo->heading) 1064 . ($ginfo->heading !== "" ? " " : "") 1065 . "</span><span class=\"plheading-count\">" 1066 . plural($ginfo->count, "paper") . "</span>"; 1067 $body[] = $rstate->heading_row($x, $attr); 1068 $rstate->colorindex = 0; 1069 } 1070 } 1071 return $grouppos; 1072 } 1073 1074 private function _field_title($fdef) { 1075 $t = $fdef->header($this, false); 1076 if (!$fdef->viewable_column() 1077 || !$fdef->sort 1078 || !$this->sortable 1079 || !($sort_url = $this->search->url_site_relative_raw())) 1080 return $t; 1081 1082 $default_score_sort = ListSorter::default_score_sort($this->conf); 1083 $sort_name = $fdef->sort_name($default_score_sort); 1084 $sort_url = htmlspecialchars(Navigation::siteurl() . $sort_url) 1085 . (strpos($sort_url, "?") ? "&" : "?") . "sort=" . urlencode($sort_name); 1086 $s0 = get($this->sorters, 0); 1087 1088 $sort_class = "pl_sort"; 1089 if ($s0 && $s0->thenmap === null 1090 && $sort_name === $s0->field->sort_name($s0->score ? : $default_score_sort)) { 1091 $sort_class = "pl_sort pl_sorting" . ($s0->reverse ? "_rev" : "_fwd"); 1092 $sort_url .= $s0->reverse ? "" : urlencode(" reverse"); 1093 } 1094 1095 if ($this->user->overrides() & Contact::OVERRIDE_CONFLICT) 1096 $sort_url .= "&forceShow=1"; 1097 return '<a class="' . $sort_class . '" rel="nofollow" href="' . $sort_url . '">' . $t . '</a>'; 1098 } 1099 1100 private function _analyze_folds($rstate, $fieldDef) { 1101 $classes = $jscol = array(); 1102 $has_sel = false; 1103 $has_statistics = $has_loadable_statistics = false; 1104 $default_score_sort = ListSorter::default_score_sort($this->conf); 1105 foreach ($fieldDef as $fdef) { 1106 $j = ["name" => $fdef->name, 1107 "title" => $fdef->header($this, false), 1108 "position" => $fdef->position]; 1109 if ($fdef->className != "pl_" . $fdef->name) 1110 $j["className"] = $fdef->className; 1111 if ($fdef->viewable_column()) { 1112 $j["column"] = true; 1113 if ($fdef->has_statistics()) { 1114 $j["has_statistics"] = true; 1115 if ($fdef->has_content) 1116 $has_loadable_statistics = true; 1117 if ($fdef->has_content && $fdef->is_visible) 1118 $has_statistics = true; 1119 } 1120 if ($fdef->sort) 1121 $j["sort_name"] = $fdef->sort_name($default_score_sort); 1122 } 1123 if (!$fdef->is_visible) 1124 $j["missing"] = true; 1125 if ($fdef->has_content && !$fdef->is_visible) 1126 $j["loadable"] = true; 1127 if ($fdef->fold) 1128 $j["foldnum"] = $fdef->fold; 1129 $fdef->annotate_field_js($this, $j); 1130 $jscol[] = $j; 1131 if ($fdef->fold) 1132 $classes[] = "fold" . $fdef->fold . ($fdef->is_visible ? "o" : "c"); 1133 if ($fdef instanceof SelectorPaperColumn) 1134 $has_sel = true; 1135 } 1136 // authorship requires special handling 1137 if ($this->has("anonau")) 1138 $classes[] = "fold2" . ($this->is_folded("anonau") ? "c" : "o"); 1139 // row number folding 1140 if ($has_sel) 1141 $classes[] = "fold6" . ($this->_view_row_numbers ? "o" : "c"); 1142 if ($this->user->is_track_manager()) 1143 $classes[] = "fold5" . ($this->_view_force ? "o" : "c"); 1144 $classes[] = "fold7" . ($this->_view_statistics ? "o" : "c"); 1145 $classes[] = "fold8" . ($has_statistics ? "o" : "c"); 1146 if ($this->_table_id) 1147 Ht::stash_script("plinfo.initialize(\"#{$this->_table_id}\"," . json_encode_browser($jscol) . ");"); 1148 return $classes; 1149 } 1150 1151 private function _make_title_header_extra($rstate, $fieldDef, $show_links) { 1152 $titleextra = ""; 1153 if ($show_links && $this->has("authors")) { 1154 $titleextra .= '<span class="sep"></span>'; 1155 if ($this->conf->submission_blindness() == Conf::BLIND_NEVER) 1156 $titleextra .= '<a class="ui js-plinfo" href="#" data-plinfo-field="au">' 1157 . '<span class="fn1">Show authors</span><span class="fx1">Hide authors</span></a>'; 1158 else if ($this->user->is_manager() && !$this->has("openau")) 1159 $titleextra .= '<a class="ui js-plinfo" href="#" data-plinfo-field="au anonau">' 1160 . '<span class="fn1 fn2">Show authors</span><span class="fx1 fx2">Hide authors</span></a>'; 1161 else if ($this->user->is_manager() && $this->has("anonau")) 1162 $titleextra .= '<a class="ui js-plinfo fn1" href="#" data-plinfo-field="au">Show authors</a>' 1163 . '<a class="ui js-plinfo fx1 fn2" href="#" data-plinfo-field="anonau">Show all authors</a>' 1164 . '<a class="ui js-plinfo fx1 fx2" href="#" data-plinfo-field="au anonau">Hide authors</a>'; 1165 else 1166 $titleextra .= '<a class="ui js-plinfo" href="#" data-plinfo-field="au">' 1167 . '<span class="fn1">Show authors</span><span class="fx1">Hide authors</span></a>'; 1168 } 1169 if ($show_links && $this->has("tags")) { 1170 $tagfold = $this->find_column("tags")->fold; 1171 $titleextra .= '<span class="sep"></span>'; 1172 $titleextra .= '<a class="ui js-plinfo" href="#" data-plinfo-field="tags">' 1173 . '<span class="fn' . $tagfold . '">Show tags</span><span class="fx' . $tagfold . '">Hide tags</span></a>'; 1174 } 1175 if ($titleextra) 1176 $titleextra = '<span class="pl_titleextra">' . $titleextra . '</span>'; 1177 return $titleextra; 1178 } 1179 1180 private function _column_split($rstate, $colhead, &$body) { 1181 if (count($rstate->groupstart) <= 1) 1182 return false; 1183 $rstate->groupstart[] = count($body); 1184 $rstate->split_ncol = count($rstate->groupstart) - 1; 1185 1186 $rownum_marker = "<span class=\"pl_rownum fx6\">"; 1187 $rownum_len = strlen($rownum_marker); 1188 $nbody = array("<tr>"); 1189 $tbody_class = "pltable" . ($rstate->hascolors ? " pltable_colored" : ""); 1190 for ($i = 1; $i < count($rstate->groupstart); ++$i) { 1191 $nbody[] = '<td class="plsplit_col top" width="' . (100 / $rstate->split_ncol) . '%"><div class="plsplit_col"><table width="100%">'; 1192 $nbody[] = $colhead . " <tbody class=\"$tbody_class\">\n"; 1193 $number = 1; 1194 for ($j = $rstate->groupstart[$i - 1]; $j < $rstate->groupstart[$i]; ++$j) { 1195 $x = $body[$j]; 1196 if (($pos = strpos($x, $rownum_marker)) !== false) { 1197 $pos += strlen($rownum_marker); 1198 $x = substr($x, 0, $pos) . preg_replace('/\A\d+/', $number, substr($x, $pos)); 1199 ++$number; 1200 } else if (strpos($x, "<td class=\"plheading-blank") !== false) 1201 $x = ""; 1202 $nbody[] = $x; 1203 } 1204 $nbody[] = " </tbody>\n</table></div></td>\n"; 1205 } 1206 $nbody[] = "</tr>"; 1207 1208 $body = $nbody; 1209 $rstate->last_trclass = "plsplit_col"; 1210 return true; 1211 } 1212 1213 private function _prepare($report_id = null) { 1214 $this->_has = []; 1215 $this->count = 0; 1216 $this->need_render = false; 1217 $this->report_id = $this->report_id ? : $report_id; 1218 return true; 1219 } 1220 1221 private function _expand_view_column($k, $report) { 1222 if (in_array($k, ["anonau", "aufull"])) 1223 return []; 1224 $fs = $this->find_columns($k); 1225 if (!$fs) { 1226 if (!$this->search->viewmap || !isset($this->search->viewmap[$k])) { 1227 if (($rfinfo = ReviewInfo::field_info($k, $this->conf)) 1228 && ($rfield = $this->conf->review_field($rfinfo->id))) 1229 $fs = $this->find_columns($rfield->name); 1230 } else if ($report && isset($this->_column_errors_by_name[$k])) { 1231 foreach ($this->_column_errors_by_name[$k] as $i => $err) 1232 $this->error_html[] = ($i ? "" : "Can’t show “" . htmlspecialchars($k) . "”: ") . $err; 1233 } 1234 } 1235 return $fs; 1236 } 1237 1238 private function _view_columns($field_list) { 1239 // add explicitly requested columns 1240 $viewmap_add = []; 1241 foreach ($this->_view_fields as $k => $v) { 1242 $f = $this->_expand_view_column($k, !!$v); 1243 foreach ($f as $fx) { 1244 $viewmap_add[$fx->name] = $v; 1245 foreach ($field_list as $ff) 1246 if ($fx && $fx->name == $ff->name) 1247 $fx = null; 1248 if ($fx) 1249 $field_list[] = $fx; 1250 } 1251 } 1252 foreach ($viewmap_add as $k => $v) 1253 $this->_view_fields[$k] = $v; 1254 foreach ($field_list as $fi => $f) { 1255 if (get($this->_view_fields, $f->name) === "edit") 1256 $f->mark_editable(); 1257 } 1258 1259 // remove deselected columns; 1260 // in compactcolumns view, remove non-minimal columns 1261 $minimal = $this->_view_compact_columns; 1262 $field_list2 = array(); 1263 foreach ($field_list as $fdef) { 1264 $v = get($this->_view_fields, $fdef->name); 1265 if ($v 1266 || $fdef->fold 1267 || ($v !== false && (!$minimal || $fdef->minimal))) 1268 $field_list2[] = $fdef; 1269 } 1270 return $field_list2; 1271 } 1272 1273 private function _prepare_sort() { 1274 $sorters = []; 1275 foreach ($this->sorters as $sorter) { 1276 if ($sorter->field) { 1277 // already prepared (e.g., NumericOrderPaperColumn) 1278 $sorters[] = $sorter; 1279 } else if ($sorter->type 1280 && ($field = $this->find_column($sorter->type))) { 1281 if ($field->prepare($this, PaperColumn::PREP_SORT) 1282 && $field->sort) { 1283 $sorter->field = $field->realize($this); 1284 $sorter->name = $field->name; 1285 $sorters[] = $sorter; 1286 } 1287 } else if ($sorter->type) { 1288 if ($this->user->can_view_tags(null) 1289 && ($tagger = new Tagger($this->user)) 1290 && ($tag = $tagger->check($sorter->type)) 1291 && $this->conf->fetch_ivalue("select exists (select * from PaperTag where tag=?)", $tag)) 1292 $this->search->warn("Unrecognized sort “" . htmlspecialchars($sorter->type) . "”. Did you mean “sort:#" . htmlspecialchars($sorter->type) . "”?"); 1293 else 1294 $this->search->warn("Unrecognized sort “" . htmlspecialchars($sorter->type) . "”."); 1295 } 1296 } 1297 if (empty($sorters)) { 1298 $sorters[] = PaperSearch::parse_sorter("id"); 1299 $sorters[0]->field = $this->find_column("id"); 1300 } 1301 $this->sorters = $sorters; 1302 1303 // set defaults 1304 foreach ($this->sorters as $s) { 1305 if ($s->reverse === null) 1306 $s->reverse = false; 1307 if ($s->score === null) 1308 $s->score = ListSorter::default_score_sort($this->conf); 1309 } 1310 } 1311 1312 private function _prepare_columns($field_list) { 1313 $field_list2 = []; 1314 $this->need_tag_attr = false; 1315 $this->table_attr = []; 1316 foreach ($field_list as $fdef) { 1317 if ($fdef) { 1318 $fdef->is_visible = !$this->is_folded($fdef); 1319 $fdef->has_content = false; 1320 if ($fdef->prepare($this, $fdef->is_visible ? 1 : 0)) { 1321 $field_list2[] = $fdef->realize($this); 1322 } 1323 } 1324 } 1325 assert(empty($this->row_attr)); 1326 return $field_list2; 1327 } 1328 1329 function make_review_analysis($xrow, PaperInfo $row) { 1330 return new PaperListReviewAnalysis($xrow, $row); 1331 } 1332 1333 function add_header_script($script, $uniqueid = false) { 1334 if ($uniqueid) { 1335 if (isset($this->_header_script_map[$uniqueid])) 1336 return; 1337 $this->_header_script_map[$uniqueid] = true; 1338 } 1339 if ($this->_header_script !== "" 1340 && ($ch = $this->_header_script[strlen($this->_header_script) - 1]) !== "}" 1341 && $ch !== "{" && $ch !== ";") 1342 $this->_header_script .= ";"; 1343 $this->_header_script .= $script; 1344 } 1345 1346 private function _columns($field_list, $table_html) { 1347 $field_list = $this->_canonicalize_columns($field_list); 1348 if ($table_html) 1349 $field_list = $this->_view_columns($field_list); 1350 $this->_prepare_sort(); // NB before prepare_columns so columns see sorter 1351 return $this->_prepare_columns($field_list); 1352 } 1353 1354 private function _statistics_rows($rstate, $fieldDef) { 1355 $any_empty = null; 1356 foreach ($fieldDef as $fdef) 1357 if ($fdef->viewable_column() && $fdef->has_statistics()) 1358 $any_empty = $any_empty || $fdef->statistic($this, ScoreInfo::COUNT) != $this->count; 1359 if ($any_empty === null) 1360 return ""; 1361 $t = ' <tr class="pl_statheadrow fx8">'; 1362 if ($rstate->titlecol) 1363 $t .= "<td colspan=\"{$rstate->titlecol}\" class=\"plstat\"></td>"; 1364 $t .= "<td colspan=\"" . ($rstate->ncol - $rstate->titlecol) . "\" class=\"plstat\">" . foldupbutton(7, "Statistics") . "</td></tr>\n"; 1365 foreach (self::$stats as $stat) { 1366 $t .= ' <tr'; 1367 if ($this->_row_id_pattern) 1368 $t .= " id=\"" . str_replace("#", "stat_" . ScoreInfo::$stat_keys[$stat], $this->_row_id_pattern) . "\""; 1369 $t .= ' class="pl_statrow fx7 fx8" data-statistic="' . ScoreInfo::$stat_keys[$stat] . '">'; 1370 $col = 0; 1371 foreach ($fieldDef as $fdef) { 1372 if (!$fdef->viewable_column() || !$fdef->is_visible) 1373 continue; 1374 $class = "plstat " . $fdef->className; 1375 if ($fdef->has_statistics()) 1376 $content = $fdef->statistic($this, $stat); 1377 else if ($col == $rstate->titlecol) { 1378 $content = ScoreInfo::$stat_names[$stat]; 1379 $class = "plstat pl_statheader"; 1380 } else 1381 $content = ""; 1382 $t .= '<td class="' . $class; 1383 if ($fdef->fold) 1384 $t .= ' fx' . $fdef->fold; 1385 $t .= '">' . $content . '</td>'; 1386 ++$col; 1387 } 1388 $t .= "</tr>\n"; 1389 } 1390 return $t; 1391 } 1392 1393 function ids_and_groups() { 1394 if (!$this->_prepare()) 1395 return null; 1396 $field_list = $this->_columns("id", false); 1397 $rows = $this->_rows($field_list); 1398 if ($rows === null) 1399 return null; 1400 $this->count = count($this->ids); 1401 return [$this->ids, $this->groups]; 1402 } 1403 1404 function paper_ids() { 1405 $idh = $this->ids_and_groups(); 1406 return $idh ? $idh[0] : null; 1407 } 1408 1409 private function _listDescription() { 1410 switch ($this->report_id) { 1411 case "reviewAssignment": 1412 return "Review assignments"; 1413 case "editpref": 1414 return "Review preferences"; 1415 case "reviewers": 1416 case "reviewersSel": 1417 return "Proposed assignments"; 1418 default: 1419 return null; 1420 } 1421 } 1422 1423 function session_list_object() { 1424 assert($this->ids !== null); 1425 return $this->search->create_session_list_object($this->ids, $this->_listDescription(), $this->sortdef()); 1426 } 1427 1428 function table_render($report_id, $options = array()) { 1429 if (!$this->_prepare($report_id)) 1430 return PaperListTableRender::make_error("Internal error"); 1431 // need tags for row coloring 1432 if ($this->user->can_view_tags(null)) 1433 $this->qopts["tags"] = true; 1434 1435 // get column list 1436 if (isset($options["field_list"])) 1437 $field_list = $options["field_list"]; 1438 else 1439 $field_list = $this->_list_columns(); 1440 if (!$field_list) 1441 return PaperListTableRender::make_error("No matching report"); 1442 1443 // turn off forceShow 1444 $overrides = $this->user->remove_overrides(Contact::OVERRIDE_CONFLICT); 1445 1446 // expand fields, check sort 1447 $field_list = $this->_columns($field_list, true); 1448 $rows = $this->_rows($field_list); 1449 1450 if (empty($rows)) { 1451 $this->user->set_overrides($overrides); 1452 if ($rows === null) 1453 return null; 1454 if (($altq = $this->search->alternate_query())) { 1455 $altqh = htmlspecialchars($altq); 1456 $url = $this->search->url_site_relative_raw($altq); 1457 if (substr($url, 0, 5) == "search") 1458 $altqh = "<a href=\"" . htmlspecialchars(Navigation::siteurl() . $url) . "\">" . $altqh . "</a>"; 1459 return PaperListTableRender::make_error("No matching papers. Did you mean “{$altqh}”?"); 1460 } 1461 return PaperListTableRender::make_error("No matching papers"); 1462 } 1463 1464 // get field array 1465 $fieldDef = array(); 1466 $ncol = $titlecol = 0; 1467 // folds: au:1, anonau:2, fullrow:3, aufull:4, force:5, rownum:6, statistics:7, 1468 // statistics-exist:8, [fields] 1469 $next_fold = 9; 1470 foreach ($field_list as $fdef) { 1471 if ($fdef->viewable()) { 1472 $fieldDef[$fdef->name] = $fdef; 1473 if ($fdef->fold === true) { 1474 $fdef->fold = $next_fold; 1475 ++$next_fold; 1476 } 1477 } 1478 if ($fdef->name == "title") 1479 $titlecol = $ncol; 1480 if ($fdef->viewable_column() && $fdef->is_visible) 1481 ++$ncol; 1482 } 1483 1484 // count non-callout columns 1485 $skipcallout = 0; 1486 foreach ($fieldDef as $fdef) { 1487 if ($fdef->position === null || $fdef->position >= 100) 1488 break; 1489 else 1490 ++$skipcallout; 1491 } 1492 1493 // create render state 1494 $rstate = new PaperListTableRender($ncol, $titlecol, $skipcallout); 1495 1496 // collect row data 1497 $body = array(); 1498 $grouppos = empty($this->groups) ? -1 : 0; 1499 $need_render = false; 1500 foreach ($rows as $row) { 1501 $this->_row_setup($row); 1502 if ($grouppos >= 0) 1503 $grouppos = $this->_groups_for($grouppos, $rstate, $body, false); 1504 $body[] = $this->_row_content($rstate, $row, $fieldDef); 1505 if ($this->need_render && !$need_render) { 1506 Ht::stash_script('$(plinfo.render_needed)', 'plist_render_needed'); 1507 $need_render = true; 1508 } 1509 if ($this->need_render && $this->count % 16 == 15) { 1510 $body[count($body) - 1] .= " " . Ht::script('plinfo.render_needed()') . "\n"; 1511 $this->need_render = false; 1512 } 1513 } 1514 if ($grouppos >= 0 && $grouppos < count($this->groups)) 1515 $this->_groups_for($grouppos, $rstate, $body, true); 1516 if ($this->count === 0) 1517 return PaperListTableRender::make_error("No matching papers"); 1518 1519 // analyze `has`, including authors 1520 foreach ($fieldDef as $fdef) 1521 $this->mark_has($fdef->name, $fdef->has_content); 1522 1523 // statistics rows 1524 $tfoot = ""; 1525 if (!$this->_view_columns) 1526 $tfoot = $this->_statistics_rows($rstate, $fieldDef); 1527 1528 // restore forceShow 1529 $this->user->set_overrides($overrides); 1530 1531 // header cells 1532 $colhead = ""; 1533 if (!defval($options, "noheader")) { 1534 $colhead .= " <thead class=\"pltable\">\n <tr class=\"pl_headrow\">"; 1535 $titleextra = $this->_make_title_header_extra($rstate, $fieldDef, 1536 get($options, "header_links")); 1537 1538 foreach ($fieldDef as $fdef) { 1539 if (!$fdef->viewable_column() || !$fdef->is_visible) 1540 continue; 1541 if ($fdef->has_content) { 1542 $colhead .= "<th class=\"pl plh " . $fdef->className; 1543 if ($fdef->fold) 1544 $colhead .= " fx" . $fdef->fold; 1545 $colhead .= "\">"; 1546 if ($fdef->has_content) 1547 $colhead .= $this->_field_title($fdef); 1548 if ($titleextra && $fdef->className == "pl_title") { 1549 $colhead .= $titleextra; 1550 $titleextra = false; 1551 } 1552 $colhead .= "</th>"; 1553 } else { 1554 $colhead .= "<th"; 1555 if ($fdef->fold) 1556 $colhead .= " class=\"fx{$fdef->fold}\""; 1557 $colhead .= "></th>"; 1558 } 1559 } 1560 1561 $colhead .= "</tr>\n"; 1562 1563 if ($this->search->is_order_anno 1564 && isset($this->table_attr["data-drag-tag"])) { 1565 $drag_tag = $this->tagger->check($this->table_attr["data-drag-tag"]); 1566 if (strcasecmp($drag_tag, $this->search->is_order_anno) == 0 1567 && $this->user->can_change_tag_anno($drag_tag)) { 1568 $colhead .= " <tr class=\"pl_headrow pl_annorow\" data-anno-tag=\"{$this->search->is_order_anno}\">"; 1569 if ($rstate->titlecol) 1570 $colhead .= "<td class=\"plh\" colspan=\"$rstate->titlecol\"></td>"; 1571 $colhead .= "<td class=\"plh\" colspan=\"" . ($rstate->ncol - $rstate->titlecol) . "\"><a class=\"ui js-annotate-order\" href=\"\">Annotate order</a></td></tr>\n"; 1572 } 1573 } 1574 1575 $colhead .= " </thead>\n"; 1576 } 1577 1578 // table skeleton including fold classes 1579 $foldclasses = array(); 1580 if ($this->foldable) 1581 $foldclasses = $this->_analyze_folds($rstate, $fieldDef); 1582 $enter = "<table class=\"pltable"; 1583 if ($this->_table_class) 1584 $enter .= " " . $this->_table_class; 1585 if (get($options, "list")) 1586 $enter .= " has-hotlist has-fold"; 1587 if (!empty($foldclasses)) 1588 $enter .= " " . join(" ", $foldclasses); 1589 if ($this->_table_id) 1590 $enter .= "\" id=\"" . $this->_table_id; 1591 if (!empty($options["attributes"])) 1592 foreach ($options["attributes"] as $n => $v) 1593 $enter .= "\" $n=\"" . htmlspecialchars($v); 1594 if (get($options, "fold_session_prefix")) 1595 $enter .= "\" data-fold-session-prefix=\"" . htmlspecialchars($options["fold_session_prefix"]); 1596 if ($this->search->is_order_anno) 1597 $enter .= "\" data-order-tag=\"{$this->search->is_order_anno}"; 1598 if ($this->groups) 1599 $enter .= "\" data-groups=\"" . htmlspecialchars(json_encode_browser($this->groups)); 1600 foreach ($this->table_attr as $k => $v) 1601 $enter .= "\" $k=\"" . htmlspecialchars($v); 1602 if (get($options, "list")) 1603 $enter .= "\" data-hotlist=\"" . htmlspecialchars($this->session_list_object()->info_string()); 1604 if ($this->sortable && ($url = $this->search->url_site_relative_raw())) { 1605 $url = Navigation::siteurl() . $url . (strpos($url, "?") ? "&" : "?") . "sort={sort}"; 1606 $enter .= "\" data-sort-url-template=\"" . htmlspecialchars($url); 1607 } 1608 $enter .= "\">\n"; 1609 if (self::$include_stash) 1610 $enter .= Ht::unstash(); 1611 $rstate->table_start = $enter; 1612 $rstate->table_end = "</table>"; 1613 1614 // maybe make columns, maybe not 1615 if ($this->_view_columns && !empty($this->ids) 1616 && $this->_column_split($rstate, $colhead, $body)) { 1617 $rstate->table_start = '<div class="plsplit_col_ctr_ctr"><div class="plsplit_col_ctr">' . $rstate->table_start; 1618 $rstate->table_end .= "</div></div>"; 1619 $ncol = $rstate->split_ncol; 1620 $rstate->tbody_class = "pltable_split"; 1621 } else { 1622 $rstate->thead = $colhead; 1623 $rstate->tbody_class = "pltable" . ($rstate->hascolors ? " pltable_colored" : ""); 1624 } 1625 if ($this->has_editable_tags) 1626 $rstate->tbody_class .= " need-editable-tags"; 1627 1628 // footer 1629 reset($fieldDef); 1630 if (current($fieldDef) instanceof SelectorPaperColumn 1631 && !get($options, "nofooter")) 1632 $tfoot .= $this->_footer($ncol, get_s($options, "footer_extra")); 1633 if ($tfoot) 1634 $rstate->tfoot = ' <tfoot class="pltable' . ($rstate->hascolors ? " pltable_colored" : "") . '">' . $tfoot . "</tfoot>\n"; 1635 1636 // header scripts to set up delegations 1637 if ($this->_header_script) 1638 $rstate->thead .= ' ' . Ht::script($this->_header_script) . "\n"; 1639 1640 $rstate->body_rows = $body; 1641 return $rstate; 1642 } 1643 1644 function table_html($report_id, $options = array()) { 1645 $render = $this->table_render($report_id, $options); 1646 if ($render->error) 1647 return $render->error; 1648 else 1649 return $render->table_start 1650 . ($render->thead ? : "") 1651 . $render->tbody_start() 1652 . join("", $render->body_rows) 1653 . " </tbody>\n" 1654 . ($render->tfoot ? : "") 1655 . "</table>"; 1656 } 1657 1658 function column_json($fieldId) { 1659 if (!$this->_prepare() 1660 || !($fdef = $this->find_column($fieldId))) 1661 return null; 1662 1663 // field is never folded, no sorting 1664 $this->set_view($fdef->name, true); 1665 assert(!$this->is_folded($fdef)); 1666 $this->sorters = []; 1667 1668 // get rows 1669 $field_list = $this->_columns([$fdef->name], false); 1670 assert(count($field_list) === 1); 1671 $rows = $this->_rows($field_list); 1672 if ($rows === null) 1673 return null; 1674 $fdef = $field_list[0]; 1675 1676 // turn off forceShow 1677 $overrides = $this->user->remove_overrides(Contact::OVERRIDE_CONFLICT); 1678 1679 // output field data 1680 $data = array(); 1681 if (($x = $fdef->header($this, false))) 1682 $data["{$fdef->name}.headerhtml"] = $x; 1683 $m = array(); 1684 foreach ($rows as $row) { 1685 $this->_row_setup($row); 1686 list($empty, $content) = $this->_row_field_content($fdef, $row); 1687 $m[$row->paperId] = $content; 1688 foreach ($this->row_attr as $k => $v) { 1689 if (!isset($data["attr.$k"])) 1690 $data["attr.$k"] = []; 1691 $data["attr.$k"][$row->paperId] = $v; 1692 } 1693 } 1694 $data["{$fdef->name}.html"] = $m; 1695 1696 // output statistics 1697 if ($fdef->has_statistics()) { 1698 $m = []; 1699 foreach (self::$stats as $stat) 1700 $m[ScoreInfo::$stat_keys[$stat]] = $fdef->statistic($this, $stat); 1701 $data["{$fdef->name}.stat.html"] = $m; 1702 } 1703 $this->mark_has($fdef->name, $fdef->has_content); 1704 1705 // restore forceShow 1706 $this->user->set_overrides($overrides); 1707 return $data; 1708 } 1709 1710 function text_json($fields) { 1711 if (!$this->_prepare()) 1712 return null; 1713 1714 // get column list, check sort 1715 $field_list = $this->_columns($fields, false); 1716 $rows = $this->_rows($field_list); 1717 if ($rows === null) 1718 return null; 1719 1720 $x = array(); 1721 foreach ($rows as $row) { 1722 $this->_row_setup($row); 1723 $p = array("id" => $row->paperId); 1724 foreach ($field_list as $fdef) { 1725 if ($fdef->viewable() 1726 && !$fdef->content_empty($this, $row) 1727 && ($text = $fdef->text($this, $row)) !== "") 1728 $p[$fdef->name] = $text; 1729 } 1730 $x[$row->paperId] = (object) $p; 1731 } 1732 1733 return $x; 1734 } 1735 1736 private function _row_text_csv_data(PaperInfo $row, $fieldDef) { 1737 $csv = []; 1738 foreach ($fieldDef as $fdef) { 1739 $empty = $fdef->content_empty($this, $row); 1740 $c = $empty ? "" : $fdef->text($this, $row); 1741 if ($c !== "") 1742 $fdef->has_content = true; 1743 $csv[$fdef->name] = $c; 1744 } 1745 return $csv; 1746 } 1747 1748 private function _groups_for_csv($grouppos, &$csv) { 1749 for (; $grouppos < count($this->groups) 1750 && $this->groups[$grouppos]->pos < $this->count; 1751 ++$grouppos) { 1752 $ginfo = $this->groups[$grouppos]; 1753 $csv["__precomment__"] = $ginfo->is_empty() ? "none" : $ginfo->heading; 1754 } 1755 return $grouppos; 1756 } 1757 1758 function text_csv($report_id, $options = array()) { 1759 if (!$this->_prepare($report_id)) 1760 return null; 1761 1762 // get column list, check sort 1763 if (isset($options["field_list"])) 1764 $field_list = $options["field_list"]; 1765 else 1766 $field_list = $this->_list_columns(); 1767 if (!$field_list) 1768 return null; 1769 $field_list = $this->_columns($field_list, true); 1770 $rows = $this->_rows($field_list); 1771 if ($rows === null || empty($rows)) 1772 return null; 1773 1774 // get field array 1775 $fieldDef = array(); 1776 foreach ($field_list as $fdef) 1777 if ($fdef->viewable() && $fdef->is_visible 1778 && $fdef->header($this, true) != "") 1779 $fieldDef[] = $fdef; 1780 1781 // collect row data 1782 $body = array(); 1783 $grouppos = empty($this->groups) ? -1 : 0; 1784 foreach ($rows as $row) { 1785 $this->_row_setup($row); 1786 $csv = $this->_row_text_csv_data($row, $fieldDef); 1787 if ($grouppos >= 0) 1788 $grouppos = $this->_groups_for_csv($grouppos, $csv); 1789 $body[] = $csv; 1790 } 1791 1792 // header cells 1793 $header = []; 1794 foreach ($fieldDef as $fdef) 1795 if ($fdef->has_content) 1796 $header[$fdef->name] = $fdef->header($this, true); 1797 1798 return [$header, $body]; 1799 } 1800 1801 1802 function display($report_id) { 1803 if (!($this->_prepare($report_id) 1804 && ($field_list = $this->_list_columns()))) 1805 return false; 1806 $field_list = $this->_columns($field_list, false); 1807 $res = []; 1808 if ($this->_view_force) 1809 $res["-3 force"] = "show:force"; 1810 if ($this->_view_compact_columns) 1811 $res["-2 ccol"] = "show:ccol"; 1812 else if ($this->_view_columns) 1813 $res["-2 col"] = "show:col"; 1814 if ($this->_view_row_numbers) 1815 $res["-1 rownum"] = "show:rownum"; 1816 if ($this->_view_statistics) 1817 $res["-1 statistics"] = "show:statistics"; 1818 $x = []; 1819 foreach ($this->_view_fields as $k => $v) { 1820 $f = $this->_expand_view_column($k, false); 1821 foreach ($f as $col) 1822 if ($v === "edit" 1823 || ($v && ($col->fold || !$col->is_visible)) 1824 || (!$v && !$col->fold && $col->is_visible)) { 1825 if ($v !== "edit") 1826 $v = $v ? "show" : "hide"; 1827 $key = ($col->position ? : 0) . " " . $col->name; 1828 $res[$key] = $v . ":" . PaperSearch::escape_word($col->name); 1829 } 1830 } 1831 $anonau = get($this->_view_fields, "anonau") && $this->conf->submission_blindness() == Conf::BLIND_OPTIONAL; 1832 $aufull = get($this->_view_fields, "aufull"); 1833 if (($anonau || $aufull) && !get($this->_view_fields, "au")) 1834 $res["150 authors"] = "hide:authors"; 1835 if ($anonau) 1836 $res["151 anonau"] = "show:anonau"; 1837 if ($aufull) 1838 $res["151 aufull"] = "show:aufull"; 1839 ksort($res, SORT_NATURAL); 1840 $res = array_values($res); 1841 foreach ($this->sorters as $s) { 1842 $w = "sort:" . ($s->reverse ? "-" : "") . PaperSearch::escape_word($s->field->sort_name($s->score)); 1843 if ($w !== "sort:id") 1844 $res[] = $w; 1845 } 1846 return join(" ", $res); 1847 } 1848 static function change_display(Contact $user, $report, $var = null, $val = null) { 1849 $pl = new PaperList(new PaperSearch($user, "NONE"), ["report" => $report, "sort" => true]); 1850 if ($var) 1851 $pl->set_view($var, $val); 1852 $user->conf->save_session("{$report}display", $pl->display("s")); 1853 } 1854} 1855