From 721dfc886aa796db7b815267e1c340eacff552d9 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Fri, 29 Mar 2024 09:56:34 -0400 Subject: [PATCH 01/78] Fix display of option-valued formulas. --- src/formula.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/formula.php b/src/formula.php index 6074445cc..02a4557eb 100644 --- a/src/formula.php +++ b/src/formula.php @@ -2761,7 +2761,7 @@ function unparse_html($x, $real_format = null) { } else if ($this->_format === Fexpr::FSUBFIELD) { $prow = $this->placeholder_prow(); $fr = new FieldRender(FieldRender::CFHTML); - $this->_format_detail->render($fr, new PaperValue($prow, $x)); + $this->_format_detail->render($fr, new PaperValue($prow, $this->_format_detail, $x)); return $fr->value_html(); } else if ($this->_format === Fexpr::FPREFEXPERTISE) { return ReviewField::make_expertise($this->conf)->unparse_span_html($x + 2, $real_format); From cfcddbf959c0cf544d82525f077dc3304c58fc76 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Fri, 29 Mar 2024 11:23:02 -0400 Subject: [PATCH 02/78] Fix previous. --- src/formula.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/formula.php b/src/formula.php index 02a4557eb..1fcf157b0 100644 --- a/src/formula.php +++ b/src/formula.php @@ -2761,7 +2761,7 @@ function unparse_html($x, $real_format = null) { } else if ($this->_format === Fexpr::FSUBFIELD) { $prow = $this->placeholder_prow(); $fr = new FieldRender(FieldRender::CFHTML); - $this->_format_detail->render($fr, new PaperValue($prow, $this->_format_detail, $x)); + $this->_format_detail->render($fr, PaperValue::make($prow, $this->_format_detail, $x)); return $fr->value_html(); } else if ($this->_format === Fexpr::FPREFEXPERTISE) { return ReviewField::make_expertise($this->conf)->unparse_span_html($x + 2, $real_format); From 5c4463dcb4d8a0bee48196240c8e24782957f810 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Fri, 29 Mar 2024 11:23:28 -0400 Subject: [PATCH 03/78] Avoid showing 'Conflict' review-type columns to authors. --- src/papercolumn.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/papercolumn.php b/src/papercolumn.php index 5651e639e..e65028b80 100644 --- a/src/papercolumn.php +++ b/src/papercolumn.php @@ -725,7 +725,8 @@ function content(PaperList $pl, PaperInfo $row) { $t = ""; if ($ranal) { $t = $ranal->icon_html(true); - } else if ($flags & self::F_CONFLICT) { + } else if (($flags & self::F_CONFLICT) !== 0 + && $pl->search->limit() !== "a") { $t = review_type_icon(-1); } $x = []; From a5d2d3b262f42956d49d555e6990d6e268c54947 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Fri, 29 Mar 2024 11:23:31 -0400 Subject: [PATCH 04/78] Types. --- batch/apispec.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/batch/apispec.php b/batch/apispec.php index 96aa4eff6..55d7d5145 100644 --- a/batch/apispec.php +++ b/batch/apispec.php @@ -78,7 +78,7 @@ private function expand($fn) { private function resolve_common_schema($name) { if (!isset($this->schemas[$name])) { if ($name === "pid") { - $this->schemas[$name] = [ + $this->schemas[$name] = (object) [ "type" => "integer", "minimum" => 1 ]; @@ -94,7 +94,7 @@ private function resolve_common_schema($name) { private function resolve_common_param($name) { if (!isset($this->parameters[$name])) { if ($name === "p") { - $this->parameters[$name] = [ + $this->parameters[$name] = (object) [ "name" => "p", "in" => "path", "required" => true, From bdf60370cba7bf9187d07815a6fe585c0eb3ef8b Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Fri, 29 Mar 2024 19:19:31 +0000 Subject: [PATCH 05/78] Text too --- src/formula.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/formula.php b/src/formula.php index 1fcf157b0..0121e8a7c 100644 --- a/src/formula.php +++ b/src/formula.php @@ -2795,7 +2795,7 @@ function unparse_text($x, $real_format) { } else if ($this->_format === Fexpr::FSUBFIELD) { $prow = $this->placeholder_prow(); $fr = new FieldRender(FieldRender::CFTEXT | FieldRender::CFCSV | FieldRender::CFVERBOSE); - $this->_format_detail->render($fr, new PaperValue($prow, $x)); + $this->_format_detail->render($fr, PaperValue::make($prow, $this->_format_detail, $x)); return $fr->value; // XXX } else if ($this->_format === Fexpr::FPREFEXPERTISE) { return ReviewField::make_expertise($this->conf)->unparse_computed($x + 2, $real_format); From a389183f3d7d85cab6f123768cf5e76eb50575a7 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Tue, 2 Apr 2024 11:57:23 -0400 Subject: [PATCH 06/78] Types. --- src/formula.php | 22 +++++++++++++---- src/papertable.php | 2 +- src/reviewfield.php | 38 ++++++++++++------------------ src/reviewfields/rf_checkbox.php | 6 ++--- src/reviewfields/rf_checkboxes.php | 8 +++---- 5 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/formula.php b/src/formula.php index 0121e8a7c..94ae5fe74 100644 --- a/src/formula.php +++ b/src/formula.php @@ -1977,6 +1977,18 @@ function placeholder_prow() { return $this->_placeholder_prow; } + /** @return PaperOption */ + function format_sf() { + assert($this->_format === Fexpr::FSUBFIELD); + return $this->_format_detail; + } + + /** @return ReviewField */ + function format_rf() { + assert($this->_format === Fexpr::FREVIEWFIELD); + return $this->_format_detail; + } + /** @param string $t * @param string $span * @return int */ @@ -2757,11 +2769,12 @@ function unparse_html($x, $real_format = null) { $rx = round($x * 100) / 100; if ($this->_format > Fexpr::FNUMERIC) { if ($this->_format === Fexpr::FREVIEWFIELD) { - return $this->_format_detail->unparse_span_html($rx, $real_format); + return $this->format_rf()->unparse_span_html($rx, $real_format); } else if ($this->_format === Fexpr::FSUBFIELD) { $prow = $this->placeholder_prow(); $fr = new FieldRender(FieldRender::CFHTML); - $this->_format_detail->render($fr, PaperValue::make($prow, $this->_format_detail, $x)); + $sf = $this->format_sf(); + $sf->render($fr, PaperValue::make($prow, $sf, $x)); return $fr->value_html(); } else if ($this->_format === Fexpr::FPREFEXPERTISE) { return ReviewField::make_expertise($this->conf)->unparse_span_html($x + 2, $real_format); @@ -2791,11 +2804,12 @@ function unparse_text($x, $real_format) { $rx = round($x * 100) / 100; if ($this->_format > Fexpr::FNUMERIC) { if ($this->_format === Fexpr::FREVIEWFIELD) { - return $this->_format_detail->unparse_computed($rx, $real_format); + return $this->format_rf()->unparse_computed($rx, $real_format); } else if ($this->_format === Fexpr::FSUBFIELD) { $prow = $this->placeholder_prow(); $fr = new FieldRender(FieldRender::CFTEXT | FieldRender::CFCSV | FieldRender::CFVERBOSE); - $this->_format_detail->render($fr, PaperValue::make($prow, $this->_format_detail, $x)); + $sf = $this->format_sf(); + $sf->render($fr, PaperValue::make($prow, $sf, $x)); return $fr->value; // XXX } else if ($this->_format === Fexpr::FPREFEXPERTISE) { return ReviewField::make_expertise($this->conf)->unparse_computed($x + 2, $real_format); diff --git a/src/papertable.php b/src/papertable.php index ec6f2c8f5..965eb9898 100644 --- a/src/papertable.php +++ b/src/papertable.php @@ -2661,7 +2661,7 @@ function review_table() { foreach ($conf->review_form()->forder as $f) { if ($f->view_score > $view_score && ($fv = $rr->fval($f)) !== null - && ($fh = $f->unparse_span_html($fv)) !== "") { + && ($fh = $f->unparse_span_html($fv, null)) !== "") { if ($score_header[$f->short_id] === "") { $score_header[$f->short_id] = '' . $f->web_abbreviation() . ""; } diff --git a/src/reviewfield.php b/src/reviewfield.php index a04f12cb3..3016be365 100644 --- a/src/reviewfield.php +++ b/src/reviewfield.php @@ -415,19 +415,19 @@ function value_clean_storage($fval) { * @return string */ abstract function unparse_value($fval); - /** @deprecated */ - function unparse($fval) { - return $this->unparse_value($fval); - } - /** @param ?int|?float|?string $fval * @return mixed */ abstract function unparse_json($fval); + /** @param int|float $fval + * @param ?string $format + * @return string */ + abstract function unparse_computed($fval, $format = null); + /** @param int|float|string $fval - * @param ?string $real_format + * @param ?string $format * @return string */ - function unparse_span_html($fval, $real_format = null) { + function unparse_span_html($fval, $format = null) { return ""; } @@ -437,13 +437,6 @@ function unparse_search($fval) { return ""; } - const VALUE_NONE = 0; - const VALUE_SC = 1; - /** @deprecated */ - function value_unparse($fval, $flags = 0, $real_format = null) { - return $flags & self::VALUE_SC ? $this->unparse_span_html($fval, $real_format) : $this->unparse_value($fval); - } - /** @param Qrequest $qreq * @param string $key * @return ?string */ @@ -649,11 +642,6 @@ function export_setting() { return $rfs; } - /** @param int|float $fval - * @param ?string $real_format - * @return string */ - abstract function unparse_computed($fval, $real_format = null); - const GRAPH_STACK = 1; const GRAPH_PROPORTIONS = 2; const GRAPH_STACK_REQUIRED = 3; @@ -982,15 +970,15 @@ function unparse_search($fval) { } /** @param int|float $fval - * @param ?string $real_format + * @param ?string $format * @return string */ - function unparse_computed($fval, $real_format = null) { + function unparse_computed($fval, $format = null) { if ($fval === null) { return ""; } $numeric = ($this->flags & self::FLAG_NUMERIC) !== 0; - if ($real_format !== null && $numeric) { - return sprintf($real_format, $fval); + if ($format !== null && $numeric) { + return sprintf($format, $fval); } if ($fval <= 0.8) { return "–"; @@ -1366,6 +1354,10 @@ function unparse_json($fval) { return $fval; } + function unparse_computed($fval, $format = null) { + return (string) $fval; + } + function parse($text) { $text = rtrim($text); if ($text === "") { diff --git a/src/reviewfields/rf_checkbox.php b/src/reviewfields/rf_checkbox.php index 3dc21209f..5b1009951 100644 --- a/src/reviewfields/rf_checkbox.php +++ b/src/reviewfields/rf_checkbox.php @@ -29,15 +29,15 @@ function unparse_search($fval) { return $fval > 0 ? "yes" : "no"; } - function unparse_computed($fval, $real_format = null) { + function unparse_computed($fval, $format = null) { if ($fval === null) { return ""; } else if ($fval == 0) { return "✗"; } else if ($fval == 1) { return "✓"; - } else if ($real_format !== null) { - return sprintf($real_format, $fval); + } else if ($format !== null) { + return sprintf($format, $fval); } else if ($fval < 0.125) { return "✗"; } else if ($fval >= 0.875) { diff --git a/src/reviewfields/rf_checkboxes.php b/src/reviewfields/rf_checkboxes.php index b1d4a9595..56384b14a 100644 --- a/src/reviewfields/rf_checkboxes.php +++ b/src/reviewfields/rf_checkboxes.php @@ -68,16 +68,16 @@ function unparse_search($fval) { } /** @param int|float $fval - * @param ?string $real_format + * @param ?string $format * @return string */ - function unparse_computed($fval, $real_format = null) { + function unparse_computed($fval, $format = null) { // XXX if ($fval === null) { return ""; } $numeric = ($this->flags & self::FLAG_NUMERIC) !== 0; - if ($real_format !== null && $numeric) { - return sprintf($real_format, $fval); + if ($format !== null && $numeric) { + return sprintf($format, $fval); } if ($fval <= 0.8) { return "–"; From 6752865bfb61241eae406fd092b138ec8bd41ea9 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Tue, 2 Apr 2024 13:56:27 -0400 Subject: [PATCH 07/78] cleannl does not add a newline to the end. --- lib/base.php | 5 +---- src/paperoption.php | 5 +++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/base.php b/lib/base.php index 1e9c2e3e6..d014d8ada 100644 --- a/lib/base.php +++ b/lib/base.php @@ -111,16 +111,13 @@ function preg_matchpos($pattern, $subject) { /** @param string $text * @return string */ function cleannl($text) { - if (substr($text, 0, 3) === "\xEF\xBB\xBF") { + if (str_starts_with($text, "\xEF\xBB\xBF")) { $text = substr($text, 3); } if (strpos($text, "\r") !== false) { $text = str_replace("\r\n", "\n", $text); $text = str_replace("\r", "\n", $text); } - if ($text !== "" && $text[strlen($text) - 1] !== "\n") { - $text .= "\n"; - } return $text; } diff --git a/src/paperoption.php b/src/paperoption.php index 16720dcb5..445d8bac6 100644 --- a/src/paperoption.php +++ b/src/paperoption.php @@ -852,12 +852,13 @@ function print_web_edit(PaperTable $pt, $ov, $reqov) { /** @param PaperValue $ov * @param PaperValue $reqov */ function print_web_edit_text(PaperTable $pt, $ov, $reqov, $extra = []) { - $default_value = null; $od = $ov->data(); $reqd = $reqov->data(); if ($od !== $reqd - && trim($od ?? "") !== trim(cleannl($reqd ?? ""))) { + && rtrim($od ?? "") !== rtrim(cleannl($reqd ?? ""))) { $default_value = $od ?? ""; + } else { + $default_value = null; } $pt->print_editable_option_papt($this); echo '
'; From e46c7585aec3121740c87ad17ea9c62f9a11bfa9 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Tue, 2 Apr 2024 14:02:00 -0400 Subject: [PATCH 08/78] Clean newlines in text options. --- lib/mailer.php | 2 +- src/paperoption.php | 36 +++++++++++++++++++----------------- test/t_paperstatus.php | 2 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/lib/mailer.php b/lib/mailer.php index fc57d1c49..fc889b099 100644 --- a/lib/mailer.php +++ b/lib/mailer.php @@ -578,7 +578,7 @@ function expand($text, $field = null) { // separate text into lines $lines = explode("\n", $text); - if (count($lines) && $lines[count($lines) - 1] === "") { + if (!empty($lines) && $lines[count($lines) - 1] === "") { array_pop($lines); } diff --git a/src/paperoption.php b/src/paperoption.php index 445d8bac6..f93fe234a 100644 --- a/src/paperoption.php +++ b/src/paperoption.php @@ -821,27 +821,29 @@ function parse_json(PaperInfo $prow, $j) { const PARSE_STRING_CONVERT = 8; /** @return ?PaperValue */ function parse_json_string(PaperInfo $prow, $j, $flags = 0) { - if (is_string($j)) { - if ($flags & self::PARSE_STRING_CONVERT) { - $j = convert_to_utf8($j); - } - if ($flags & self::PARSE_STRING_SIMPLIFY) { - $j = simplify_whitespace($j); - } else if ($flags & self::PARSE_STRING_TRIM) { + if ($j === null) { + return null; + } else if (!is_string($j)) { + return PaperValue::make_estop($prow, $this, "<0>Expected string"); + } + if (($flags & self::PARSE_STRING_CONVERT) !== 0) { + $j = convert_to_utf8($j); + } + if (($flags & self::PARSE_STRING_SIMPLIFY) !== 0) { + $j = simplify_whitespace($j); + } else { + $j = cleannl($j); + if (($flags & self::PARSE_STRING_TRIM) !== 0) { $j = rtrim($j); if ($j !== "" && ctype_space($j[0])) { - $j = preg_replace('/\A(?: {0,3}[\r\n]*)*/', "", $j); + $j = preg_replace('/\A(?: ? ? ?\n)*+ ? ? ?/', "", $j); } } - if ($j !== "" || ($flags & self::PARSE_STRING_EMPTY) !== 0) { - return PaperValue::make($prow, $this, 1, $j); - } else { - return PaperValue::make($prow, $this); - } - } else if ($j === null) { - return null; + } + if ($j !== "" || ($flags & self::PARSE_STRING_EMPTY) !== 0) { + return PaperValue::make($prow, $this, 1, $j); } else { - return PaperValue::make_estop($prow, $this, "<0>Expected string"); + return PaperValue::make($prow, $this); } } @@ -1680,7 +1682,7 @@ function value_export_json(PaperValue $ov, PaperExport $pex) { } function parse_qreq(PaperInfo $prow, Qrequest $qreq) { - return $this->parse_json_string($prow, convert_to_utf8($qreq[$this->formid] ?? "")); + return $this->parse_json_string($prow, $qreq[$this->formid] ?? "", PaperOption::PARSE_STRING_CONVERT); } function parse_json(PaperInfo $prow, $j) { return $this->parse_json_string($prow, $j); diff --git a/test/t_paperstatus.php b/test/t_paperstatus.php index f52c882d1..57b64b75a 100644 --- a/test/t_paperstatus.php +++ b/test/t_paperstatus.php @@ -696,7 +696,7 @@ function test_save_abstract_format() { xassert(!$ps->has_problem()); xassert_array_eqq($ps->changed_keys(), ["abstract"], true); $nprow1 = $this->u_estrin->checked_paper_by_id($this->pid2); - xassert_eqq($nprow1->abstract, "They\nsee\r\nlots of\n\n\ncolors."); + xassert_eqq($nprow1->abstract, "They\nsee\nlots of\n\n\ncolors."); } function test_save_collaborators() { From b376df2461360572333fcc3bbb2c9787be5f1e77 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Tue, 2 Apr 2024 14:02:07 -0400 Subject: [PATCH 09/78] Style improvement. --- scripts/script.js | 2 +- stylesheets/style.css | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/script.js b/scripts/script.js index c8c3c8c30..cb3c91fff 100644 --- a/scripts/script.js +++ b/scripts/script.js @@ -3881,7 +3881,7 @@ handle_ui.on("js-tracker", function (evt) { $pu.on("closedialog", clear_elapsed) .on("click", "button[name=new]", new_tracker) .on("click", "button[name=stopall]", stop_all) - .on("submit", "form", submit); + .on("submit", submit); } if (evt.shiftKey || evt.ctrlKey diff --git a/stylesheets/style.css b/stylesheets/style.css index dab48c0da..2c025832d 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -5053,7 +5053,8 @@ a.help { max-width: 32em; } .modal-dialog-w40 { - width: 40rem; + width: fit-content; + min-width: 40rem; max-width: 80vw; } .modal-dialog-wide { From e4dd45c7f409b682505bf6a10cc4f0a874973ed3 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Tue, 2 Apr 2024 14:34:46 -0400 Subject: [PATCH 10/78] Clean newlines in review fields. --- src/reviewfield.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reviewfield.php b/src/reviewfield.php index 3016be365..12ad39f84 100644 --- a/src/reviewfield.php +++ b/src/reviewfield.php @@ -1359,7 +1359,7 @@ function unparse_computed($fval, $format = null) { } function parse($text) { - $text = rtrim($text); + $text = rtrim(cleannl($text)); if ($text === "") { return null; } From 3f678879ad6b746f46af6d17bc16fa95e487daf0 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Mon, 8 Apr 2024 10:24:46 -0400 Subject: [PATCH 11/78] Correct error on browser that returns null from getAttribute. They're supposed to return empty-string but whatever. --- scripts/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/script.js b/scripts/script.js index cb3c91fff..fc00e6453 100644 --- a/scripts/script.js +++ b/scripts/script.js @@ -3318,7 +3318,7 @@ function resize(b) { document.body.style.minHeight = "calc(100vh - " + h + "px)"; } else { for (const e of offs) { - const bo = e.getAttribute("data-banner-offset"); + const bo = e.getAttribute("data-banner-offset") || ""; e.style[bo.startsWith("B") ? "bottom" : "top"] = null; } document.body.style.minHeight = null; From 2034df6fbd946ee348d014b3054141d9ef18148e Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Mon, 8 Apr 2024 11:28:31 -0400 Subject: [PATCH 12/78] Improve interaction with faceted tables. Reduce JS errors. --- scripts/script.js | 200 ++++++++++++++++++++++++++-------------------- 1 file changed, 115 insertions(+), 85 deletions(-) diff --git a/scripts/script.js b/scripts/script.js index fc00e6453..dcc2f6b8a 100644 --- a/scripts/script.js +++ b/scripts/script.js @@ -9230,14 +9230,13 @@ function tablelist(elt) { return elt ? elt.closest(".pltable") : null; } -function tablelist_each_facet(tbl, f) { - if (hasClass(tbl, "pltable-facets")) { - for (tbl = tbl.firstChild; tbl; tbl = tbl.nextSibling) { - if (tbl.nodeName === "TABLE") - f(tbl); - } +function tablelist_facets(tbl) { + if (!tbl) { + return []; + } else if (!hasClass(tbl, "pltable-facets")) { + return [tbl]; } else { - f(tbl); + return tbl.children; } } @@ -9316,17 +9315,20 @@ function tagannorow_fill(row, anno) { } } -function tagannorow_add(tbl, tbody, before, anno) { - tbl = tbl || tablelist(tbody); - var $r = $(tbl).find("thead > tr.pl_headrow:first-child > th"), - selcol = -1, titlecol = -1, ncol = $r.length, i, x, tr; - for (i = 0; i !== ncol; ++i) { - if (hasClass($r[i], "pl_sel")) - selcol = i; - else if (hasClass($r[i], "pl_title")) - titlecol = i; +function tagannorow_add(tfacet, tbody, before, anno) { + let selcol = -1, titlecol = -1, ncol = 0, i = 0; + for (const th of tfacet.tHead.rows[0].children) { + if (th.nodeName === "TH") { + if (hasClass(th, "pl_sel")) { + selcol = ncol; + } else if (hasClass(th, "pl_title")) { + titlecol = ncol; + } + ++ncol; + } } + let tr; if (anno.blank) { tr = $e("tr", "plheading-blank", $e("td", {"class": "plheading", colspan: ncol})); @@ -9371,51 +9373,62 @@ function tagannorow_add(tbl, tbody, before, anno) { function tablelist_reorder(tbl, pids, groups, remove_all) { - var tbody = tbl.tBodies[0], pida = "data-pid"; - remove_all && $(tbody).detach(); - - var rowmap = [], xpid = 0, cur = tbody.firstChild, next; - while (cur) { - if (cur.nodeType === 1 && (xpid = cur.getAttribute(pida))) - rowmap[xpid] = rowmap[xpid] || []; - next = cur.nextSibling; - if (xpid) - rowmap[xpid].push(cur); - else - tbody.removeChild(cur); - cur = next; + const pida = "data-pid", tfacets = tablelist_facets(tbl), tbodies = [], rowmap = []; + for (const tf of tfacets) { + const tb = tf.tBodies[0]; + tbodies.push(tb); + remove_all && $(tb).detach(); + let cur = tb.firstChild; + while (cur) { + const xpid = cur.nodeType === 1 && cur.getAttribute(pida), + next = cur.nextSibling; + if (xpid) { + rowmap[xpid] = rowmap[xpid] || []; + rowmap[xpid].push(cur); + } else { + cur.remove(); + } + cur = next; + } } - cur = tbody.firstChild; - var cpid = cur ? cur.getAttribute(pida) : 0; - - var pid_index = 0, grp_index = 0; + let tf = tfacets[0], tb = tbodies[0], + cur = tb.firstChild, cpid = cur && cur.getAttribute(pida), + pid_index = 0, grp_index = 0; groups = groups || []; while (pid_index < pids.length || grp_index < groups.length) { - // handle headings - if (grp_index < groups.length && groups[grp_index].pos == pid_index) { + if (grp_index < groups.length && groups[grp_index].pos === pid_index) { + if (tbodies.length > 1) { + tf = tfacets[grp_index]; + tb = tbodies[grp_index]; + cur = tb.firstChild; + cpid = cur && cur.getAttribute(pida); + } if (grp_index > 0 || !groups[grp_index].blank) { - tagannorow_add(tbl, tbody, cur, groups[grp_index]); + tagannorow_add(tf, tb, cur, groups[grp_index]); } ++grp_index; } else { - var npid = pids[pid_index]; + const npid = pids[pid_index]; if (cpid == npid) { do { cur = cur.nextSibling; - if (!cur || cur.nodeType == 1) - cpid = cur ? cur.getAttribute(pida) : 0; + cpid = cur && cur.getAttribute(pida); } while (cpid == npid); } else { - for (var j = 0; rowmap[npid] && j < rowmap[npid].length; ++j) - tbody.insertBefore(rowmap[npid][j], cur); - delete rowmap[npid]; + for (const tr of rowmap[npid] || []) { + tb.insertBefore(tr, cur); + } } ++pid_index; } } - remove_all && $(tbody).appendTo(tbl); + if (remove_all) { + for (let i = 0; i !== tfacets.length; ++i) { + tfacets[i].insertBefore(tbodies[i], tfacets[i].tFoot); + } + } tablelist_postreorder(tbl); } @@ -9433,20 +9446,22 @@ function tablelist_postreorder(tbl) { nh = 0; lasthead = head; } - for (let cur = tbl.tBodies[0].firstChild; cur; cur = cur.nextSibling) { - if (cur.nodeName !== "TR") { - continue; - } else if (hasClass(cur, "plheading")) { - change_heading(cur); - } else if (hasClass(cur, "pl")) { - e = !e; - ++n; - ++nh; - $(cur.firstChild).find(".pl_rownum").text(n + ". "); - } - if (hasClass(cur, e ? "k0" : "k1")) { - toggleClass(cur, "k0", !e); - toggleClass(cur, "k1", e); + for (const tf of tablelist_facets(tbl)) { + for (let cur = tf.tBodies[0].firstChild; cur; cur = cur.nextSibling) { + if (cur.nodeName !== "TR") { + continue; + } else if (hasClass(cur, "plheading")) { + change_heading(cur); + } else if (hasClass(cur, "pl")) { + e = !e; + ++n; + ++nh; + $(cur.firstChild).find(".pl_rownum").text(n + ". "); + } + if (hasClass(cur, e ? "k0" : "k1")) { + toggleClass(cur, "k0", !e); + toggleClass(cur, "k1", e); + } } } lasthead && change_heading(null); @@ -9470,23 +9485,40 @@ function sorter_analyze(sorter) { } function tablelist_ids(tbl) { - var tbody = tbl.tBodies[0], tbl_ids = [], xpid; - for (var cur = tbody.firstChild; cur; cur = cur.nextSibling) - if (cur.nodeType === 1 - && /^pl\b/.test(cur.className) - && (xpid = cur.getAttribute("data-pid"))) - tbl_ids.push(+xpid); - return tbl_ids; + const ids = []; + let xpid; + for (const t of tablelist_facets(tbl)) { + for (const tr of t.tBodies[0].children) { + if (tr.nodeType === 1 + && (tr.className === "pl" || tr.className.startsWith("pl ")) + && (xpid = tr.getAttribute("data-pid"))) { + ids.push(+xpid); + } + } + } + return ids; } -function tablelist_ids_equal(tbl, ids) { - var tbl_ids = tablelist_ids(tbl); +function tablelist_compatible(tbl, data) { + if (hasClass(tbl, "pltable-facets") + && tbl.children.length !== data.groups.length) { + return false; + } + const tbl_ids = tablelist_ids(tbl), ids = [].concat(data.ids); tbl_ids.sort(); - ids = [].concat(ids); ids.sort(); return tbl_ids.join(" ") === ids.join(" "); } +function facet_sortable_ths(tbl) { + const l = []; + for (const th of tbl.tHead.rows[0].children) { + if (th.nodeName === "TH" && hasClass(th, "sortable")) + l.push(th); + } + return l; +} + function tablelist_header_sorter(th) { var pc = th.getAttribute("data-pc"), pcsort = th.getAttribute("data-pc-sort"), @@ -9552,7 +9584,7 @@ function tablelist_load(tbl, k, v) { } function success(data) { var use_history = tbl === mainlist() && k; - if (data.ok && data.ids && tablelist_ids_equal(tbl, data.ids)) { + if (data.ok && data.ids && tablelist_compatible(tbl, data)) { use_history && push_history_state(); tablelist_apply(tbl, data, searchparam); use_history && push_history_state(hoturl_search(window.location.href, k, v)); @@ -9578,20 +9610,19 @@ function search_sort_click(evt) { function scoresort_change() { var tbl = mainlist(); $.post(hoturl("=api/session"), {v: "scoresort=" + this.value}); - if (tbl) { - tablelist_load(tbl, "scoresort", this.value); - } + tbl && tablelist_load(tbl, "scoresort", this.value); } function showforce_click() { - var tbl = mainlist(), v = this.checked ? 1 : null; - if (tbl) { - siteinfo.want_override_conflict = !!v; - $(tbl.tHead.rows[0]).find("th.sortable a.pl_sort").each(function () { - this.setAttribute("href", hoturl_search(this.getAttribute("href"), "forceShow", v)); - }); - tablelist_load(tbl, "forceShow", v); + const plt = mainlist(), v = this.checked ? 1 : null; + siteinfo.want_override_conflict = !!v; + for (const tbl of tablelist_facets(plt)) { + for (const th of facet_sortable_ths(tbl)) { + const a = th.querySelector("a.pl_sort"); + a && a.setAttribute("href", hoturl_search(a.getAttribute("href"), "forceShow", v)); + } } + plt && tablelist_load(plt, "forceShow", v); } if ("pushState" in window.history) { @@ -10386,14 +10417,13 @@ Assign_DraggableTable.prototype.commit = function () { return { try_reorder: function (tbl, data) { if (data.search_params === tbl.getAttribute("data-search-params") - && tablelist_ids_equal(tbl, data.ids)) { + && tablelist_compatible(tbl, data)) { tablelist_reorder(tbl, data.ids, data.groups); } }, prepare_draggable: function () { - tablelist_each_facet(this, function (tbl) { - var tr; - for (tr = tbl.tBodies[0].firstChild; tr; tr = tr.nextSibling) { + for (const tbl of tablelist_facets(this)) { + for (const tr of tbl.tBodies[0].children) { if (tr.nodeName === "TR" && (hasClass(tr, "pl") || (hasClass(tr, "plheading") && tr.hasAttribute("data-anno-id"))) @@ -10401,7 +10431,7 @@ return { add_draghandle(tr); } } - }); + } } }; })(); @@ -10551,9 +10581,9 @@ function populate_pidrows(tbl, bypid) { Plist.prototype.pidrow = function (pid) { if (!(pid in this._bypid)) { var bypid = this._bypid; - tablelist_each_facet(this.pltable, function (tbl) { + for (const tbl of tablelist_facets(this.pltable)) { populate_pidrows(tbl, bypid); - }); + } } return this._bypid[pid]; }; From a994dd238beca10d68f76e421b86e150fd407c76 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Mon, 8 Apr 2024 11:33:12 -0400 Subject: [PATCH 13/78] Allow combinations of sortable tag columns and THEN. --- src/paperlist.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/paperlist.php b/src/paperlist.php index 05ee66b98..d3cf55866 100644 --- a/src/paperlist.php +++ b/src/paperlist.php @@ -953,8 +953,7 @@ function sorters() { } // default editable tag $this->_sort_etag = ""; - if (!$thenqe - && $this->_sortcol[0] instanceof Tag_PaperColumn + if ($this->_sortcol[0] instanceof Tag_PaperColumn && !$this->_sortcol[0]->sort_descending) { $this->_sort_etag = $this->_sortcol[0]->etag(); } @@ -1000,10 +999,9 @@ private function _sort(PaperInfoSet $rowset) { $this->user->set_overrides($overrides); // clean up, assign groups - if ($this->_sort_etag !== "") { + $groups = $this->search->group_anno_list(); + if (empty($groups) && $this->_sort_etag !== "") { $groups = $this->_sort_etag_anno_groups(); - } else { - $groups = $this->search->group_anno_list(); } if (!empty($groups)) { $this->_collect_groups($rowset->as_list(), $groups); From 44d58d38d3f2c4463fc024debbc3a036d30f9f43 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Mon, 8 Apr 2024 11:33:22 -0400 Subject: [PATCH 14/78] Change jserror filtering. --- scripts/script.js | 4 +++- src/api/api_error.php | 5 +---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/scripts/script.js b/scripts/script.js index dcc2f6b8a..24c88175b 100644 --- a/scripts/script.js +++ b/scripts/script.js @@ -379,7 +379,9 @@ function log_jserror(errormsg, error, noconsole) { (function () { var old_onerror = window.onerror, nerrors_logged = 0; window.onerror = function (errormsg, url, lineno, colno, error) { - if ((url || !lineno) && ++nerrors_logged <= 10) { + if ((url || !lineno) + && ++nerrors_logged <= 10 + && !/(?:moz|safari|chrome)-extension|AdBlock/.test(errormsg)) { var x = {error: errormsg, url: url, lineno: lineno}; if (colno) x.colno = colno; diff --git a/src/api/api_error.php b/src/api/api_error.php index 255448e72..61aae763b 100644 --- a/src/api/api_error.php +++ b/src/api/api_error.php @@ -6,10 +6,7 @@ class Error_API { static function jserror(Contact $user, Qrequest $qreq) { $errormsg = trim((string) $qreq->error); if ($errormsg === "" - || (isset($_SERVER["HTTP_USER_AGENT"]) - && preg_match('/MSIE [78]|MetaSr/', $_SERVER["HTTP_USER_AGENT"])) - || preg_match('/(?:moz|safari|chrome)-extension/', $errormsg . ($qreq->stack ?? "")) - || strpos($errormsg, "Uncaught ReferenceError: hotcrp") !== false) { + || preg_match('/(?:moz|safari|chrome)-extension/', $errormsg . ($qreq->stack ?? ""))) { return new JsonResult(["ok" => true]); } $url = $qreq->url ?? ""; From 1865f76f48526e8241d081b13affbaa753a33564 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Mon, 8 Apr 2024 11:52:13 -0400 Subject: [PATCH 15/78] Reduce jserrors for API failures. These errors (cannot save preference, cannot save comment) are common (e.g. attempting to edit a response after the deadline), and error messages about them are not useful. Most likely we should use error codes for all errors. --- src/api/api_comment.php | 12 +++++++----- src/api/api_preference.php | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/api/api_comment.php b/src/api/api_comment.php index 25f68bf11..423f52ff6 100644 --- a/src/api/api_comment.php +++ b/src/api/api_comment.php @@ -11,6 +11,8 @@ class Comment_API { private $prow; /** @var int */ private $status = 200; + /** @var bool */ + private $ok = true; /** @var MessageSet */ private $ms; @@ -95,7 +97,7 @@ private function run_post(Qrequest $qreq, $rrd, $crow) { // empty if ($req["text"] === "" && empty($docs)) { if (!$qreq->delete && (!$xcrow->commentId || !isset($qreq->text))) { - $this->status = 400; + $this->ok = false; $this->ms->error_at(null, "<0>Refusing to save empty comment"); return null; } else { @@ -107,7 +109,7 @@ private function run_post(Qrequest $qreq, $rrd, $crow) { $newctype = $xcrow->requested_type($req); $whyNot = $this->user->perm_edit_comment($this->prow, $xcrow, $newctype); if ($whyNot) { - $this->status = 403; + $this->ok = false; $whyNot->append_to($this->ms, null, 2); return null; } @@ -260,16 +262,16 @@ private function run_qreq(Qrequest $qreq) { } // check post - if ($this->status === 200 && $qreq->is_post()) { + if ($this->status === 200 && $this->ok && $qreq->is_post()) { $crow = $this->run_post($qreq, $rrd, $crow); } if ($this->status === self::RESPONSE_REPLACED) { // report response replacement error - $jr = JsonResult::make_error(404, "<0>{$uccmttype} was edited concurrently"); + $jr = JsonResult::make_error(200, "<0>{$uccmttype} was edited concurrently"); $jr["conflict"] = true; } else { - $jr = new JsonResult($this->status, ["ok" => $this->status <= 299]); + $jr = new JsonResult($this->status, ["ok" => $this->ok && $this->status <= 299]); if ($this->ms->has_message()) { $jr["message_list"] = $this->ms->message_list(); } diff --git a/src/api/api_preference.php b/src/api/api_preference.php index 23bfa9c74..9e5c7759a 100644 --- a/src/api/api_preference.php +++ b/src/api/api_preference.php @@ -18,7 +18,7 @@ static function pref_api(Contact $user, Qrequest $qreq, ?PaperInfo $prow) { } } if (!$user->can_edit_preference_for($u, $prow)) { - return JsonResult::make_error(403, "<0>Can’t edit preference for #{$prow->paperId}"); + return JsonResult::make_error(200, "<0>Can’t edit preference for #{$prow->paperId}"); } if ($qreq->method() === "POST" || isset($qreq->pref)) { From 9c978fb3a9607cabc98bed491368d4cd4b4aaae4 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 10 Apr 2024 07:57:32 -0400 Subject: [PATCH 16/78] Make UserStatus::parse_roles public and static. --- src/userstatus.php | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/userstatus.php b/src/userstatus.php index 31a1a7922..82d999a03 100644 --- a/src/userstatus.php +++ b/src/userstatus.php @@ -405,13 +405,13 @@ private function make_keyed_object($x, $field, $lc = false) { } else if (is_array($x)) { foreach ($x as $v) { if (!is_string($v)) { - $this->error_at($field, "<0>Format error [$field]"); + $this->error_at($field, "<0>Format error [{$field}]"); } else if ($v !== "") { $res[$lc ? strtolower($v) : $v] = true; } } } else { - $this->error_at($field, "<0>Format error [$field]"); + $this->error_at($field, "<0>Format error [{$field}]"); } return (object) $res; } @@ -478,7 +478,7 @@ private function normalize($cj, $old_user) { "affiliation", "phone", "new_password", "city", "state", "zip", "country"] as $k) { if (isset($cj->$k) && !is_string($cj->$k)) { - $this->error_at($k, "<0>Format error [$k]"); + $this->error_at($k, "<0>Format error [{$k}]"); unset($cj->$k); } } @@ -682,8 +682,9 @@ private function normalize_topics($cj, $old_user, $tk, $in_topics) { } /** @param int $old_roles + * @param ?MessageSet $ms * @return int */ - private function parse_roles($j, $old_roles) { + static function parse_roles($j, $old_roles, $ms = null) { if (is_object($j) || is_associative_array($j)) { $reset_roles = true; $ij = []; @@ -691,7 +692,7 @@ private function parse_roles($j, $old_roles) { if ($v === true) { $ij[] = $k; } else if ($v !== false && $v !== null) { - $this->error_at("roles", "<0>Format error [roles]"); + $ms && $ms->error_at("roles", "<0>Format error in roles"); return $old_roles; } } @@ -703,7 +704,7 @@ private function parse_roles($j, $old_roles) { $ij = $j; } else { if ($j !== null) { - $this->error_at("roles", "<0>Format error [roles]"); + $ms && $ms->error_at("roles", "<0>Format error in roles"); } return $old_roles; } @@ -711,7 +712,7 @@ private function parse_roles($j, $old_roles) { $add_roles = $remove_roles = 0; foreach ($ij as $v) { if (!is_string($v)) { - $this->error_at("roles", "<0>Format error [roles]"); + $ms && $ms->error_at("roles", "<0>Format error in roles"); return $old_roles; } else if ($v !== "") { $action = null; @@ -720,13 +721,13 @@ private function parse_roles($j, $old_roles) { $v = $m[2]; } if ($v === "") { - $this->error_at("roles", "<0>Format error [roles]"); + $ms && $ms->error_at("roles", "<0>Format error in roles"); return $old_roles; } else if (is_bool($action) && strcasecmp($v, "none") === 0) { - $this->error_at("roles", "<0>Format error near “none” [roles]"); + $ms && $ms->error_at("roles", "<0>Format error near “none”"); return $old_roles; } else if (is_bool($reset_roles) && is_bool($action) === $reset_roles) { - $this->warning_at("roles", "<0>Expected ‘" . ($reset_roles ? "" : "+") . "{$v}’ in roles"); + $ms && $ms->warning_at("roles", "<0>Expected ‘" . ($reset_roles ? "" : "+") . "{$v}’ in roles"); } else if ($reset_roles === null) { $reset_roles = $action === null; } @@ -739,7 +740,7 @@ private function parse_roles($j, $old_roles) { || strcasecmp($v, "admin") === 0) { $role = Contact::ROLE_ADMIN; } else if (strcasecmp($v, "none") !== 0) { - $this->warning_at("roles", "<0>Unknown role ‘{$v}’"); + $ms && $ms->warning_at("roles", "<0>Unknown role ‘{$v}’"); } if ($action !== false) { $add_roles |= $role; @@ -913,7 +914,7 @@ function save_user($cj, $old_user = null) { $this->normalize($cj, $user); $roles = $old_roles = $old_user ? $old_user->roles : 0; if (isset($cj->roles)) { - $roles = $this->parse_roles($cj->roles, $roles); + $roles = self::parse_roles($cj->roles, $roles, $this); if ($old_user) { $roles = $this->check_role_change($roles, $old_user); } From 9d0afc9d9e8a2aa960d787a471c465d2234bdc94 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 10 Apr 2024 16:18:21 -0400 Subject: [PATCH 17/78] Correct errors with dragging regions. --- .eslintrc.json | 3 ++- scripts/graph.js | 5 +++++ scripts/script.js | 17 +++++++++++------ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 27182da08..ba381711b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -9,7 +9,8 @@ }, "globals": { "Set": "readonly", - "WeakMap": "readonly" + "WeakMap": "readonly", + "Promise": "readonly" }, "rules": { "no-empty": 0, diff --git a/scripts/graph.js b/scripts/graph.js index 46a11fe84..3f206faa1 100644 --- a/scripts/graph.js +++ b/scripts/graph.js @@ -1,6 +1,11 @@ // graph.js -- HotCRP JavaScript library for graph drawing // Copyright (c) 2006-2024 Eddie Kohler; see LICENSE. +/* global hotcrp, siteinfo, $$, svge */ +/* global hasClass */ +/* global append_feedback_near, log_jserror */ +/* global make_bubble */ +/* global strftime, text_to_html, escape_html */ hotcrp.graph = (function ($, d3) { var handle_ui = hotcrp.handle_ui, ensure_pattern = hotcrp.ensure_pattern, diff --git a/scripts/script.js b/scripts/script.js index 24c88175b..b68b57ed3 100644 --- a/scripts/script.js +++ b/scripts/script.js @@ -9242,6 +9242,10 @@ function tablelist_facets(tbl) { } } +function facet_tablelist(tfacet) { + return hasClass(tfacet, "pltable-facet") ? tfacet.parentElement : tfacet; +} + function tablelist_search(tbl) { var x = tbl.getAttribute("data-search-params"); if (x === "" && tbl === mainlist()) { /* XXX backward compat */ @@ -9318,7 +9322,7 @@ function tagannorow_fill(row, anno) { } function tagannorow_add(tfacet, tbody, before, anno) { - let selcol = -1, titlecol = -1, ncol = 0, i = 0; + let selcol = -1, titlecol = -1, ncol = 0; for (const th of tfacet.tHead.rows[0].children) { if (th.nodeName === "TH") { if (hasClass(th, "pl_sel")) { @@ -9362,11 +9366,12 @@ function tagannorow_add(tfacet, tbody, before, anno) { $e("span", "plheading-count"))); } - if (anno.tag - && anno.annoid - && (x = tbl.getAttribute("data-drag-action")).startsWith("tagval:") - && strnatcasecmp(x, "#" + tag_canonicalize(anno.tag)) === 0) { - add_draghandle(tr); + if (anno.tag && anno.annoid) { + const dra = facet_tablelist(tfacet).getAttribute("data-drag-action") || ""; + if (dra.startsWith("tagval:") + && strnatcasecmp(dra, "tagval:" + tag_canonicalize(anno.tag)) === 0) { + add_draghandle(tr); + } } tbody.insertBefore(tr, before); tagannorow_fill(tr, anno) From a750c121e05cd97de5ebff63c6a638a36d5593ec Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 10 Apr 2024 16:31:52 -0400 Subject: [PATCH 18/78] Fix warning if form doesn't exist. --- scripts/script.js | 93 ++++++++++++++++++++++++++--------------------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/scripts/script.js b/scripts/script.js index b68b57ed3..cdcbbdafc 100644 --- a/scripts/script.js +++ b/scripts/script.js @@ -5088,12 +5088,13 @@ function populate(e, v, placeholder) { } handle_ui.on("input.js-email-populate", function () { - var self = this, - v = self.value.toLowerCase().trim(), - f = this.form || this.closest("form"), + const self = this, f = this.form || this.closest("form"); + if (!f) { + return; + } + let v = self.value.toLowerCase().trim(), fn = null, ln = null, nn = null, af = null, - country = null, orcid = null, placeholder = false, - idx; + country = null, orcid = null, placeholder = false, idx; if (this.name === "email" || this.name === "uemail") { fn = f.elements.firstName; ln = f.elements.lastName; @@ -5109,38 +5110,46 @@ handle_ui.on("input.js-email-populate", function () { idx = parseInt(this.name.substring(9)); nn = f.elements["contacts:" + idx + ":name"]; } - if (!fn && !ln && !nn && !af) + if (!fn && !ln && !nn && !af) { return; + } function success(data) { if (data) { - if (data.email) + if (data.email) { data.lemail = data.email.toLowerCase(); - else + } else { data.lemail = v + "~"; - if (!email_info.length) + } + if (!email_info.length) { email_info_at = now_sec(); - var i = 0; - while (i !== email_info.length && email_info[i] !== v) + } + let i = 0; + while (i !== email_info.length && email_info[i] !== v) { i += 2; - if (i === email_info.length) + } + if (i === email_info.length) { email_info.push(v, data); + } } - if (!data || !data.email || data.lemail !== v) + if (!data || !data.email || data.lemail !== v) { data = {}; + } if (self.value.trim() !== v - && self.getAttribute("data-populated-email") === v) + && self.getAttribute("data-populated-email") === v) { return; - if (!data.name) { - if (data.firstName && data.lastName) + } + fn && populate(fn, data.firstName || "", placeholder); + ln && populate(ln, data.lastName || "", placeholder); + if (nn && !data.name) { + if (data.firstName && data.lastName) { data.name = data.firstName + " " + data.lastName; - else if (data.lastName) + } else if (data.lastName) { data.name = data.lastName; - else + } else { data.name = data.firstName; + } } - fn && populate(fn, data.firstName || "", placeholder); - ln && populate(ln, data.lastName || "", placeholder); nn && populate(nn, data.name || "", placeholder); af && populate(af, data.affiliation || "", placeholder); country && populate(country, data.country || "", false); @@ -5152,28 +5161,30 @@ handle_ui.on("input.js-email-populate", function () { self.setAttribute("data-populated-email", v); } - if (/^\S+@\S+\.\S\S+$/.test(v)) { - if ((email_info_at && now_sec() - email_info_at >= 3600) - || email_info.length > 200) - email_info = []; - var i = 0; - while (i !== email_info.length - && (v < email_info[i] || v > email_info[i + 1].lemail)) - i += 2; - if (i === email_info.length) { - var args = {email: v}; - if (hasClass(this, "want-potential-conflict")) { - args.potential_conflict = 1; - args.p = siteinfo.paperid; - } - $.ajax(hoturl("=api/user", args), { - method: "GET", success: success - }); - } else if (v === email_info[i + 1].lemail) { - success(email_info[i + 1]); - } else { - success(null); + if (!/^\S+@\S+\.\S\S+$/.test(v)) { + success(null); + return; + } + if ((email_info_at && now_sec() - email_info_at >= 3600) + || email_info.length > 200) { + email_info = []; + } + let i = 0; + while (i !== email_info.length + && (v < email_info[i] || v > email_info[i + 1].lemail)) { + i += 2; + } + if (i === email_info.length) { + let args = {email: v}; + if (hasClass(this, "want-potential-conflict")) { + args.potential_conflict = 1; + args.p = siteinfo.paperid; } + $.ajax(hoturl("=api/user", args), { + method: "GET", success: success + }); + } else if (v === email_info[i + 1].lemail) { + success(email_info[i + 1]); } else { success(null); } From 9e674c665c3f5deace972d952284bd134b8cf989 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Thu, 11 Apr 2024 11:52:24 -0400 Subject: [PATCH 19/78] Rearrange tooltip.html logic: clear CSS in show(). The point is to allow manipulating content_node() on its own. --- scripts/script.js | 41 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/scripts/script.js b/scripts/script.js index cdcbbdafc..5292c400b 100644 --- a/scripts/script.js +++ b/scripts/script.js @@ -2542,6 +2542,7 @@ return function (content, bubopt) { ds = /^[ahv]$/.test(dsx) ? dsx : "a"; var wpos = $(window).geometry(); + $(bubdiv).css({maxWidth: "", left: "", top: ""}); var bpos = make_bpos(wpos, dsx); if (ds === "a") { @@ -2668,20 +2669,22 @@ return function (content, bubopt) { }, html: function (content) { var n = bubch[1]; - if (content === undefined) + if (content === undefined) { return n.innerHTML; + } if (typeof content === "string" && content === n.innerHTML - && bubdiv.style.visibility === "visible") + && bubdiv.style.visibility === "visible") { return bubble; - nearpos && $(bubdiv).css({maxWidth: "", left: "", top: ""}); - if (typeof content === "string") + } + if (typeof content === "string") { n.innerHTML = content; - else if (content && content.jquery) { + } else if (content && content.jquery) { n.replaceChildren(); content.appendTo(n); - } else + } else { n.replaceChildren(content); + } nearpos && show(); return bubble; }, @@ -2719,9 +2722,8 @@ return function (content, bubopt) { })(); -var global_tooltip; hotcrp.tooltip = (function ($) { -var builders = {}; +var builders = {}, global_tooltip = null; function prepare_info(elt, info) { var xinfo = elt.getAttribute("data-tooltip-info"); @@ -2761,7 +2763,7 @@ function show_tooltip(info) { var tt, bub = null, to = null, near = null, refcount = 0, content = info.content; - function erase() { + function close() { to = clearTimeout(to); bub && bub.remove(); $self.removeData("tooltipState"); @@ -2813,11 +2815,10 @@ function show_tooltip(info) { var delay = info.type === "focus" ? 0 : 200; to = clearTimeout(to); if (--refcount == 0 && info.type !== "sticky") - to = setTimeout(erase, delay); + to = setTimeout(close, delay); return tt; }, - close: erase, - erase: erase, /* XXX backward compat */ + close: close, _element: $self[0], html: function (new_content) { if (new_content === undefined) { @@ -2863,7 +2864,11 @@ tooltip.close = function (e) { var tt = e ? $(e).data("tooltipState") : global_tooltip; tt && tt.close(); }; -tooltip.erase = tooltip.close; /* XXX backward compat */ +tooltip.close_under = function (e) { + if (global_tooltip && e.contains(global_tooltip.near())) { + global_tooltip.close(); + } +}; tooltip.add_builder = function (name, f) { builders[name] = f; }; @@ -3345,10 +3350,7 @@ return { remove: function (id) { const e = $$(id); if (e) { - if (global_tooltip - && e.contains(global_tooltip.near())) { - global_tooltip.close(); - } + hotcrp.tooltip.close_under(e); const b = e.parentElement; e.remove(); if (!b.firstChild) { @@ -3647,10 +3649,7 @@ function display_tracker() { t = tracker_html(dl.tracker); } if (t !== last_tracker_html) { - if (global_tooltip - && mne.contains(global_tooltip.near())) { - global_tooltip.close(); - } + hotcrp.tooltip.close_under(mne); last_tracker_html = mne.innerHTML = t; $(mne).awaken(); if (tracker_has_format) From 60f12dbff857261758b49fa51d9b532a393310d8 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Thu, 11 Apr 2024 12:30:23 -0400 Subject: [PATCH 20/78] Remove unused. --- scripts/graph.js | 2 -- scripts/script.js | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/graph.js b/scripts/graph.js index 3f206faa1..658e5398a 100644 --- a/scripts/graph.js +++ b/scripts/graph.js @@ -1687,10 +1687,8 @@ var graphers = { procrastination: {filter: true, function: procrastination_filter}, scatter: {function: graph_scatter}, cdf: {function: graph_cdf}, - "cumulative-count": {function: graph_cdf}, /* XXX backward compat */ cumulative_count: {function: graph_cdf}, bar: {function: graph_bars}, - "full-stack": {function: graph_bars}, /* XXX backward compat */ full_stack: {function: graph_bars}, box: {function: graph_boxplot} }; diff --git a/scripts/script.js b/scripts/script.js index 5292c400b..12c334d37 100644 --- a/scripts/script.js +++ b/scripts/script.js @@ -10532,7 +10532,7 @@ function Plist(tbl) { this.taghighlighter = false; this.next_foldnum = 8; this._bypid = {}; - var fs = JSON.parse(tbl.getAttribute("data-fields") || tbl.getAttribute("data-columns") /* XXX backward compat */), i; + var fs = JSON.parse(tbl.getAttribute("data-fields")), i; for (i = 0; i !== fs.length; ++i) { this.add_field(fs[i], true); } @@ -14068,7 +14068,6 @@ Object.assign(window.hotcrp, { // monitor_autoassignment // monitor_job // onload - paper_edit_conditions: function () {}, // XXX popup_skeleton: popup_skeleton, // render_list render_text: render_text, From f48af3de51bc8f9d6c282d12e7af6172531b6050 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Thu, 11 Apr 2024 12:33:03 -0400 Subject: [PATCH 21/78] Include tags in JSON download (John Heidemann request). --- src/paperexport.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/paperexport.php b/src/paperexport.php index 30351aa6e..2356630e8 100644 --- a/src/paperexport.php +++ b/src/paperexport.php @@ -185,6 +185,13 @@ function paper_json($prow) { $pj->final_submitted_at = $prow->timeFinalSubmitted; } + if (($tlist = $prow->sorted_viewable_tags($this->user))) { + $pj->tags = []; + foreach (Tagger::split_unpack($tlist) as $tv) { + $pj->tags[] = (object) ["tag" => $tv[0], "value" => $tv[1]]; + } + } + return $pj; } } From faa3219199dc48731dd7fa15d7027c390e281988 Mon Sep 17 00:00:00 2001 From: Tobias Fiebig Date: Fri, 12 Apr 2024 13:23:23 +0200 Subject: [PATCH 22/78] Extended oauth to support importing affiliations and group mappings (#341) * enable import of groups and affiliations from openid * added documentation * added comments from #341 * make deletion of groups configurable * adjusted documentation and catch false-case * fixed typo * fixed typo * adjusted role setting logic * fix removal logic * fix bool capitalization in the documentation * removed state meddling * adjusted group handling --------- Co-authored-by: Tobias Fiebig --- devel/manual/oauth.md | 26 ++++++++++++++++++++++++++ src/pages/p_oauth.php | 18 ++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/devel/manual/oauth.md b/devel/manual/oauth.md index fd21dcb9b..24fb44dda 100644 --- a/devel/manual/oauth.md +++ b/devel/manual/oauth.md @@ -92,6 +92,32 @@ users. If `$Opt["loginType"]` is `"oauth"` or `"none"`, then HotCRP will not use its own password storage or allow attempts to sign in other than through OAuth. +## Importing group permissions + +Group permissions can be imported from an openID provider if the token contains +a "group" claim. This can be configured via a "group mappings" setting in an +oAuthProvider setting: + +``` +$Opt["oAuthProviders"][] = '{ + "name": "Google", + "issuer": "https://accounts.google.com", + "auth_uri": "https://accounts.google.com/o/oauth2/v2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "client_id": "123456789-nnnnnnnnnnnnnnnnnnnnnnnnn.apps.googleusercontent.com", + "client_secret": "GOCSPX-nnnnnnnnnnnnnnnnnnnnnnnn", + "button_html": "Sign in with Google", + "remove_groups": true, + "group_mappings": { + "operators": "sysadmin", + "reviewers": "pc", + "chairs": "chair" + } +}'; +``` + +Setting `remove_groups` to `true` enables removing group permissions if these +are absent in the OpenID claim. [OAuth]: https://en.wikipedia.org/wiki/OAuth [OpenID Connect]: https://en.wikipedia.org/wiki/OpenID diff --git a/src/pages/p_oauth.php b/src/pages/p_oauth.php index ff7750939..1642e079d 100644 --- a/src/pages/p_oauth.php +++ b/src/pages/p_oauth.php @@ -58,6 +58,8 @@ static function find($conf, $name) { $instance->redirect_uri = $authdata->redirect_uri ?? $conf->hoturl("oauth", null, Conf::HOTURL_RAW | Conf::HOTURL_ABSOLUTE); $instance->token_function = $authdata->token_function ?? null; $instance->require = $authdata->require ?? null; + $instance->group_mappings = $authdata->group_mappings ?? null; + $instance->remove_groups = $authdata->remove_groups ?? false; foreach (["title", "issuer", "scope"] as $k) { if ($instance->$k !== null && !is_string($instance->$k)) return null; @@ -242,6 +244,9 @@ private function instance_response($authi, $tok, $tokdata) { if (isset($jid->orcid) && is_string($jid->orcid)) { $reg["orcid"] = $jid->orcid; } + if (isset($jid->affiliation) && is_string($jid->affiliation)) { + $reg["affiliation"] = $jid->affiliation; + } $info = LoginHelper::check_external_login(Contact::make_keyed($this->conf, $reg)); if (!$info["ok"]) { LoginHelper::login_error($this->conf, $jid->email, $info, null); @@ -249,6 +254,19 @@ private function instance_response($authi, $tok, $tokdata) { } $user = $info["user"]; + if (isset($jid->groups) && isset($authi->group_mappings)) { + if ($authi->remove_groups) { + $user_roles = 0; + } else { + $user_roles = $user->roles; + } + foreach ($authi->group_mappings as $group => $role) { + if (in_array($group, $jid->groups, true)) { + $user_roles = $user_roles | UserStatus::parse_roles($role, $user_roles); + } + } + $user->save_roles($user_roles, $user); + } if (!$tokdata->quiet) { $this->conf->feedback_msg(new MessageItem(null, "<0>Signed in", MessageSet::SUCCESS)); } From b4c8d9cfc6812c7b3868dc868d028f24ab35d284 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Sat, 13 Apr 2024 16:15:34 -0400 Subject: [PATCH 23/78] DOM rendering for graph tooltips, I guess. --- scripts/graph.js | 289 +++++++++++++++++++++++++--------------------- scripts/script.js | 11 +- 2 files changed, 164 insertions(+), 136 deletions(-) diff --git a/scripts/graph.js b/scripts/graph.js index 658e5398a..5f0f2fe56 100644 --- a/scripts/graph.js +++ b/scripts/graph.js @@ -1,11 +1,11 @@ // graph.js -- HotCRP JavaScript library for graph drawing // Copyright (c) 2006-2024 Eddie Kohler; see LICENSE. -/* global hotcrp, siteinfo, $$, svge */ +/* global hotcrp, siteinfo, $$, $e, $frag, svge */ /* global hasClass */ /* global append_feedback_near, log_jserror */ /* global make_bubble */ -/* global strftime, text_to_html, escape_html */ +/* global strftime */ hotcrp.graph = (function ($, d3) { var handle_ui = hotcrp.handle_ui, ensure_pattern = hotcrp.ensure_pattern, @@ -395,20 +395,22 @@ function proj2(d) { } function pid_sorter(a, b) { - if (typeof a === "object") + if (typeof a === "object") { a = a.id || a[2]; - if (typeof b === "object") + } + if (typeof b === "object") { b = b.id || b[2]; - var d = (typeof a === "string" ? parseInt(a, 10) : a) - + } + const d = (typeof a === "string" ? parseInt(a, 10) : a) - (typeof b === "string" ? parseInt(b, 10) : b); return d ? d : (a < b ? -1 : (a == b ? 0 : 1)); } -function pid_renderer(ps, cc) { +function render_pid_p(ps, cc) { ps.sort(pid_sorter); - var a = []; - for (var i = 0; i !== ps.length; ++i) { - var p = ps[i], cx = cc, rest = null; + const e = $e("p"); + for (let i = 0; i !== ps.length; ++i) { + let p = ps[i], cx = cc, rest = ""; if (typeof p === "object") { if (p.id) { rest = p.rest; @@ -419,20 +421,21 @@ function pid_renderer(ps, cc) { p = p[2]; } } + const comma = i === ps.length - 1 ? "" : ","; + let pe = "#" + p; if (cx) { ensure_pattern(cx); - p = '#', p, ''); - } else - p = '#' + p; - var comma = i === ps.length - 1 ? "" : "," - if (rest) - a.push(''.concat(p, rest, comma, '')); - else if (cx && comma) - a.push(''.concat(p, comma, '')); - else - a.push(p + comma); + pe = $e("span", cx, pe); + } + i > 0 && e.append(" "); + if (rest || cx) { + e.append($e("span", "nw", pe, rest, comma)); + } else { + e.append(pe + comma); + } } - return a.join(" "); + e.normalize(); + return e; } function clicker(pids, event) { @@ -487,12 +490,14 @@ function make_axis(ticks) { return $.extend({ prepare: function () {}, rewrite: function () {}, - unparse_html: function (value) { - if (value == Math.floor(value)) - return value; - var dom = this.scale().domain(), - dig = Math.max(0, -Math.round(Math.log10(dom[1] - dom[0])) + 2); - return value.toFixed(dig); + render_onto: function (e, value) { + if (value == Math.floor(value)) { + e.append(value); + } else { + const dom = this.scale().domain(), + dig = Math.max(0, -Math.round(Math.log10(dom[1] - dom[0])) + 2); + e.append(value.toFixed(dig)); + } }, search: function () { return null; } }, ticks); @@ -517,11 +522,13 @@ function make_args(selector, args) { return args; } -function position_label(axis, p, prefix) { - var aa = axis.axis_args, t = '' + (prefix || ""); - if (aa.label) - t += escape_html(aa.label) + " "; - return t + aa.ticks.unparse_html.call(axis, p, true) + ''; +function render_position(axis, p, prefix) { + const e = $e("span", "nw"), aa = axis.axis_args; + if (prefix || aa.label) { + e.append((prefix || "") + (aa.label ? aa.label + " " : "")); + } + aa.ticks.render_onto.call(axis, e, p, true); + return e; } @@ -626,13 +633,15 @@ function graph_cdf(selector, args) { hubble = hubble || make_bubble("", {color: args.tooltip_class || "graphtip", "pointer-events": "none"}); var dir = Math.abs(tangentAngle(p.pathNode, p.pathLength)); if (args.cdf_tooltip_position) { - var label = (hovered_series.label ? text_to_html(hovered_series.label) + " " : "") + - args.x.ticks.unparse_html.call(xAxis, x.invert(p[0]), true) + - ", " + - args.y.ticks.unparse_html.call(yAxis, y.invert(p[1]), true); - hubble.html(label); - } else + const f = $frag(); + hovered_series.label && f.append(hovered_series.label + " "); + args.x.ticks.render_onto.call(xAxis, f, x.invert(p[0]), true); + f.append(", "); + args.y.ticks.render_onto.call(yAxis, f, y.invert(p[1]), true); + hubble.replace_content(f); + } else { hubble.text(hovered_series.label); + } hubble.anchor(dir >= 0.25*Math.PI && dir <= 0.75*Math.PI ? "e" : "s") .at(p[0] + args.left, p[1], this); } else if (hubble) { @@ -919,13 +928,19 @@ function scatter_highlight(svg, data, klass) { } function scatter_union(p) { - if (p.head) + if (!p) { + return null; + } + if (p.head) { p = p.head; - if (!p.next) + } + if (!p.next) { return p; + } if (!p.union) { - var u = [p[0], p[1], [].concat(p[2]), p[3]], pp = p.next; + const u = [p[0], p[1], [].concat(p[2]), p[3]]; u.r = p.r; + let pp = p.next; while (pp) { u.r = Math.max(u.r, pp.r); Array.prototype.push.apply(u[2], pp[2]); @@ -975,40 +990,38 @@ function graph_scatter(selector, args) { .on("click", mouseclick); function make_tooltip(p, ps) { - return '

' + position_label(xAxis, p[0]) + ', ' + - position_label(yAxis, p[1]) + '

' + - pid_renderer(ps, p[3]) + '

'; + return [ + $e("p", null, render_position(xAxis, p[0]), ", ", render_position(yAxis, p[1])), + render_pid_p(ps, p[3]) + ]; } var hovered_data, hubble; function mousemoved(event) { - var m = d3.pointer(event), p = data.quadtree.gfind(m, 4); - if (p && (p.head || p.next)) - p = scatter_union(p); - if (p != hovered_data) { - if (p) - hovers.datum(p) - .attr("d", scatter_annulus) - .attr("transform", scatter_transform) - .style("display", null); - else - hovers.style("display", "none"); - svg.style("cursor", p ? "pointer" : null); - hovered_data = p; + let m = d3.pointer(event), p = scatter_union(data.quadtree.gfind(m, 4)); + if (!p) { + hovered_data && mouseout(); + return; + } else if (p === hovered_data) { + return; } - if (p) { - hubble = hubble || make_bubble("", {color: "graphtip", "pointer-events": "none"}); - hubble.html(make_tooltip(p[2][0], p[2])) - .anchor("s") - .near(hovers.node()); - } else if (hubble) - hubble = hubble.remove() && null; + hovers.datum(p) + .attr("d", scatter_annulus) + .attr("transform", scatter_transform) + .style("display", null); + hubble = hubble || make_bubble("", {color: "graphtip", "pointer-events": "none"}); + hubble.replace_content(...make_tooltip(p[2][0], p[2])) + .anchor("s") + .near(hovers.node()); + svg.style("cursor", "pointer"); + hovered_data = p; } function mouseout() { hovers.style("display", "none"); hubble && hubble.remove(); hovered_data = hubble = null; + svg.style("cursor", null); } function mouseclick(event) { @@ -1148,22 +1161,25 @@ function graph_bars(selector, args) { .on("click", mouseclick); function make_tooltip(p) { - return '

' + position_label(xAxis, p[0]) + ', ' + - position_label(yAxis, p[1]) + '

' + - pid_renderer(p[2], p[3]) + '

'; + return [ + $e("p", null, render_position(xAxis, p[0]), ", ", render_position(yAxis, p[1])), + render_pid_p(p[2], p[3]) + ]; } function make_mouseover(d) { - if (!d || d.i0 == null) + if (!d || d.i0 == null) { return d; + } if (!d.ia) { d.ia = [d[0], 0, [], "", d[4]]; d.ia.yoff = 0; - for (var i = d.i0; i !== data.length && data[i].i0 === d.i0; ++i) { + for (let i = d.i0; i !== data.length && data[i].i0 === d.i0; ++i) { d.ia[1] = data[i][1] + data[i].yoff; - var pids = data[i][2], cc = data[i][3]; - for (var j = 0; j !== pids.length; ++j) + const pids = data[i][2], cc = data[i][3]; + for (let j = 0; j !== pids.length; ++j) { d.ia[2].push(cc ? {id: pids[j], color_classes: cc} : pids[j]); + } } } return d.ia; @@ -1171,25 +1187,27 @@ function graph_bars(selector, args) { var hovered_data, hubble; function mouseover() { - var p = make_mouseover(d3.select(this).data()[0]); - if (p != hovered_data) { - if (p) - place(hovers.datum(p), "Z").style("display", null); - else - hovers.style("display", "none"); - svg.style("cursor", p ? "pointer" : null); - hovered_data = p; - } - if (p) { - hubble = hubble || make_bubble("", {color: "graphtip", "pointer-events": "none"}); - hubble.html(make_tooltip(p)).anchor("h").near(hovers.node()); + const p = make_mouseover(d3.select(this).data()[0]); + if (!p) { + hovered_data && mouseout(); + return; + } else if (p === hovered_data) { + return; } + place(hovers.datum(p), "Z").style("display", null); + svg.style("cursor", "pointer"); + hubble = hubble || make_bubble("", {color: "graphtip", "pointer-events": "none"}); + hubble.replace_content(...make_tooltip(p)) + .anchor("h") + .near(hovers.node()); + hovered_data = p; } function mouseout() { hovers.style("display", "none"); hubble && hubble.remove(); hovered_data = hubble = null; + svg.style("cursor", null); } function mouseclick(event) { @@ -1374,42 +1392,45 @@ function graph_boxplot(selector, args) { }, false); function make_tooltip(p, ps, ds, cc) { - var yformat = args.y.ticks.unparse_html, t, x = []; - t = '

' + position_label(xAxis, p[0]); + const yformat = args.y.ticks.render_onto, pe = $e("p"); + let x = ps; + pe.append(render_position(xAxis, p[0])); if (p.q) { - t += ", " + position_label(yAxis, p.q[2], "median "); - for (var i = 0; i < ps.length; ++i) - x.push({id: ps[i], rest: " (" + yformat.call(yAxis, ds[i]) + ")"}); + pe.append(", ", render_position(yAxis, p.q[2], "median ")); + x = []; + for (let i = 0; i < ps.length; ++i) { + const rest = $frag(" ("); + yformat.call(yAxis, rest, ds[i]); + rest.append(")"); + x.push({id: ps[i], rest: rest}); + } } else { - t += ", " + position_label(yAxis, ds[0]); - x = ps; + pe.append(", ", render_position(yAxis, ds[0])); } - x.sort(pid_sorter); - return t + '

' + pid_renderer(x, cc) + '

'; + return [pe, render_pid_p(x, cc)]; } var hovered_data, hubble; function mouseover() { - var p = d3.select(this).data()[0]; - if (p != hovered_data) { - hovers.style("display", "none"); - if (p) { - hovers.filter(":not(.outlier)").style("display", null).datum(p); - place_whisker(0, hovers.filter(".whiskerl")); - place_whisker(3, hovers.filter(".whiskerh")); - place_box(hovers.filter(".box")); - place_median(hovers.filter(".median")); - place_mean(hovers.filter(".mean")); - } - svg.style("cursor", p ? "pointer" : null); - hovered_data = p; - } - if (p) { - hubble = hubble || make_bubble("", {color: "graphtip", "pointer-events": "none"}); - if (!p.th) - p.th = make_tooltip(p, p.p, p.d, p.c); - hubble.html(p.th).anchor("h").near(hovers.filter(".box").node()); + let p = d3.select(this).data()[0]; + if (!p) { + hovered_data && mouseout(); + return; + } else if (p === hovered_data) { + return; } + hovers.filter(":not(.outlier)").style("display", null).datum(p); + place_whisker(0, hovers.filter(".whiskerl")); + place_whisker(3, hovers.filter(".whiskerh")); + place_box(hovers.filter(".box")); + place_median(hovers.filter(".median")); + place_mean(hovers.filter(".mean")); + svg.style("cursor", "pointer"); + hovered_data = p; + hubble = hubble || make_bubble("", {color: "graphtip", "pointer-events": "none"}); + hubble.replace_content(...make_tooltip(p, p.p, p.d, p.c)) + .anchor("h") + .near(hovers.filter(".box").node()); } function mouseover_outlier() { @@ -1433,6 +1454,7 @@ function graph_boxplot(selector, args) { hovers.style("display", "none"); hubble && hubble.remove(); hovered_data = hubble = null; + svg.style("cursor", null); } function mouseclick(event) { @@ -1490,16 +1512,15 @@ function score_ticks(rf) { d.text(rf.unparse_symbol(value, split)); }); }, - unparse_html: function (value, include_numeric) { - var k = rf.className(value), t = rf.unparse_symbol(value, true); - if (!k) - return t; - t = '', t, ''); + render_onto: function (e, value, include_numeric) { + const k = rf.className(value), + t = rf.unparse_symbol(value, true); + e.append(k ? $e("span", "sv " + k, t) : t); if (include_numeric && !rf.default_numeric - && value !== Math.round(value * 2) / 2) - t = t.concat(' (', value.toFixed(2).replace(/\.00$/, ""), ')'); - return t; + && value !== Math.round(value * 2) / 2) { + e.append(" (" + value.toFixed(2).replace(/\.00$/, "") + ")"); + } }, type: "score" }; @@ -1510,12 +1531,12 @@ function time_ticks() { if (value < 1000000000) { value = Math.round(value / 8640) / 10; return value + "d"; + } + const d = new Date(value * 1000); + if (d.getHours() || d.getMinutes()) { + return strftime("%Y-%m-%dT%R", d); } else { - var d = new Date(value * 1000); - if (d.getHours() || d.getMinutes()) - return strftime("%Y-%m-%dT%R", d); - else - return strftime("%Y-%m-%d", d); + return strftime("%Y-%m-%d", d); } } return { @@ -1536,7 +1557,9 @@ function time_ticks() { } this.tickFormat(format); }, - unparse_html: format, + render_onto: function (e, value) { + e.append(format(value)); + }, type: "time" }; } @@ -1638,16 +1661,16 @@ function named_integer_ticks(map) { this.ticks(count).tickFormat(mtext); }, rewrite: rewrite, - unparse_html: function (value, include_numeric) { - var fvalue = Math.round(value); + render_onto: function (e, value, include_numeric) { + const fvalue = Math.round(value); if (Math.abs(value - fvalue) <= 0.05 && map[fvalue]) { - var t = text_to_html(mtext(fvalue)); - // NB `value` might be a bool - if (value !== fvalue && include_numeric && typeof value === "number") - t += " (" + value.toFixed(2) + ")"; - return t; - } else - return value.toFixed(2); + e.append(mtext(fvalue)); + if (include_numeric + && value !== fvalue + && typeof value === "number") { + e.append(" (" + value.toFixed(2) + ")"); + } + } }, search: function (value) { var m = map[value]; diff --git a/scripts/script.js b/scripts/script.js index 12c334d37..1a9f8acbb 100644 --- a/scripts/script.js +++ b/scripts/script.js @@ -2689,14 +2689,19 @@ return function (content, bubopt) { return bubble; }, text: function (text) { - if (text === undefined) + if (text === undefined) { return $(bubch[1]).text(); - else - return bubble.html(text ? text_to_html(text) : text); + } + return bubble.replace_content(text); }, content_node: function () { return bubch[1]; }, + replace_content: function (...es) { + bubch[1].replaceChildren(...es); + nearpos && show(); + return bubble; + }, hover: function (enter, leave) { $(bubdiv).hover(enter, leave); return bubble; From 7bac899cb85a467f77af6395d235f5d64014af04 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Sat, 13 Apr 2024 16:32:54 -0400 Subject: [PATCH 24/78] Outlier boxplot fix --- scripts/graph.js | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/scripts/graph.js b/scripts/graph.js index 5f0f2fe56..e04ac0380 100644 --- a/scripts/graph.js +++ b/scripts/graph.js @@ -1419,6 +1419,7 @@ function graph_boxplot(selector, args) { } else if (p === hovered_data) { return; } + hovers.style("display", "none"); hovers.filter(":not(.outlier)").style("display", null).datum(p); place_whisker(0, hovers.filter(".whiskerl")); place_whisker(3, hovers.filter(".whiskerh")); @@ -1434,20 +1435,21 @@ function graph_boxplot(selector, args) { } function mouseover_outlier() { - var p = d3.select(this).data()[0]; - if (p != hovered_data) { - hovers.style("display", "none"); - if (p) - place_outlier(hovers.filter(".outlier").style("display", null).datum(p)); - svg.style("cursor", p ? "pointer" : null); - hovered_data = p; - } - if (p) { - hubble = hubble || make_bubble("", {color: "graphtip", "pointer-events": "none"}); - if (!p.th) - p.th = make_tooltip(p[2][0], p[2].map(proj2), p[2].map(proj1), p[3]); - hubble.html(p.th).anchor("h").near(hovers.filter(".outlier").node()); + let p = d3.select(this).data()[0]; + if (!p) { + hovered_data && mouseout(); + return; + } else if (p === hovered_data) { + return; } + hovers.style("display", "none"); + place_outlier(hovers.filter(".outlier").style("display", null).datum(p)); + svg.style("cursor", "pointer"); + hovered_data = p; + hubble = hubble || make_bubble("", {color: "graphtip", "pointer-events": "none"}); + hubble.replace_content(...make_tooltip(p[2][0], p[2].map(proj2), p[2].map(proj1), p[3])) + .anchor("h") + .near(hovers.filter(".outlier").node()); } function mouseout() { From e96decdd3e8d162216853fda0e582fc790c2890f Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 17 Apr 2024 10:31:19 -0400 Subject: [PATCH 25/78] checkinvariants: Add --fix whitespace. --- batch/checkinvariants.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/batch/checkinvariants.php b/batch/checkinvariants.php index 52181b7e7..8ea9a9fbf 100644 --- a/batch/checkinvariants.php +++ b/batch/checkinvariants.php @@ -141,6 +141,10 @@ function run() { $this->report_fix("document match"); $this->fix_document_match(); } + if (isset($ic->problems["user_whitespace"]) && $this->want_fix("whitespace")) { + $this->report_fix("whitespace"); + $this->fix_whitespace(); + } return 0; } @@ -171,6 +175,20 @@ private function fix_document_match() { $this->conf->qe("update Paper p join PaperStorage s on (s.paperId=p.paperId and s.paperStorageId=p.finalPaperStorageId) set p.size=s.size where p.size<0 and p.finalPaperStorageId>1"); } + private function fix_whitespace() { + $result = $this->conf->qe("select * from Contact"); + $mq = Dbl::make_multi_qe_stager($this->conf->dblink); + while (($u = $result->fetch_object())) { + $fn = simplify_whitespace($u->firstName); + $ln = simplify_whitespace($u->lastName); + $af = simplify_whitespace($u->affiliation); + if ($fn !== $u->firstName || $ln !== $u->lastName || $af !== $u->affiliation) { + $mq("update Contact set firstName=?, lastName=?, affiliation=? where contactId=?", $fn, $ln, $af, $u->contactId); + } + } + $mq(null); + } + /** @return CheckInvariants_Batch */ static function make_args($argv) { $arg = (new Getopt)->long( @@ -180,7 +198,7 @@ static function make_args($argv) { "verbose,V Be verbose", "fix-autosearch ! Repair any incorrect autosearch tags", "fix-inactive ! Repair any inappropriately inactive documents", - "fix[] =PROBLEM Repair PROBLEM [all, autosearch, inactive, setting, document-match]", + "fix[] =PROBLEM Repair PROBLEM [all, autosearch, inactive, setting, document-match, whitespace]", "color", "no-color !", "pad-prefix !" From 2072e3d46d4ea5f7cd07e890d277676425ca698b Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 17 Apr 2024 10:32:16 -0400 Subject: [PATCH 26/78] Dederp --- batch/checkinvariants.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/batch/checkinvariants.php b/batch/checkinvariants.php index 8ea9a9fbf..cee9b0ec9 100644 --- a/batch/checkinvariants.php +++ b/batch/checkinvariants.php @@ -176,14 +176,14 @@ private function fix_document_match() { } private function fix_whitespace() { - $result = $this->conf->qe("select * from Contact"); + $result = $this->conf->qe("select * from ContactInfo"); $mq = Dbl::make_multi_qe_stager($this->conf->dblink); while (($u = $result->fetch_object())) { $fn = simplify_whitespace($u->firstName); $ln = simplify_whitespace($u->lastName); $af = simplify_whitespace($u->affiliation); if ($fn !== $u->firstName || $ln !== $u->lastName || $af !== $u->affiliation) { - $mq("update Contact set firstName=?, lastName=?, affiliation=? where contactId=?", $fn, $ln, $af, $u->contactId); + $mq("update ContactInfo set firstName=?, lastName=?, affiliation=? where contactId=?", $fn, $ln, $af, $u->contactId); } } $mq(null); From 42111c807fdd4a8c3b1fc98968ba07297920d4fd Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 17 Apr 2024 10:35:26 -0400 Subject: [PATCH 27/78] Also update unaccentedName. --- batch/checkinvariants.php | 7 +++++-- src/confinvariants.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/batch/checkinvariants.php b/batch/checkinvariants.php index cee9b0ec9..31e5b4790 100644 --- a/batch/checkinvariants.php +++ b/batch/checkinvariants.php @@ -178,12 +178,15 @@ private function fix_document_match() { private function fix_whitespace() { $result = $this->conf->qe("select * from ContactInfo"); $mq = Dbl::make_multi_qe_stager($this->conf->dblink); - while (($u = $result->fetch_object())) { + while (($u = Contact::fetch($result, $this->conf))) { $fn = simplify_whitespace($u->firstName); $ln = simplify_whitespace($u->lastName); $af = simplify_whitespace($u->affiliation); if ($fn !== $u->firstName || $ln !== $u->lastName || $af !== $u->affiliation) { - $mq("update ContactInfo set firstName=?, lastName=?, affiliation=? where contactId=?", $fn, $ln, $af, $u->contactId); + $u->firstName = $fn; + $u->lastName = $ln; + $u->affiliation = $af; + $mq("update ContactInfo set firstName=?, lastName=?, affiliation=?, unaccentedName=? where contactId=?", $fn, $ln, $af, $u->db_searchable_name(), $u->contactId); } } $mq(null); diff --git a/src/confinvariants.php b/src/confinvariants.php index 1a66fe738..a69edadb0 100644 --- a/src/confinvariants.php +++ b/src/confinvariants.php @@ -442,7 +442,7 @@ function check_users() { // whitespace is simplified $t = " "; - foreach ([$u->firstName, $u->lastName, $u->email, $u->affiliation] as $s) { + foreach ([$u->firstName, $u->lastName, $u->email, $u->affiliation, $u->unaccentedName] as $s) { if ($s !== "") $t .= "{$s} "; } From 8a73b16d553dae0334ca17c7ff54d96507215e5b Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 17 Apr 2024 10:36:09 -0400 Subject: [PATCH 28/78] Derp --- src/confinvariants.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/confinvariants.php b/src/confinvariants.php index a69edadb0..05064fe15 100644 --- a/src/confinvariants.php +++ b/src/confinvariants.php @@ -417,7 +417,7 @@ function check_users() { // load users $primary = []; - $result = $this->conf->qe("select " . $this->conf->user_query_fields() . " from ContactInfo"); + $result = $this->conf->qe("select " . $this->conf->user_query_fields() . ", unaccentedName from ContactInfo"); while (($u = $result->fetch_object())) { $u->contactId = intval($u->contactId); $u->primaryContactId = intval($u->primaryContactId); From 5f86169df908a581e82fcf9ffc4acbcfe7198de5 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 17 Apr 2024 10:38:34 -0400 Subject: [PATCH 29/78] Derp --- batch/checkinvariants.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/batch/checkinvariants.php b/batch/checkinvariants.php index 31e5b4790..c21763bc7 100644 --- a/batch/checkinvariants.php +++ b/batch/checkinvariants.php @@ -179,14 +179,14 @@ private function fix_whitespace() { $result = $this->conf->qe("select * from ContactInfo"); $mq = Dbl::make_multi_qe_stager($this->conf->dblink); while (($u = Contact::fetch($result, $this->conf))) { - $fn = simplify_whitespace($u->firstName); - $ln = simplify_whitespace($u->lastName); - $af = simplify_whitespace($u->affiliation); - if ($fn !== $u->firstName || $ln !== $u->lastName || $af !== $u->affiliation) { - $u->firstName = $fn; - $u->lastName = $ln; - $u->affiliation = $af; - $mq("update ContactInfo set firstName=?, lastName=?, affiliation=?, unaccentedName=? where contactId=?", $fn, $ln, $af, $u->db_searchable_name(), $u->contactId); + $u->firstName = simplify_whitespace(($fn = $u->firstName)); + $u->lastName = simplify_whitespace(($ln = $u->lastName)); + $u->affiliation = simplify_whitespace(($af = $u->affiliation)); + $un = $u->unaccentedName; + $u->unaccentedName = $u->db_searchable_name(); + if ($fn !== $u->firstName || $ln !== $u->lastName + || $af !== $u->affiliation || $un !== $u->unaccentedName) { + $mq("update ContactInfo set firstName=?, lastName=?, affiliation=?, unaccentedName=? where contactId=?", $u->firstName, $u->lastName, $u->affiliation, $u->unaccentedName, $u->contactId); } } $mq(null); From ecfc12f5b0d7f603f2f441b3f3a5350dda23331a Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Fri, 12 Apr 2024 07:18:53 -0400 Subject: [PATCH 30/78] Tweaklet. --- src/userstatus.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/userstatus.php b/src/userstatus.php index 82d999a03..1976b1b36 100644 --- a/src/userstatus.php +++ b/src/userstatus.php @@ -716,7 +716,7 @@ static function parse_roles($j, $old_roles, $ms = null) { return $old_roles; } else if ($v !== "") { $action = null; - if (preg_match('/\A(\+|-|–|—|−)\s*(.*)\z/', $v, $m)) { + if (preg_match('/\A(\+|-|–|—|−)\s*(.*)\z/s', $v, $m)) { $action = $m[1] === "+"; $v = $m[2]; } From 19717db7fdb95700f8425e2b17ca4a96972b61e1 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 17 Apr 2024 10:55:28 -0400 Subject: [PATCH 31/78] Add support for Observable10 categorical color scheme. --- batch/checkinvariants.php | 2 +- etc/reviewfieldlibrary.json | 2 +- etc/settinginfo.json | 2 +- scripts/script.js | 2 +- scripts/settings.js | 3 ++- src/pages/p_scorechart.php | 4 ++-- src/reviewfield.php | 3 ++- stylesheets/style.css | 10 ++++++++++ 8 files changed, 20 insertions(+), 8 deletions(-) diff --git a/batch/checkinvariants.php b/batch/checkinvariants.php index c21763bc7..9da1dd6c5 100644 --- a/batch/checkinvariants.php +++ b/batch/checkinvariants.php @@ -1,6 +1,6 @@ "9c3131a04b00a26300a179009d8f00929e007fad005fbd0000cc00", "bupu" => "4b8bc14181be3b76bb396bb73b5fb24053ab4646a34d389a54278f", "pkrd" => "e14da0d7448bcc3b76c13363b52b50a9243e9c1e2c8f1819821201", "viridis" => "440154472c7a3b518b2c718e21908d27ad815cc863aadc32dbcb39", "orbu" => "fca636f68443e86659d14d6fb23a818e2c8f6721963e15940d0887", "turbo" => "23171b4569ee26bce13ff3936be619ecd12eff821dcb2f0d900c00", "catx" => "1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf", "none" => "222222"]; - public static $scheme_categorical = ["catx" => true, "none" => true]; + public static $scheme_colors = ["sv" => "9c3131a04b00a26300a179009d8f00929e007fad005fbd0000cc00", "bupu" => "4b8bc14181be3b76bb396bb73b5fb24053ab4646a34d389a54278f", "pkrd" => "e14da0d7448bcc3b76c13363b52b50a9243e9c1e2c8f1819821201", "viridis" => "440154472c7a3b518b2c718e21908d27ad815cc863aadc32dbcb39", "orbu" => "fca636f68443e86659d14d6fb23a818e2c8f6721963e15940d0887", "turbo" => "23171b4569ee26bce13ff3936be619ecd12eff821dcb2f0d900c00", "observablex" => "4269d0efb118ff725c6cc5b03ca951ff8ab7a463f297bbf59c6b4e9498a0", "catx" => "1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf", "none" => "222222"]; + public static $scheme_categorical = ["observablex" => true, "catx" => true, "none" => true]; public static $scheme_reverse = ["sv" => "svr", "svr" => "sv", "bupu" => "pubu", "pubu" => "bupu", "rdpk" => "pkrd", "pkrd" => "rdpk", "viridisr" => "viridis", "viridis" => "viridisr", "orbu" => "buor", "buor" => "orbu", "turbo" => "turbor", "turbor" => "turbo"]; /** @param array{int,int,int} $c1 diff --git a/src/reviewfield.php b/src/reviewfield.php index 12ad39f84..ac1b285d0 100644 --- a/src/reviewfield.php +++ b/src/reviewfield.php @@ -564,7 +564,8 @@ abstract class Discrete_ReviewField extends ReviewField { "viridisr" => [1, 9, "viridis"], "viridis" => [0, 9, "viridisr"], "orbu" => [0, 9, "buor"], "buor" => [1, 9, "orbu"], "turbo" => [0, 9, "turbor"], "turbor" => [1, 9, "turbo"], - "catx" => [2, 10, null], "none" => [2, 1, null] + "observablex" => [2, 10, null], "catx" => [2, 10, null], + "none" => [2, 1, null] ]; /** @var array diff --git a/stylesheets/style.css b/stylesheets/style.css index 2c025832d..10d7efe32 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -5166,6 +5166,16 @@ div.sc { .sv-catx8 { color: #7f7f7f; } .sv-catx9 { color: #bcbd22; } .sv-catx10 { color: #17becf; } +.sv-observablex1 { color: #4269d0; } +.sv-observablex2 { color: #efb118; } +.sv-observablex3 { color: #ff725c; } +.sv-observablex4 { color: #6cc5b0; } +.sv-observablex5 { color: #3ca951; } +.sv-observablex6 { color: #ff8ab7; } +.sv-observablex7 { color: #a463f2; } +.sv-observablex8 { color: #97bbf5; } +.sv-observablex9 { color: #9c6b4e; } +.sv-observablex10 { color: #9498a0; } .sv-orbu1 { color: #fca636; } .sv-orbu2 { color: #f68443; } .sv-orbu3 { color: #e86659; } From 92fd7a206664ff46d8a215d7a28409451cfc567c Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 17 Apr 2024 12:12:57 -0400 Subject: [PATCH 32/78] Formulas: re:words infers the review index. Same results in general, but better performance (infers a for loop over all *reviews*, not all *PC reviewers*). --- src/formulas/f_reviewwordcount.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/formulas/f_reviewwordcount.php b/src/formulas/f_reviewwordcount.php index bdbb7f467..8ed79f080 100644 --- a/src/formulas/f_reviewwordcount.php +++ b/src/formulas/f_reviewwordcount.php @@ -1,13 +1,13 @@ is_reviewer(); From 9ea2beb9868de3bcf1773acf7d494d250c9b324b Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 17 Apr 2024 12:13:51 -0400 Subject: [PATCH 33/78] Formulas: An unknown fexpr is usually nullable. Only if there *is* an inferred format should we infer nullability. --- src/formula.php | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/formula.php b/src/formula.php index 94ae5fe74..74771cea5 100644 --- a/src/formula.php +++ b/src/formula.php @@ -168,7 +168,7 @@ function math_format() { /** @return bool */ function nonnullable_format() { return $this->_format === Fexpr::FNUMERIC - || $this->_format === Fexpr::FBOOL; + || $this->_format === Fexpr::FBOOL; } /** @return bool */ @@ -234,19 +234,23 @@ function typecheck_arguments(Formula $formula, $ismath = false) { private function _typecheck_format() { $commonf = null; - $nonnull = true; - foreach ($this->inferred_format() ?? [] as $fe) { - $nonnull = $nonnull && $fe->nonnull_format(); - if ($fe->format() < Fexpr::FNUMERIC) { - /* ignore it */ - } else if (!$commonf) { - $commonf = $fe; - } else if ($commonf->_format !== $fe->_format - || (!$commonf->nonnullable_format() - && $commonf->_format_detail !== $fe->_format_detail)) { - $commonf = null; - break; + if (($inferred = $this->inferred_format())) { + $nonnull = true; + foreach ($inferred as $fe) { + $nonnull = $nonnull && $fe->nonnull_format(); + if ($fe->format() < Fexpr::FNUMERIC) { + /* ignore it */ + } else if (!$commonf) { + $commonf = $fe; + } else if ($commonf->_format !== $fe->_format + || (!$commonf->nonnullable_format() + && $commonf->_format_detail !== $fe->_format_detail)) { + $commonf = null; + break; + } } + } else { + $nonnull = false; } if ($this->_format === Fexpr::FUNKNOWN) { $this->_format = $commonf ? $commonf->_format : Fexpr::FNUMERIC; From 41c3e384a9e4b6f40220303e5fdd412bc9e4b185 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 17 Apr 2024 12:23:37 -0400 Subject: [PATCH 34/78] Formulas: Improve inference of non-null values. --- src/formula.php | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/formula.php b/src/formula.php index 74771cea5..90ebfb9be 100644 --- a/src/formula.php +++ b/src/formula.php @@ -234,23 +234,20 @@ function typecheck_arguments(Formula $formula, $ismath = false) { private function _typecheck_format() { $commonf = null; - if (($inferred = $this->inferred_format())) { - $nonnull = true; - foreach ($inferred as $fe) { - $nonnull = $nonnull && $fe->nonnull_format(); - if ($fe->format() < Fexpr::FNUMERIC) { - /* ignore it */ - } else if (!$commonf) { - $commonf = $fe; - } else if ($commonf->_format !== $fe->_format - || (!$commonf->nonnullable_format() - && $commonf->_format_detail !== $fe->_format_detail)) { - $commonf = null; - break; - } + $inferred = $this->inferred_format(); + $nonnull = !empty($inferred); + foreach ($inferred ?? [] as $fe) { + $nonnull = $nonnull && $fe->nonnull_format(); + if ($fe->format() < Fexpr::FNUMERIC) { + /* ignore it */ + } else if (!$commonf) { + $commonf = $fe; + } else if ($commonf->_format !== $fe->_format + || (!$commonf->nonnullable_format() + && $commonf->_format_detail !== $fe->_format_detail)) { + $commonf = null; + break; } - } else { - $nonnull = false; } if ($this->_format === Fexpr::FUNKNOWN) { $this->_format = $commonf ? $commonf->_format : Fexpr::FNUMERIC; @@ -633,7 +630,7 @@ function inferred_format() { if ((!$d0 && !$d1) || (!$d0 && $f0) || (!$d1 && $f1)) { - return null; + return $this->args; } else if ($this->op === "-" && $d0 && $d1 && !$delta0 && !$delta1) { $fx = Fexpr::FDATEDELTA; } else if ($d0 && (!$d1 || $delta1)) { @@ -641,7 +638,7 @@ function inferred_format() { } else if ($d1 && (!$d0 || $delta0)) { $fx = $delta1 ? Fexpr::FDATEDELTA : Fexpr::FDATE; } else { - return null; + return $this->args; } if ($f0 === Fexpr::FTIME || $f0 === Fexpr::FTIMEDELTA || $f1 === Fexpr::FTIME || $f1 === Fexpr::FTIMEDELTA) { @@ -665,6 +662,9 @@ function __construct($op, $e1, $e2) { function typecheck(Formula $formula) { return $this->typecheck_arguments($formula, true); } + function inferred_format() { + return $this->args; + } function compile(FormulaCompiler $state) { $t1 = $state->_addltemp($this->args[0]->compile($state)); $t2 = $state->_addltemp($this->args[1]->compile($state)); @@ -1052,7 +1052,8 @@ function inferred_format() { } function compile(FormulaCompiler $state) { $cmp = $this->compiled_relation($this->op === "min" ? "<" : ">"); - return $state->_compile_loop("null", "(~r~ === null || (~l~ !== null && ~l~ {$cmp} ~r~) ? ~l~ : ~r~)", $this); + $cmpx = $this->args[0]->nonnull_format() ? "~l~ {$cmp} ~r~" : "(~l~ !== null && ~l~ {$cmp} ~r~)"; + return $state->_compile_loop("null", "(~r~ === null || {$cmpx} ? ~l~ : ~r~)", $this); } } @@ -1096,7 +1097,11 @@ function typecheck(Formula $formula) { && Count_Fexpr::check_private_tag_index($this); } function compile(FormulaCompiler $state) { - return $state->_compile_loop("null", "(~l~ !== null ? (~r~ !== null ? ~r~ + ~l~ : +~l~) : ~r~)", $this); + if ($this->args[0]->nonnull_format()) { + return $state->_compile_loop("null", "(~r~ !== null ? ~r~ + ~l~ : +~l~)", $this); + } else { + return $state->_compile_loop("null", "(~l~ !== null ? (~r~ !== null ? ~r~ + ~l~ : +~l~) : ~r~)", $this); + } } } From 4674fcfbb76511072a1145dad620756fc1d4b4e9 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 17 Apr 2024 12:24:32 -0400 Subject: [PATCH 35/78] Formulas: Output error_log if DEBUG === 1. --- src/formula.php | 86 +++++++++++++++++++++++++++---------------------- 1 file changed, 48 insertions(+), 38 deletions(-) diff --git a/src/formula.php b/src/formula.php index 90ebfb9be..9d115db11 100644 --- a/src/formula.php +++ b/src/formula.php @@ -1775,8 +1775,8 @@ class Formula implements JsonSerializable { const BINARY_OPERATOR_REGEX = '/\A(?:[-\+\/%^]|\*\*?|\&\&?|\|\|?|\?\?|==?|!=|<[<=]?|>[>=]?|≤|≥|≠)/'; - /** @var bool */ - const DEBUG = false; + /** @var 0|1|2 */ + const DEBUG = 1; static public $opprec = [ "**" => 14, @@ -2587,6 +2587,14 @@ private function _parse_ternary(&$t, $in_qc) { } + private static function debug_report($function) { + if (self::DEBUG === 1) { + error_log("{$function}\n"); + } else if (self::DEBUG > 1) { + Conf::msg_debugt("{$function}\n"); + } + } + /** @param Contact $user * @param string $expr * @param int $sortable @@ -2621,14 +2629,15 @@ private function _compile_function($sortable) { if ($this->check()) { $state = new FormulaCompiler($this->user); $expr = $this->_parse->fexpr->compile($state); - $t = self::compile_body($this->user, $state, $expr, $sortable); + $body = self::compile_body($this->user, $state, $expr, $sortable); } else { - $t = "return null;\n"; + $body = "return null;\n"; } - - $args = '$prow, $rrow_cid, $contact'; - self::DEBUG && Conf::msg_debugt("function ({$args}) {\n // " . simplify_whitespace($this->expression) . "\n {$t}}\n"); - return eval("return function ($args) {\n $t};"); + $function = "function (\$prow, \$rrow_cid, \$contact) {\n" + . " // " . simplify_whitespace($this->expression) + . "\n {$body}}"; + self::DEBUG && self::debug_report($function); + return eval("return {$function};"); } /** @return callable(PaperInfo,?int,Contact):mixed */ @@ -2663,31 +2672,30 @@ static function combine_index_types(...$index_types) { } static function compile_indexes_function(Contact $user, $index_types) { - if ($index_types !== 0) { - $state = new FormulaCompiler($user); - $g = $state->loop_variable($index_types); - $t = "assert(\$contact->contactXid === {$user->contactXid});\n " - . join("\n ", $state->gstmt) . "\n"; - if (($index_types & Fexpr::IDX_REVIEW) !== 0) { - $check = ""; - if ($index_types === Fexpr::IDX_CREVIEW) { - $check = " if (!\$rrow->reviewSubmitted) { continue; }\n"; - } - $t .= " \$cids = [];\n" - . " foreach ({$g} as \$rrow) {\n" - . $check - . " \$cids[] = \$rrow->contactId;\n" - . " }\n" - . " return \$cids;\n"; - } else { - $t .= " return array_keys({$g});\n"; + if ($index_types === 0) { + return null; + } + $state = new FormulaCompiler($user); + $g = $state->loop_variable($index_types); + $body = "assert(\$contact->contactXid === {$user->contactXid});\n " + . join("\n ", $state->gstmt) . "\n"; + if (($index_types & Fexpr::IDX_REVIEW) !== 0) { + $check = ""; + if ($index_types === Fexpr::IDX_CREVIEW) { + $check = " if (!\$rrow->reviewSubmitted) { continue; }\n"; } - $args = '$prow, $contact'; - self::DEBUG && error_log("function ({$args}) {\n {$t}}\n"); - return eval("return function ({$args}) {\n {$t}};"); + $body .= " \$cids = [];\n" + . " foreach ({$g} as \$rrow) {\n" + . $check + . " \$cids[] = \$rrow->contactId;\n" + . " }\n" + . " return \$cids;\n"; } else { - return null; + $body .= " return array_keys({$g});\n"; } + $function = "function (\$prow, \$contact) {\n {$body}}"; + self::DEBUG && self::debug_report($function); + return eval("return {$function};"); } /** @return bool */ @@ -2709,18 +2717,20 @@ function support_combiner() { function compile_extractor_function() { $this->support_combiner(); - $t = $this->_extractorf ? : " return null;\n"; - $args = '$prow, $rrow_cid, $contact'; - self::DEBUG && error_log("function ({$args}) {\n // extractor " . simplify_whitespace($this->expression) . "\n {$t}}\n"); - return eval("return function ({$args}) {\n {$t}};"); + $function = "function (\$prow, \$rrow_cid, \$contact) {\n" + . " // extractor " . simplify_whitespace($this->expression) + . "\n " . ($this->_extractorf ? : "return null;\n") . "}"; + self::DEBUG && self::debug_report($function); + return eval("return {$function};"); } function compile_combiner_function() { $this->support_combiner(); - $t = $this->_combinerf ? : " return null;\n"; - $args = '$extractor_results'; - self::DEBUG && error_log("function ({$args}) {\n // combiner " . simplify_whitespace($this->expression) . "\n {$t}}\n"); - return eval("return function ({$args}) {\n {$t}};"); + $function = "function (\$extractor_results) {\n" + . " // combiner " . simplify_whitespace($this->expression) + . "\n " . ($this->_combinerf ? : "return null;\n") . "}"; + self::DEBUG && self::debug_report($function); + return eval("return {$function};"); } /* function _unparse_iso_duration($x) { From a698bb406b809489ef76c96eb05719a483aaa00e Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 17 Apr 2024 11:39:22 -0400 Subject: [PATCH 36/78] Graphs: New visuals, improve spacing. * Automatically set left margin based on width of ticks. * Less aggressive axes (no Y-axis line, for example). * Refactor to allow that. --- scripts/graph.js | 563 ++++++++++++++++++++++++------------------ scripts/script.js | 44 ++-- src/formulagraph.php | 4 - stylesheets/style.css | 3 + 4 files changed, 350 insertions(+), 264 deletions(-) diff --git a/scripts/graph.js b/scripts/graph.js index e04ac0380..587e0ca85 100644 --- a/scripts/graph.js +++ b/scripts/graph.js @@ -10,7 +10,7 @@ hotcrp.graph = (function ($, d3) { var handle_ui = hotcrp.handle_ui, ensure_pattern = hotcrp.ensure_pattern, hoturl = hotcrp.hoturl; -var BOTTOM_MARGIN = 30; +var BOTTOM_MARGIN = 38; var PATHSEG_ARGMAP = { m: 2, M: 2, z: 0, Z: 0, l: 2, L: 2, h: 1, H: 1, v: 1, V: 1, c: 6, C: 6, s: 4, S: 4, q: 4, Q: 4, t: 2, T: 2, a: 7, A: 7, b: 1, B: 1 @@ -18,17 +18,20 @@ var PATHSEG_ARGMAP = { var normalized_path_cache = {}, normalized_path_cache_size = 0; function svg_path_number_of_items(s) { - if (s instanceof SVGPathElement) + if (s instanceof SVGPathElement) { s = s.getAttribute("d"); - if (normalized_path_cache[s]) + } + if (normalized_path_cache[s]) { return normalized_path_cache[s].length; - else + } else { return s.replace(/[^A-DF-Za-df-z]+/g, "").length; + } } function make_svg_path_parser(s) { - if (s instanceof SVGPathElement) + if (s instanceof SVGPathElement) { s = s.getAttribute("d"); + } s = s.split(/([a-zA-Z]|[-+]?(?:\d+\.?\d*|\.\d+)(?:[Ee][-+]?\d+)?)/); var i = 1, e = s.length, next_cmd; return function () { @@ -57,10 +60,12 @@ function make_svg_path_parser(s) { var normalize_path_complaint = false; function normalize_svg_path(s) { - if (s instanceof SVGPathElement) + if (s instanceof SVGPathElement) { s = s.getAttribute("d"); - if (normalized_path_cache[s]) + } + if (normalized_path_cache[s]) { return normalized_path_cache[s]; + } var res = [], cx = 0, cy = 0, cx0 = 0, cy0 = 0, copen = false, @@ -147,16 +152,16 @@ function normalize_svg_path(s) { cx0 = a[1]; cy0 = a[2]; copen = false; - } else if (ch === "L") + } else if (ch === "L") { res.push(["L", cx, cy, a[1], a[2]]); - else if (ch === "C") + } else if (ch === "C") { res.push(["C", cx, cy, a[1], a[2], a[3], a[4], a[5], a[6]]); - else if (ch === "Q") + } else if (ch === "Q") { res.push(["C", cx, cy, cx + 2 * (a[1] - cx) / 3, cy + 2 * (a[2] - cy) / 3, a[3] + 2 * (a[1] - a[3]) / 3, a[4] + 2 * (a[2] - a[4]) / 3, a[3], a[4]]); - else { + } else { // XXX should render "A" as a bezier if (++normalize_path_complaint == 1) log_jserror("bad normalize_svg_path " + ch); @@ -288,7 +293,7 @@ function max_procrastination_seq(ri, dl) { max_procrastination_seq.label = function (dl) { return dl.length > 1 ? "Days until maximum deadline" : "Days until deadline"; }; -procrastination_seq.tick_format = max_procrastination_seq.tick_format = +procrastination_seq.tickFormat = max_procrastination_seq.tickFormat = function (x) { return -x; }; function seq_to_cdf(seq, flip, raw) { @@ -307,14 +312,14 @@ function seq_to_cdf(seq, flip, raw) { function expand_extent(e, args) { - var l = e[0], h = e[1], delta; + let l = e[0], h = e[1]; if (l > 0 && l < h / 11) { l = 0; } else if (l > 0 && args.discrete) { l -= 0.5; } if (h - l < 10) { - delta = Math.min(1, h - l) * 0.2; + const delta = Math.min(1, h - l) * 0.2; if (args.orientation !== "y" || l > 0) { l -= delta; } @@ -327,27 +332,31 @@ function expand_extent(e, args) { } -function make_axes(svg, xAxis, yAxis, args) { +function draw_axes(svg, xAxis, yAxis, args) { function axisLabelStyles(x) { x.style("text-anchor", "end") - .style("font-size", "smaller") .style("pointer-events", "none"); } - svg.append("g") + const parent = d3.select(svg.node().parentElement); + const xaxe = parent.append("g") .attr("class", "x axis") - .attr("transform", "translate(0," + args.height + ")") + .attr("transform", `translate(${args.marginLeft},${args.marginTop+args.plotHeight})`) .call(xAxis) .attr("font-family", null) .attr("font-size", null) .attr("fill", null) - .call(make_rotate_ticks(args.x.rotate_ticks)) - .append("text") - .attr("x", args.width).attr("y", 0).attr("dy", "-.5em") - .call(axisLabelStyles) - .text(args.x.label || ""); + .call(make_rotate_ticks(args.x.tickRotation)); + if (args.x.label) { + xaxe.append("text") + .attr("x", args.plotWidth) + .attr("y", args.marginBottom - 3) + .style("text-anchor", "end") + .style("pointer-events", "none") + .text(`${args.x.label} →`); + } - args.x.discrete && svg.select(".x.axis .domain").each(function () { + xaxe.select(".domain").each(function () { var d = this.getAttribute("d"); this.setAttribute("d", d.replace(/^M([^A-Z]*),([^A-Z]*)V0H([^A-Z]*)V([^A-Z]*)$/, function (m, x1, y1, x2, y2) { @@ -355,20 +364,25 @@ function make_axes(svg, xAxis, yAxis, args) { })); }); - svg.append("g") + const yaxe = parent.append("g") .attr("class", "y axis") + .attr("transform", `translate(${args.marginLeft},${args.marginTop})`) .call(yAxis) .attr("font-family", null) .attr("font-size", null) .attr("fill", null) - .call(make_rotate_ticks(args.y.rotate_ticks)) - .append("text") - .attr("transform", "rotate(-90)") - .attr("y", 6).attr("dy", ".71em") - .call(axisLabelStyles) - .text(args.y.label || ""); - - args.y.discrete && svg.select(".y.axis .domain").each(function () { + .call(make_rotate_ticks(args.y.tickRotation)); + if (args.y.label) { + yaxe.append("text") + .attr("x", -args.marginLeft) + .attr("y", -14) + .style("text-anchor", "start") + .style("pointer-events", "none") + .text(`↑ ${args.y.label}`); + } + yaxe.select(".domain").remove(); + + args.y.discrete && xaxe.select(".domain").each(function () { var d = this.getAttribute("d"); this.setAttribute("d", d.replace(/^M([^A-Z]*),([^A-Z]*)H0V([^A-Z]*)H([^A-Z]*)$/, function (m, x1, y1, y2, x2) { @@ -376,10 +390,8 @@ function make_axes(svg, xAxis, yAxis, args) { })); }); - args.x.ticks.rewrite.call(svg.select(".x.axis"), svg); - args.y.ticks.rewrite.call(svg.select(".y.axis"), svg); - xAxis.axis_args = args.x; - yAxis.axis_args = args.y; + args.x.ticks.rewrite.call(xaxe, svg); + args.y.ticks.rewrite.call(yaxe, svg); } function proj0(d) { @@ -472,73 +484,60 @@ function make_reviewer_clicker(email) { } function clicker_go(url, event) { - if (event && event.metaKey) + if (event && event.metaKey) { window.open(url, "_blank", "noopener"); - else + } else { window.location = url; + } } -function make_axis(ticks) { - if (ticks && ticks[0] === "named") +const default_axis = { + make_axis: numeric_make_axis, + rewrite: function () {}, + render_onto: function (e, value) { + e.append(this.scale.tickFormat()(value)); + }, + search: function () { return null; } +}; + +function make_ticks(ticks) { + if (ticks && ticks[0] === "named") { ticks = named_integer_ticks(ticks[1]); - else if (ticks && ticks[0] === "score") + } else if (ticks && ticks[0] === "score") { ticks = score_ticks(hotcrp.make_review_field(ticks[1])); - else if (ticks && ticks[0] === "time") + } else if (ticks && ticks[0] === "time") { ticks = time_ticks(); - else + } else { ticks = {type: ticks ? ticks[0] : null}; - return $.extend({ - prepare: function () {}, - rewrite: function () {}, - render_onto: function (e, value) { - if (value == Math.floor(value)) { - e.append(value); - } else { - const dom = this.scale().domain(), - dig = Math.max(0, -Math.round(Math.log10(dom[1] - dom[0])) + 2); - e.append(value.toFixed(dig)); - } - }, - search: function () { return null; } - }, ticks); + } + return $.extend({}, default_axis, ticks); } -function axis_domain(axis, argextent, e) { - if (argextent && argextent[0] != null) +function make_linear_scale(argextent, e) { + if (argextent && argextent[0] != null) { e = [argextent[0], e[1]]; - if (argextent && argextent[1] != null) + } + if (argextent && argextent[1] != null) { e = [e[0], argextent[1]]; - axis.domain(e); -} - -function make_args(selector, args) { - args = $.extend({top: 20, right: 20, bottom: BOTTOM_MARGIN, left: 50}, args); - args.x = args.x || {}; - args.y = args.y || {}; - args.width = $(selector).width() - args.left - args.right; - args.height = 520 - args.top - args.bottom; - args.x.ticks = make_axis(args.x.ticks); - args.y.ticks = make_axis(args.y.ticks); - return args; + } + return d3.scaleLinear().domain(e); } -function render_position(axis, p, prefix) { - const e = $e("span", "nw"), aa = axis.axis_args; +function render_position(aa, p, prefix) { + const e = $e("span", "nw"); if (prefix || aa.label) { e.append((prefix || "") + (aa.label ? aa.label + " " : "")); } - aa.ticks.render_onto.call(axis, e, p, true); + aa.ticks.render_onto.call(aa, e, p, true); return e; } // args: {selector: JQUERYSELECTOR, // data: [{d: [ARRAY], label: STRING, className: STRING}], -// x/y: {label: STRING, tick_format: STRING}} +// x/y: {label: STRING, tickFormat: STRING}} function graph_cdf(selector, args) { - var x = d3.scaleLinear().range(args.x.flip ? [args.width, 0] : [0, args.width]), - y = d3.scaleLinear().range([args.height, 0]), - svg = this; + const svg = this; // massage data var series = args.data; @@ -564,16 +563,13 @@ function graph_cdf(selector, args) { }, [Infinity, -Infinity]); xdomain = [xdomain[0] - (xdomain[1] - xdomain[0]) / 32, xdomain[1] + (xdomain[1] - xdomain[0]) / 32]; - axis_domain(x, args.x.extent, xdomain); - axis_domain(y, args.y.extent, [0, Math.ceil(d3.max(data, function (d) { - return d[d.length - 1][1]; - }) * 10) / 10]); - - // axes - var xAxis = d3.axisBottom(x); - args.x.ticks.prepare.call(xAxis, x.domain(), x.range()); - args.x.tick_format && xAxis.tickFormat(args.x.tick_format); - var yAxis = d3.axisLeft(y); + const x = make_linear_scale(args.x.extent, xdomain), + y = make_linear_scale(args.y.extent, [0, Math.ceil(d3.max(data, function (d) { + return d[d.length - 1][1]; + }) * 10) / 10]), + axes = make_axis_pair(args, x, y); + + // lines var line = d3.line().x(function (d) {return x(d[0]);}) .y(function (d) {return y(d[1]);}); @@ -596,12 +592,12 @@ function graph_cdf(selector, args) { var hovers = svg.selectAll(".gcdf-hover0, .gcdf-hover1"); hovers.style("display", "none"); - make_axes(svg, xAxis, yAxis, args); + draw_axes(svg, axes[0], axes[1], args); svg.append("rect") - .attr("x", -args.left) - .attr("width", args.width + args.left) - .attr("height", args.height + args.bottom) + .attr("x", -args.marginLeft) + .attr("width", args.plotWidth + args.marginLeft) + .attr("height", args.plotHeight + args.marginBottom) .attr("fill", "none") .style("pointer-events", "all") .on("mouseover", mousemoved) @@ -635,15 +631,15 @@ function graph_cdf(selector, args) { if (args.cdf_tooltip_position) { const f = $frag(); hovered_series.label && f.append(hovered_series.label + " "); - args.x.ticks.render_onto.call(xAxis, f, x.invert(p[0]), true); + args.x.ticks.render_onto.call(args.x, f, x.invert(p[0]), true); f.append(", "); - args.y.ticks.render_onto.call(yAxis, f, y.invert(p[1]), true); + args.y.ticks.render_onto.call(args.y, f, y.invert(p[1]), true); hubble.replace_content(f); } else { hubble.text(hovered_series.label); } hubble.anchor(dir >= 0.25*Math.PI && dir <= 0.75*Math.PI ? "e" : "s") - .at(p[0] + args.left, p[1], this); + .at(p[0] + args.marginLeft, p[1], this); } else if (hubble) { hubble = hubble.remove() && null; } @@ -691,22 +687,24 @@ function procrastination_filter(revdata) { var dlf = max_procrastination_seq; // infer deadlines when not set - for (i in revdata.deadlines) + for (i in revdata.deadlines) { if (!revdata.deadlines[i]) { var subat = alldata.filter(function (d) { return (d[2] || 0) == i; }) .map(proj0); subat.sort(d3.ascending); revdata.deadlines[i] = subat.length ? d3.quantile(subat, 0.8) : 0; } + } // make cdfs - for (i in args.data) + for (i in args.data) { args.data[i].d = seq_to_cdf(dlf(args.data[i].d, revdata.deadlines)); + } - if (dlf.tick_format) - args.x.tick_format = dlf.tick_format; + if (dlf.tickFormat) { + args.x.tickFormat = dlf.tickFormat; + } args.x.label = dlf.label(revdata.deadlines); args.y.label = "Fraction of assignments completed"; - return args; } @@ -953,20 +951,12 @@ function scatter_union(p) { } function graph_scatter(selector, args) { - var data = data_to_scatter(args.data), - svg = this; - - var xe = d3.extent(data, proj0), - ye = d3.extent(data, proj1), - x = d3.scaleLinear().range(args.x.flip ? [args.width, 0] : [0, args.width]), - y = d3.scaleLinear().range(args.y.flip ? [0, args.height] : [args.height, 0]); - axis_domain(x, args.x.extent, expand_extent(xe, args.x)); - axis_domain(y, args.y.extent, expand_extent(ye, args.y)); + const svg = this; + let data = data_to_scatter(args.data); - var xAxis = d3.axisBottom(x); - args.x.ticks.prepare.call(xAxis, xe, x.range()); - var yAxis = d3.axisLeft(y); - args.y.ticks.prepare.call(yAxis, ye, y.range()); + const x = make_linear_scale(args.x.extent, expand_extent(d3.extent(data, proj0), args.x)), + y = make_linear_scale(args.y.extent, expand_extent(d3.extent(data, proj1), args.y)), + axes = make_axis_pair(args, x, y); $(selector).on("hotgraphhighlight", highlight); @@ -976,12 +966,12 @@ function graph_scatter(selector, args) { svg.append("path").attr("class", "gdot gdot-hover"); var hovers = svg.selectAll(".gdot-hover").style("display", "none"); - make_axes(svg, xAxis, yAxis, args); + draw_axes(svg, axes[0], axes[1], args); svg.append("rect") - .attr("x", -args.left) - .attr("width", args.width + args.left) - .attr("height", args.height + args.bottom) + .attr("x", -args.marginLeft) + .attr("width", args.plotWidth + args.marginLeft) + .attr("height", args.plotHeight + args.marginBottom) .attr("fill", "none") .style("pointer-events", "all") .on("mouseover", mousemoved) @@ -991,7 +981,7 @@ function graph_scatter(selector, args) { function make_tooltip(p, ps) { return [ - $e("p", null, render_position(xAxis, p[0]), ", ", render_position(yAxis, p[1])), + $e("p", null, render_position(args.x, p[0]), ", ", render_position(args.y, p[1])), render_pid_p(ps, p[3]) ]; } @@ -1103,11 +1093,11 @@ function data_to_barchart(data, yaxis) { } function graph_bars(selector, args) { - var data = data_to_barchart(args.data, args.y), - ystart = args.y.ticks.type === "score" ? 0.75 : 0, - svg = this; + const svg = this, + data = data_to_barchart(args.data, args.y); - var xe = d3.extent(data, proj0), + const ystart = args.y.ticks.type === "score" ? 0.75 : 0, + xe = d3.extent(data, proj0), ge = d3.extent(data, function (d) { return d[4] || 0; }), ye = [d3.min(data, function (d) { return Math.max(d.yoff, ystart); }), d3.max(data, function (d) { return d.yoff + d[1]; })], @@ -1115,24 +1105,20 @@ function graph_bars(selector, args) { var delta = i ? d[0] - data[i-1][0] : 0; return delta || Infinity; }), - x = d3.scaleLinear().range(args.x.flip ? [args.width, 0] : [0, args.width]), - y = d3.scaleLinear().range(args.y.flip ? [0, args.height] : [args.height, 0]); - axis_domain(x, args.x.extent, expand_extent(xe, args.x)); - axis_domain(y, args.y.extent, ye); - - var dpr = window.devicePixelRatio || 1; - var barwidth = args.width / 20; - if (deltae[0] != Infinity) + x = make_linear_scale(args.x.extent, expand_extent(xe, args.x)), + y = make_linear_scale(args.y.extent, ye), + axes = make_axis_pair(args, x, y); + + const dpr = window.devicePixelRatio || 1; + let barwidth = args.plotWidth / 20; + if (deltae[0] != Infinity) { barwidth = Math.min(barwidth, Math.abs(x(xe[0] + deltae[0]) - x(xe[0]))); + } barwidth = Math.max(5, barwidth); - if (ge[1]) + if (ge[1]) { barwidth = Math.floor((barwidth - 3) * dpr) / (dpr * (ge[1] + 1)); - var gdelta = -(ge[1] + 1) * barwidth / 2; - - var xAxis = d3.axisBottom(x); - args.x.ticks.prepare.call(xAxis, xe, x.range()); - var yAxis = d3.axisLeft(y); - args.y.ticks.prepare.call(yAxis, ye, y.range()); + } + const gdelta = -(ge[1] + 1) * barwidth / 2; function place(sel, close) { return sel.attr("d", function (d) { @@ -1150,7 +1136,7 @@ function graph_bars(selector, args) { }) .style("fill", function (d) { return ensure_pattern(d[3], "gdot"); })); - make_axes(svg, xAxis, yAxis, args); + draw_axes(svg, axes[0], axes[1], args); svg.append("path").attr("class", "gbar gbar-hover0"); svg.append("path").attr("class", "gbar gbar-hover1"); @@ -1162,7 +1148,7 @@ function graph_bars(selector, args) { function make_tooltip(p) { return [ - $e("p", null, render_position(xAxis, p[0]), ", ", render_position(yAxis, p[1])), + $e("p", null, render_position(args.x, p[0]), ", ", render_position(args.y, p[1])), render_pid_p(p[2], p[3]) ]; } @@ -1227,13 +1213,14 @@ function boxplot_sort(data) { function data_to_boxplot(data, septags) { data = boxplot_sort(data_quantize_x(data_to_scatter(data))); - var active = null; + let active = null; data = data.reduce(function (newdata, d) { if (!active || active[0] != d[0] || (septags && active[4] != d[3])) { active = {"0": d[0], ymin: d[1], c: d[3] || "", d: [], p: []}; newdata.push(active); - } else if (active.c != d[3]) + } else if (active.c != d[3]) { active.c = ""; + } active.ymax = d[1]; active.d.push(d[1]); active.p.push(d[2]); @@ -1241,11 +1228,11 @@ function data_to_boxplot(data, septags) { }, []); data.map(function (d) { - var l = d.d.length, med = d3.quantile(d.d, 0.5); - if (l < 4) + const l = d.d.length, med = d3.quantile(d.d, 0.5); + if (l < 4) { d.q = [d.d[0], d.d[0], med, d.d[l-1], d.d[l-1]]; - else { - var q1 = d3.quantile(d.d, 0.25), q3 = d3.quantile(d.d, 0.75), + } else { + const q1 = d3.quantile(d.d, 0.25), q3 = d3.quantile(d.d, 0.75), iqr = q3 - q1; d.q = [Math.max(d.d[0], q1 - 1.5 * iqr), q1, med, q3, Math.min(d.d[l-1], q3 + 1.5 * iqr)]; @@ -1257,30 +1244,25 @@ function data_to_boxplot(data, septags) { } function graph_boxplot(selector, args) { - var data = data_to_boxplot(args.data, !!args.y.fraction, true), + const data = data_to_boxplot(args.data, !!args.y.fraction, true), $sel = $(selector), svg = this; - var xe = d3.extent(data, proj0), + const xe = d3.extent(data, proj0), ye = [d3.min(data, function (d) { return d.ymin; }), d3.max(data, function (d) { return d.ymax; })], deltae = d3.extent(data, function (d, i) { var delta = i ? d[0] - data[i-1][0] : 0; return delta || Infinity; }), - x = d3.scaleLinear().range(args.x.flip ? [args.width, 0] : [0, args.width]), - y = d3.scaleLinear().range(args.y.flip ? [0, args.height] : [args.height, 0]); - axis_domain(x, args.x.extent, expand_extent(xe, args.x)); - axis_domain(y, args.y.extent, expand_extent(ye, args.y)); + x = make_linear_scale(args.x.extent, expand_extent(xe, args.x)), + y = make_linear_scale(args.y.extent, expand_extent(ye, args.y)), + axes = make_axis_pair(args, x, y); - var barwidth = args.width/80; - if (deltae[0] != Infinity) + let barwidth = args.plotWidth / 80; + if (deltae[0] != Infinity) { barwidth = Math.max(Math.min(barwidth, Math.abs(x(xe[0] + deltae[0]) - x(xe[0])) * 0.5), 6); - - var xAxis = d3.axisBottom(x); - args.x.ticks.prepare.call(xAxis, xe, x.range()); - var yAxis = d3.axisLeft(y); - args.y.ticks.prepare.call(yAxis, ye, y.range()); + } function place_whisker(l, sel) { sel.attr("x1", function (d) { return x(d[0]); }) @@ -1358,7 +1340,7 @@ function graph_boxplot(selector, args) { .data(outliers.data).enter().append("circle") .attr("class", function (d) { return "gbox outlier " + d[3]; })); - make_axes(svg, xAxis, yAxis, args); + draw_axes(svg, axes[0], axes[1], args); svg.append("line").attr("class", "gbox whiskerl gbox-hover"); svg.append("line").attr("class", "gbox whiskerh gbox-hover"); @@ -1394,18 +1376,18 @@ function graph_boxplot(selector, args) { function make_tooltip(p, ps, ds, cc) { const yformat = args.y.ticks.render_onto, pe = $e("p"); let x = ps; - pe.append(render_position(xAxis, p[0])); + pe.append(render_position(args.x, p[0])); if (p.q) { - pe.append(", ", render_position(yAxis, p.q[2], "median ")); + pe.append(", ", render_position(args.y, p.q[2], "median ")); x = []; for (let i = 0; i < ps.length; ++i) { const rest = $frag(" ("); - yformat.call(yAxis, rest, ds[i]); + yformat.call(args.y, rest, ds[i]); rest.append(")"); x.push({id: ps[i], rest: rest}); } } else { - pe.append(", ", render_position(yAxis, ds[0])); + pe.append(", ", render_position(args.y, ds[0])); } return [pe, render_pid_p(x, cc)]; } @@ -1494,30 +1476,87 @@ function graph_boxplot(selector, args) { } } +function make_axis_pair(args, x, y) { + const axes = [ + args.x.ticks.make_axis.call(args.x, "x", args, x), + args.y.ticks.make_axis.call(args.y, "y", args, y) + ]; + if (args.y.tickLength > 0 && args.marginLeftDefault) { + args.marginLeft = 10 * args.y.tickLength + 6; + args.plotWidth = args.width - args.marginLeft - args.marginRight; + x.range(args.x.flip ? [args.plotWidth, 0] : [0, args.plotWidth]); + } + args.svg.attr("transform", "translate(".concat(args.marginLeft, ",", args.marginTop, ")")); + return axes; +} + +function basic_make_axis(side, args, scale) { + const dimen = side === "x" ? args.plotWidth : args.plotHeight; + scale.range(!this.flip === (side === "y") ? [dimen, 0] : [0, dimen]); + const ax = side === "x" ? d3.axisBottom(scale) : d3.axisLeft(scale); + if (this.tickFormat) { + ax.tickFormat(this.tickFormat); + } + this.scale = scale; + this.axis = ax; + return ax; +} + +function numeric_make_axis(side, args, scale) { + const ax = basic_make_axis.call(this, side, args, scale), + tf = scale.tickFormat(); + this.tickLength = 0; + for (const v of scale.ticks()) { + this.tickLength = Math.max(this.tickLength, tf(v).replace(/,/g, "").length); + } + return ax; +} + function score_ticks(rf) { - var split = true; + let myfmt; return { - prepare: function (extent) { - var count = Math.floor(extent[1] * 2) - Math.ceil(extent[0] * 2) + 1; + make_axis: function (side, args, scale) { + const domain = scale.domain(); + let count = Math.floor(domain[1] * 2) - Math.ceil(domain[0] * 2) + 1; if (count > 11) { - split = false; - count = Math.floor(extent[1]) - Math.ceil(extent[0]) + 1; + count = Math.floor(domain[1]) - Math.ceil(domain[0]) + 1; + } + const ax = basic_make_axis.call(this, side, args, scale); + if (!rf.default_numeric) { + ax.ticks(count); + } + this.tickLength = 1; + myfmt = scale.tickFormat(); + for (const v of scale.ticks()) { + let vt = rf.unparse_symbol(v); + if (typeof vt === "number") { + vt = myfmt(vt); + } + this.tickLength = Math.max(this.tickLength, vt.length); } - if (!rf.default_numeric) - this.ticks(count); + return ax; }, rewrite: function () { this.selectAll("g.tick text").each(function () { - var d = d3.select(this), value = +d.text(); - d.attr("fill", rf.color(value)); - if (!rf.default_numeric && value) - d.text(rf.unparse_symbol(value, split)); + const d = d3.select(this), v = +d.text(); + d.attr("class", "sv"); + d.attr("fill", rf.color(v)); + if (!rf.default_numeric && v) { + let vt = rf.unparse_symbol(v); + if (typeof vt === "number") { + vt = myfmt(vt); + } + d.text(vt); + } }); }, render_onto: function (e, value, include_numeric) { - const k = rf.className(value), - t = rf.unparse_symbol(value, true); - e.append(k ? $e("span", "sv " + k, t) : t); + const k = rf.className(value); + let vt = rf.unparse_symbol(value); + if (typeof vt === "number") { + vt = vt.toFixed(2).replace(/\.00$/, ""); + } + e.append(k ? $e("span", "sv " + k, vt) : vt); if (include_numeric && !rf.default_numeric && value !== Math.round(value * 2) / 2) { @@ -1542,22 +1581,26 @@ function time_ticks() { } } return { - prepare: function (domain, range) { - var ddomain, scale; + make_axis: function (side, args, scale) { + const ax = basic_make_axis.call(this, side, args, scale), + domain = scale.domain(); if (domain[0] < 1000000000 || domain[1] < 1000000000) { - ddomain = [domain[0] / 86400, domain[1] / 86400]; - scale = d3.scaleLinear().domain(ddomain).range(range); - this.tickValues(scale.ticks().map(function (value) { + const ddomain = [domain[0] / 86400, domain[1] / 86400], + nscale = d3.scaleLinear().domain(ddomain).range(scale.range()); + ax.tickValues(nscale.ticks().map(function (value) { return value * 86400; })); + this.tickLength = Math.ceil(Math.log10(domain[1])); } else { - ddomain = [new Date(domain[0] * 1000), new Date(domain[1] * 1000)]; - scale = d3.scaleTime().domain(ddomain).range(range); - this.tickValues(scale.ticks().map(function (value) { + const ddomain = [new Date(domain[0] * 1000), new Date(domain[1] * 1000)], + nscale = d3.scaleTime().domain(ddomain).range(scale.range()); + ax.tickValues(nscale.ticks().map(function (value) { return value.getTime() / 1000; })); + this.tickLength = 10; } - this.tickFormat(format); + ax.tickFormat(format); + return ax; }, render_onto: function (e, value) { e.append(format(value)); @@ -1569,20 +1612,22 @@ function time_ticks() { function get_max_tick_width(axis) { return d3.max($(axis.selectAll("g.tick text").nodes()).map(function () { if (this.getBoundingClientRect) { - var r = this.getBoundingClientRect(); + const r = this.getBoundingClientRect(); return r.right - r.left; - } else + } else { return $(this).width(); + } })); } function get_sample_tick_height(axis) { return d3.quantile($(axis.selectAll("g.tick text").nodes()).map(function () { if (this.getBoundingClientRect) { - var r = this.getBoundingClientRect(); + const r = this.getBoundingClientRect(); return r.bottom - r.top; - } else + } else { return $(this).height(); + } }), 0.5); } @@ -1592,39 +1637,41 @@ function named_integer_ticks(map) { var want_mclasses = Object.keys(map).some(function (k) { return mclasses(k); }); function mtext(value) { - var m = map[value]; + const m = map[value]; return m && typeof m === "object" ? m.text : m; } function mclasses(value) { - var m = map[value]; + const m = map[value]; return (m && typeof m === "object" && m.color_classes) || ""; } function rewrite() { - if (!want_tilt && !want_mclasses) + if (!want_tilt && !want_mclasses) { return; + } - var max_width = get_max_tick_width(this); + let max_width = get_max_tick_width(this); if (max_width > 100) { // shrink font this.attr("class", function () { return this.getAttribute("class") + " widelabel"; }); max_width = get_max_tick_width(this); } - var example_height = get_sample_tick_height(this); + const example_height = get_sample_tick_height(this); // apply offset first (so `mclasses` rects include offset) - if (want_tilt) + if (want_tilt) { this.selectAll("g.tick text").style("text-anchor", "end") .attr("dx", "-9px").attr("dy", "2px"); + } // apply classes by adding them and adding background rects if (want_mclasses) { this.selectAll("g.tick text").filter(mclasses).each(function (i) { - var c = mclasses(i); + const c = mclasses(i); d3.select(this).attr("class", c + " taghh"); if (/\btagbg\b/.test(c)) { - var b = this.getBBox(); + const b = this.getBBox(); d3.select(this.parentNode).insert("rect", "text") .attr("x", b.x - 3).attr("y", b.y) .attr("width", b.width + 6).attr("height", b.height + 1) @@ -1647,20 +1694,31 @@ function named_integer_ticks(map) { // prevent label overlap if (want_tilt) { - var total_height = Object.values(map).length * (example_height * Math.cos(1.13446) + 8); - var alternation = Math.ceil(total_height / this.node().getBBox().width - 0.1); - if (alternation > 1) + const total_height = Object.values(map).length * (example_height * Math.cos(1.13446) + 8), + alternation = Math.ceil(total_height / this.node().getBBox().width - 0.1); + if (alternation > 1) { this.selectAll("g.tick").each(function (i) { if (i % alternation != 1) d3.select(this).style("display", "none"); }); + } } } return { - prepare: function (domain) { - var count = Math.floor(domain[1]) - Math.ceil(domain[0]) + 1; - this.ticks(count).tickFormat(mtext); + make_axis: function (side, args, scale) { + const domain = scale.domain(), + count = Math.floor(domain[1]) - Math.ceil(domain[0]) + 1, + ax = basic_make_axis.call(this, side, args, scale); + ax.ticks(count).tickFormat(mtext); + this.tickLength = 1; + for (const v of scale.ticks(count)) { + const m = mtext(v); + if (m) { + this.tickLength = Math.max(this.tickLength, m.length); + } + } + return ax; }, rewrite: rewrite, render_onto: function (e, value, include_numeric) { @@ -1675,7 +1733,7 @@ function named_integer_ticks(map) { } }, search: function (value) { - var m = map[value]; + const m = map[value]; return (m && typeof m === "object" && m.search) || null; }, type: "named_integer", @@ -1684,15 +1742,15 @@ function named_integer_ticks(map) { } function make_rotate_ticks(angle) { - if (!angle) + if (!angle) { return function () {}; - else - return function (axis) { - axis.selectAll("text") - .attr("x", 0).attr("y", 0).attr("dy", "-.71em") - .attr("transform", "rotate(" + angle + ")") - .style("text-anchor", "middle"); - }; + } + return function (axis) { + axis.selectAll("text") + .attr("x", 0).attr("y", 0).attr("dy", "-.71em") + .attr("transform", "rotate(" + angle + ")") + .style("text-anchor", "middle"); + }; } handle_ui.on("js-hotgraph-highlight", function () { @@ -1708,7 +1766,7 @@ handle_ui.on("js-hotgraph-highlight", function () { $(this).closest(".has-hotgraph").find(".hotgraph").trigger(e); }); -var graphers = { +const graphers = { procrastination: {filter: true, function: procrastination_filter}, scatter: {function: graph_scatter}, cdf: {function: graph_cdf}, @@ -1718,29 +1776,60 @@ var graphers = { box: {function: graph_boxplot} }; +function make_args(selector, args) { + args = $.extend({}, args); + const mns = ["marginTop", "marginRight", "marginBottom", "marginLeft"], + m = args.margin || [null, null, null, null], + mdefaults = [24, 20, BOTTOM_MARGIN, 50]; + for (let i = 0; i < 4; ++i) { + const mn = mns[i]; + if (args[mn] == null) { + args[mn] = m[i]; + } + if (args[mn] == null) { + args[mn] = mdefaults[i]; + args[mn + "Default"] = true; + } + } + if (args.width == null) { + args.width = $(selector).width(); + args.widthDefault = true; + } + if (args.height == null) { + args.height = 520; + args.heightDefault = true; + } + args.plotWidth = args.width - args.marginLeft - args.marginRight; + args.plotHeight = args.height - args.marginTop - args.marginBottom; + args.x = $.extend({}, args.x || {}); + args.y = $.extend({}, args.y || {}); + args.x.ticks = make_ticks(args.x.ticks); + args.y.ticks = make_ticks(args.y.ticks); + return args; +} + return function (selector, args) { - while (true) { - var g = graphers[args.type]; - if (!g) - return null; - else if (!d3) { - var $err = $('
').appendTo(selector); - append_feedback_near($err[0], {message: "<0>Graphs are not supported on this browser", status: 2}); - if (document.documentMode) { - append_feedback_near($err[0], {message: "<5>You appear to be using a version of Internet Explorer, which is no longer supported. Edge, Firefox, Chrome, and Safari are supported, among others.", status: -5 /*MessageSet::INFORM*/}); - } - return null; - } else if (g.filter) - args = g["function"](args); - else { - args = make_args(selector, args); - var svg = d3.select(selector).append("svg") - .attr("width", args.width + args.left + args.right) - .attr("height", args.height + args.top + args.bottom) - .append("g") - .attr("transform", "translate(".concat(args.left, ",", args.top, ")")); - return g["function"].call(svg, selector, args); + if (!d3) { + const $err = $('
').appendTo(selector); + append_feedback_near($err[0], {message: "<0>Graphs are not supported on this browser", status: 2}); + if (document.documentMode) { + append_feedback_near($err[0], {message: "<5>You appear to be using a version of Internet Explorer, which is no longer supported. Edge, Firefox, Chrome, and Safari are supported, among others.", status: -5 /*MessageSet::INFORM*/}); } + return null; + } + let g = graphers[args.type]; + while (g && g.filter) { + args = g["function"](args); + g = graphers[args.type]; + } + if (!g) { + return null; } + args = make_args(selector, args); + args.svg = d3.select(selector).append("svg") + .attr("width", args.width) + .attr("height", args.height) + .append("g"); + return g["function"].call(args.svg, selector, args); }; })(jQuery, window.d3); diff --git a/scripts/script.js b/scripts/script.js index 461d9e60a..f7514b048 100644 --- a/scripts/script.js +++ b/scripts/script.js @@ -675,12 +675,6 @@ var urldecode = function (s) { return decodeURIComponent(s.replace(/\+/g, "%20")); }; -function text_to_html(text) { - var n = document.createElement("div"); - n.appendChild(document.createTextNode(text)); - return n.innerHTML; -} - function text_eq(a, b) { if (a === b) return true; @@ -6580,18 +6574,20 @@ function Score_ReviewField(fj) { Object.setPrototypeOf(Score_ReviewField.prototype, DiscreteValues_ReviewField.prototype); -Score_ReviewField.prototype.unparse_symbol = function (val, split) { - if (val === (val | 0) && this.symbols[val - 1] != null) +Score_ReviewField.prototype.unparse_symbol = function (val) { + if (val === (val | 0) && this.symbols[val - 1] != null) { return this.symbols[val - 1]; - var rval = (split ? Math.round(val * 2) / 2 : Math.round(val)) - 1; - if (this.default_numeric || rval < 0 || rval > this.symbols.length - 1) - return val.toFixed(2); - else if (rval === (rval | 0)) - return this.symbols[rval]; - else if (this.flip) + } + const rval = Math.round(val * 2) / 2 - 1; + if (this.default_numeric || rval < 0 || rval > this.symbols.length - 1) { + return val; + } else if (rval === (rval | 0)) { + return this.symbols[rval].toString(); + } else if (this.flip) { return this.symbols[rval + 0.5].concat("~", this.symbols[rval - 0.5]); - else + } else { return this.symbols[rval - 0.5].concat("~", this.symbols[rval + 0.5]); + } }; Score_ReviewField.prototype.render_in = function (fv, rrow, fe) { @@ -6637,18 +6633,19 @@ Checkbox_ReviewField.prototype.className = function (val) { }; Checkbox_ReviewField.prototype.unparse_symbol = function (val) { - if (val === true || val === false) + if (val === true || val === false) { return val ? "✓" : "✗"; - else if (val < 0.125) + } else if (val < 0.125) { return "✗"; - else if (val < 0.375) + } else if (val < 0.375) { return "¼✓"; - else if (val < 0.625) + } else if (val < 0.625) { return "½✓"; - else if (val < 0.875) + } else if (val < 0.875) { return "¾✓"; - else + } else { return "✓"; + } }; Checkbox_ReviewField.prototype.render_in = function (fv, rrow, fe) { @@ -6668,9 +6665,10 @@ function Checkboxes_ReviewField(fj) { Object.setPrototypeOf(Checkboxes_ReviewField.prototype, DiscreteValues_ReviewField.prototype); Checkboxes_ReviewField.prototype.unparse_symbol = function (val) { - if (!val || val !== (val | 0)) + if (!val || val !== (val | 0)) { return ""; - var s, b, t = []; + } + let s, b, t = []; for (s = b = 1; b <= val; b <<= 1, ++s) { if (val & b) t.push(this.symbols[s - 1]); diff --git a/src/formulagraph.php b/src/formulagraph.php index 1bb959afc..997d216e1 100644 --- a/src/formulagraph.php +++ b/src/formulagraph.php @@ -1048,10 +1048,6 @@ function axis_json($axis) { } } - if (!$isx && ($rotate_y ?? $ticks !== null || $named_ticks !== null)) { - $j["rotate_ticks"] = -90; - } - if ($isx && $this->_xorder_map && $named_ticks !== null) { $newticks = []; foreach ($named_ticks as $n => $x) { diff --git a/stylesheets/style.css b/stylesheets/style.css index 10d7efe32..b4c32ce89 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -5328,3 +5328,6 @@ th.pl_logaction { fill: none; pointer-events: none; } +.hotgraph .axis { + font-size: smaller; +} From 91e1054eb3e664cbf35d8e5973908527bed05bb9 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 17 Apr 2024 23:54:17 -0400 Subject: [PATCH 37/78] Remove debug printout. --- src/formula.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/formula.php b/src/formula.php index 9d115db11..0df6670e5 100644 --- a/src/formula.php +++ b/src/formula.php @@ -1776,7 +1776,7 @@ class Formula implements JsonSerializable { const BINARY_OPERATOR_REGEX = '/\A(?:[-\+\/%^]|\*\*?|\&\&?|\|\|?|\?\?|==?|!=|<[<=]?|>[>=]?|≤|≥|≠)/'; /** @var 0|1|2 */ - const DEBUG = 1; + const DEBUG = 0; static public $opprec = [ "**" => 14, From ca04cb2365661bf00cb38c2e9b00d1ceda54b41d Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Wed, 17 Apr 2024 23:56:17 -0400 Subject: [PATCH 38/78] Nits --- scripts/graph.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/scripts/graph.js b/scripts/graph.js index 587e0ca85..9db2f68bf 100644 --- a/scripts/graph.js +++ b/scripts/graph.js @@ -333,12 +333,8 @@ function expand_extent(e, args) { function draw_axes(svg, xAxis, yAxis, args) { - function axisLabelStyles(x) { - x.style("text-anchor", "end") - .style("pointer-events", "none"); - } - const parent = d3.select(svg.node().parentElement); + const xaxe = parent.append("g") .attr("class", "x axis") .attr("transform", `translate(${args.marginLeft},${args.marginTop+args.plotHeight})`) @@ -355,7 +351,6 @@ function draw_axes(svg, xAxis, yAxis, args) { .style("pointer-events", "none") .text(`${args.x.label} →`); } - xaxe.select(".domain").each(function () { var d = this.getAttribute("d"); this.setAttribute("d", d.replace(/^M([^A-Z]*),([^A-Z]*)V0H([^A-Z]*)V([^A-Z]*)$/, @@ -381,14 +376,13 @@ function draw_axes(svg, xAxis, yAxis, args) { .text(`↑ ${args.y.label}`); } yaxe.select(".domain").remove(); - - args.y.discrete && xaxe.select(".domain").each(function () { + /*args.y.discrete && yaxe.select(".domain").each(function () { var d = this.getAttribute("d"); this.setAttribute("d", d.replace(/^M([^A-Z]*),([^A-Z]*)H0V([^A-Z]*)H([^A-Z]*)$/, function (m, x1, y1, y2, x2) { return x1 === x2 ? "M0,".concat(y1, "V", y2) : m; })); - }); + });*/ args.x.ticks.rewrite.call(xaxe, svg); args.y.ticks.rewrite.call(yaxe, svg); From 688db457d126b04e37e1c4ba2f729a37831fc465 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Sat, 20 Apr 2024 14:36:05 -0400 Subject: [PATCH 39/78] Update affiliation matchers. --- etc/affiliationmatchers.json | 6 +++++- scripts/graph.js | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/etc/affiliationmatchers.json b/etc/affiliationmatchers.json index afedaeefb..0db0f8e4d 100644 --- a/etc/affiliationmatchers.json +++ b/etc/affiliationmatchers.json @@ -714,6 +714,9 @@ "korea": { "alternate": [{"word": "kaist", "if": "advanced institute science technology"}] }, + "law": { + "weak": true + }, "london": { "weak": true, "alternate": [{"word": "ucl", "if": "university college"}] @@ -961,7 +964,8 @@ "weak": true, "alternate": [ {"word": "nyu", "if": "new university"} - ] + ], + "sync": ["new"] }, "zurich": { "weak": true diff --git a/scripts/graph.js b/scripts/graph.js index 9db2f68bf..dcfe96336 100644 --- a/scripts/graph.js +++ b/scripts/graph.js @@ -10,7 +10,7 @@ hotcrp.graph = (function ($, d3) { var handle_ui = hotcrp.handle_ui, ensure_pattern = hotcrp.ensure_pattern, hoturl = hotcrp.hoturl; -var BOTTOM_MARGIN = 38; +var BOTTOM_MARGIN = 37; var PATHSEG_ARGMAP = { m: 2, M: 2, z: 0, Z: 0, l: 2, L: 2, h: 1, H: 1, v: 1, V: 1, c: 6, C: 6, s: 4, S: 4, q: 4, Q: 4, t: 2, T: 2, a: 7, A: 7, b: 1, B: 1 @@ -1790,7 +1790,7 @@ function make_args(selector, args) { args.widthDefault = true; } if (args.height == null) { - args.height = 520; + args.height = 540; args.heightDefault = true; } args.plotWidth = args.width - args.marginLeft - args.marginRight; From dc3dbee7f22e1cb252f73c541223a83bc6071832 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Thu, 2 May 2024 23:34:18 +0000 Subject: [PATCH 40/78] Need to simplify_whitespace in db_searchable_name. --- src/contact.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/contact.php b/src/contact.php index 094dc35d7..60a163846 100644 --- a/src/contact.php +++ b/src/contact.php @@ -1115,7 +1115,8 @@ function searchable_name() { /** @return string */ function db_searchable_name() { - return substr(strtolower(UnicodeHelper::deaccent($this->searchable_name())), 0, 2048); + $n = strtolower(UnicodeHelper::deaccent($this->searchable_name())); + return simplify_whitespace(substr($n, 0, 2048)); } /** @return array{email?:string,first?:string,last?:string,affiliation?:string} */ From ded108d322a1aa43ec43eb20de76df54aad760fa Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Thu, 18 Apr 2024 12:48:59 -0400 Subject: [PATCH 41/78] Fix bar-graph stacking. --- scripts/graph.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/graph.js b/scripts/graph.js index dcfe96336..565feb6df 100644 --- a/scripts/graph.js +++ b/scripts/graph.js @@ -1056,29 +1056,30 @@ function data_to_barchart(data, yaxis) { || (a[3] || "").localeCompare(b[3] || ""); }); - var i, maxy, cur, last, ndata = []; - for (i = 0; i != data.length; ++i) { - cur = data[i]; + let last = null; + const ndata = []; + for (let i = 0; i !== data.length; ++i) { + const cur = data[i]; if (cur[1] == null) { continue; } ndata.push(cur); if (last && cur[0] == last[0] && cur[4] == last[4]) { cur.yoff = last.yoff + last[1]; - if (last.i0 == null) - last.i0 = ndata.length - 1; cur.i0 = last.i0; } else { cur.yoff = 0; + cur.i0 = ndata.length - 1; } + last = cur; } if (yaxis.fraction && ndata.some(function (d) { return d[4] != data[0][4]; })) { - maxy = {}; + let maxy = {}; ndata.forEach(function (d) { maxy[d[0]] = d[1] + d.yoff; }); ndata.forEach(function (d) { d.yoff /= maxy[d[0]]; d[1] /= maxy[d[0]]; }); } else if (yaxis.fraction) { - maxy = 0; + let maxy = 0; ndata.forEach(function (d) { maxy += d[1]; }); ndata.forEach(function (d) { d.yoff /= maxy; d[1] /= maxy; }); } From 93ad16fe0217eb7b2febabe669b3ad106e6f973a Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Thu, 9 May 2024 09:58:17 -0400 Subject: [PATCH 42/78] Dewarn on newer PHPs. --- src/pages/p_oauth.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pages/p_oauth.php b/src/pages/p_oauth.php index 1642e079d..9a65d1e10 100644 --- a/src/pages/p_oauth.php +++ b/src/pages/p_oauth.php @@ -23,6 +23,10 @@ class OAuthProvider { public $token_uri; /** @var ?string */ public $token_function; + /** @var ?object */ + public $group_mappings; + /** @var bool */ + public $remove_groups; /** @var ?string */ public $nonce; From 920d8c347a85c929ab451b0dc2be3aafdd1f6599 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Thu, 9 May 2024 09:58:49 -0400 Subject: [PATCH 43/78] Sessions: Check session IDs. --- lib/phpqsession.php | 8 ++++++-- lib/qsession.php | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/phpqsession.php b/lib/phpqsession.php index 370e6177e..2f1b31ce9 100644 --- a/lib/phpqsession.php +++ b/lib/phpqsession.php @@ -4,11 +4,15 @@ class PHPQsession extends Qsession { function start($sid) { - if ($sid !== null) { + if ($sid !== null + && strlen($sid) >= 20 + && strlen($sid) <= 128 + && (ctype_alnum($sid) || preg_match('/\A[-,0-9A-Za-z]+\z/', $sid))) { session_id($sid); } session_start(); - return session_id(); + $sid = session_id(); + return $sid !== "" ? $sid : null; } function new_sid() { diff --git a/lib/qsession.php b/lib/qsession.php index 8a776cf72..a929ef209 100644 --- a/lib/qsession.php +++ b/lib/qsession.php @@ -98,7 +98,7 @@ function handle_open() { /** @param ?string $sid * @return ?string */ - function start($sid) { + protected function start($sid) { return null; } From 413061d318807cf6afb7c91254025929901a8189 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Thu, 9 May 2024 10:04:39 -0400 Subject: [PATCH 44/78] Don't refer to a nonexistent session on the search page. If given an author-view capability, the search page can be asked to redisplay something without a session. --- lib/qsession.php | 5 +++++ src/pages/p_search.php | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/qsession.php b/lib/qsession.php index a929ef209..cf8076fed 100644 --- a/lib/qsession.php +++ b/lib/qsession.php @@ -102,6 +102,11 @@ protected function start($sid) { return null; } + /** @return bool */ + function is_open() { + return $this->sopen; + } + /** @return ?string */ function new_sid() { return null; diff --git a/src/pages/p_search.php b/src/pages/p_search.php index ace2c6837..934910b99 100644 --- a/src/pages/p_search.php +++ b/src/pages/p_search.php @@ -418,7 +418,9 @@ function print($qreq) { static function redisplay(Contact $user, Qrequest $qreq) { // change session based on request - Session_API::parse_view($qreq, "pl", $qreq); + if ($qreq->qsession()->is_open()) { + Session_API::parse_view($qreq, "pl", $qreq); + } // redirect, including differences between search and request // create PaperList if (isset($qreq->q)) { From fa1b46a33734d00fcaa73b2aafbb996c5a4b8d76 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Thu, 9 May 2024 10:22:46 -0400 Subject: [PATCH 45/78] Download assignment doesn't attempt to output. --- src/pages/p_autoassign.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/p_autoassign.php b/src/pages/p_autoassign.php index 5760206b1..d1391d2ec 100644 --- a/src/pages/p_autoassign.php +++ b/src/pages/p_autoassign.php @@ -677,6 +677,7 @@ private function handle_download_assignment(TokenInfo $tok) { $csvg = $this->conf->make_csvg("assignments"); $aset->make_acsv()->unparse_into($csvg); $csvg->sort(SORT_NATURAL)->emit(); + exit; } private function handle_execute(TokenInfo $tok) { From 0e8fea8c9ff38650c13c6c19feafb66cea818ef3 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Thu, 9 May 2024 10:27:20 -0400 Subject: [PATCH 46/78] Always call exit with parens. --- lib/login.php | 8 ++++---- lib/navigation.php | 2 +- src/api/api_upload.php | 2 +- src/init.php | 2 +- src/listaction.php | 4 ++-- src/listactions/la_get_sub.php | 2 +- src/listactions/la_getdocument.php | 2 +- src/listactions/la_getjson.php | 2 +- src/listactions/la_getjsonrqc.php | 2 +- src/listactions/la_getreviewbase.php | 2 +- src/listactions/la_revpref.php | 2 +- src/multiconference.php | 4 ++-- src/pages/p_api.php | 4 ++-- src/pages/p_authorize.php | 2 +- src/pages/p_autoassign.php | 10 +++++----- src/pages/p_changeemail.php | 2 +- src/pages/p_doc.php | 4 ++-- src/pages/p_home.php | 2 +- src/pages/p_log.php | 2 +- src/pages/p_search.php | 2 +- 20 files changed, 31 insertions(+), 31 deletions(-) diff --git a/lib/login.php b/lib/login.php index c42fcd875..210f24bec 100644 --- a/lib/login.php +++ b/lib/login.php @@ -19,7 +19,7 @@ static function check_http_auth(Contact $user, Qrequest $qreq) { } else { header("WWW-Authenticate: Basic realm=\"HotCRP\""); } - exit; + exit(); } // if user is still valid, OK @@ -36,7 +36,7 @@ static function check_http_auth(Contact $user, Qrequest $qreq) { MessageItem::inform("<0>This site is using HTTP authentication to manage its users, but you have not provided authentication data. This usually indicates a server configuration error.") ]); $qreq->print_footer(); - exit; + exit(); } $qreq->email = $_SERVER["REMOTE_USER"]; if (validate_email($qreq->email)) { @@ -57,7 +57,7 @@ static function check_http_auth(Contact $user, Qrequest $qreq) { MessageItem::inform("<0>This site is using HTTP authentication to manage its users. You have provided incorrect authentication data.") ]); $qreq->print_footer(); - exit; + exit(); } } @@ -205,7 +205,7 @@ static function check_postlogin(Contact $user, Qrequest $qreq) { $where = $user->conf->hoturl_raw("index"); } $user->conf->redirect($where); - exit; + exit(); } diff --git a/lib/navigation.php b/lib/navigation.php index 191fed42b..3938e0341 100644 --- a/lib/navigation.php +++ b/lib/navigation.php @@ -704,6 +704,6 @@ static function redirect_absolute($url) { Redirection

You should be redirected to here.

\n"; - exit; + exit(); } } diff --git a/src/api/api_upload.php b/src/api/api_upload.php index 97b06e383..ed3c7f81b 100644 --- a/src/api/api_upload.php +++ b/src/api/api_upload.php @@ -703,7 +703,7 @@ function exec(Contact $user, Qrequest $qreq, ?PaperInfo $prow) { fastcgi_finish_request(); } $this->transfer(false, "{$offset}+{$length}"); - exit; + exit(); } else { $this->transfer(true, "finish"); return $this->_make_result(); diff --git a/src/init.php b/src/init.php index c54441ef7..09ff0ac37 100644 --- a/src/init.php +++ b/src/init.php @@ -284,7 +284,7 @@ function initialize_request($kwarg = null) { && $qreq->method() !== "POST" && $qreq->method() !== "HEAD") { header("HTTP/1.0 405 Method Not Allowed"); - exit; + exit(); } // mark as already expired to discourage caching, but allow the browser diff --git a/src/listaction.php b/src/listaction.php index ceea930fd..8c8f64b43 100644 --- a/src/listaction.php +++ b/src/listaction.php @@ -127,10 +127,10 @@ static function call($name, Contact $user, Qrequest $qreq, $selection) { } } else if ($res instanceof CsvGenerator) { $res->emit(); - exit; + exit(); } else if ($res instanceof Redirection) { $user->conf->redirect($res->url); - exit; + exit(); } } diff --git a/src/listactions/la_get_sub.php b/src/listactions/la_get_sub.php index fd44b7e7b..06f2b7a84 100644 --- a/src/listactions/la_get_sub.php +++ b/src/listactions/la_get_sub.php @@ -40,7 +40,7 @@ function run(Contact $user, Qrequest $qreq, SearchSelection $ssel) { ob_flush(); flush(); } - exit; + exit(); } } diff --git a/src/listactions/la_getdocument.php b/src/listactions/la_getdocument.php index 6f12265cf..24a7c6552 100644 --- a/src/listactions/la_getdocument.php +++ b/src/listactions/la_getdocument.php @@ -55,7 +55,7 @@ function run(Contact $user, Qrequest $qreq, SearchSelection $ssel) { $dopt->single = true; $dopt->log_user = $user; if ($docset->download($dopt)) { - exit; + exit(); } else { $user->conf->feedback_msg($docset->message_list()); } diff --git a/src/listactions/la_getjson.php b/src/listactions/la_getjson.php index a49113721..6341e0c7a 100644 --- a/src/listactions/la_getjson.php +++ b/src/listactions/la_getjson.php @@ -54,6 +54,6 @@ function run(Contact $user, Qrequest $qreq, SearchSelection $ssel) { header("Content-Disposition: attachment; filename=" . mime_quote_string($pj_filename)); echo json_encode($pj, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"; } - exit; + exit(); } } diff --git a/src/listactions/la_getjsonrqc.php b/src/listactions/la_getjsonrqc.php index 3754de095..22087c0ca 100644 --- a/src/listactions/la_getjsonrqc.php +++ b/src/listactions/la_getjsonrqc.php @@ -38,6 +38,6 @@ function run(Contact $user, Qrequest $qreq, SearchSelection $ssel) { header("Content-Type: application/json; charset=utf-8"); header("Content-Disposition: attachment; filename=" . mime_quote_string($user->conf->download_prefix . "rqc.json")); echo json_encode($results, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n"; - exit; + exit(); } } diff --git a/src/listactions/la_getreviewbase.php b/src/listactions/la_getreviewbase.php index 53f8639e7..859283480 100644 --- a/src/listactions/la_getreviewbase.php +++ b/src/listactions/la_getreviewbase.php @@ -60,7 +60,7 @@ protected function finish(Contact $user, $texts, $ms) { $zip->message_set()->append_item($mi); } $zip->download(); - exit; + exit(); } } } diff --git a/src/listactions/la_revpref.php b/src/listactions/la_revpref.php index 7bb1b0eac..a7fa7afdc 100644 --- a/src/listactions/la_revpref.php +++ b/src/listactions/la_revpref.php @@ -194,7 +194,7 @@ function run_uploadpref(Contact $user, Qrequest $qreq, SearchSelection $ssel, Ht::submit("cancel", "Cancel", ["formnovalidate" => true]) ], ["class" => "aab aabig"]), "\n"; $qreq->print_footer(); - exit; + exit(); } } } diff --git a/src/multiconference.php b/src/multiconference.php index 21a251132..049a96369 100644 --- a/src/multiconference.php +++ b/src/multiconference.php @@ -169,7 +169,7 @@ static function fail(...$arg) { $j["maintenance"] = true; } echo json_encode_browser($j), "\n"; - exit; + exit(); } http_response_code($status); @@ -184,7 +184,7 @@ static function fail(...$arg) { } echo '
', MessageSet::feedback_html($mis), '
'; $qreq->print_footer(); - exit; + exit(); } /** @return Qrequest */ diff --git a/src/pages/p_api.php b/src/pages/p_api.php index 1976f7d27..f41561eec 100644 --- a/src/pages/p_api.php +++ b/src/pages/p_api.php @@ -144,7 +144,7 @@ static function go_options(NavigationState $nav) { header("Allow: OPTIONS, GET, HEAD, POST"); // XXX other methods? } http_response_code($ok ? 200 : 403); - exit; + exit(); } /** @param NavigationState $nav @@ -167,7 +167,7 @@ static function go_nav($nav, $conf) { http_response_code(404); header("Content-Type: application/json; charset=utf-8"); echo '{"ok": false, "error": "API function missing"}', "\n"; - exit; + exit(); } } if ($_GET["fn"] === "deadlines") { diff --git a/src/pages/p_authorize.php b/src/pages/p_authorize.php index 8302752b7..472d1926a 100644 --- a/src/pages/p_authorize.php +++ b/src/pages/p_authorize.php @@ -279,7 +279,7 @@ private function print_error_exit($m) { $this->qreq->print_header("Sign in", "authorize", ["action_bar" => "", "hide_header" => true, "body_class" => "body-error"]); $this->conf->error_msg($m); $this->qreq->print_footer(); - exit; + exit(); } function go() { diff --git a/src/pages/p_autoassign.php b/src/pages/p_autoassign.php index d1391d2ec..b554aa7b5 100644 --- a/src/pages/p_autoassign.php +++ b/src/pages/p_autoassign.php @@ -571,7 +571,7 @@ function run_try_job() { } $this->conf->error_msg("<5>{$m} conf->selfurl($this->qreq, ["a" => $this->qreq->a]) . "\">Try again"); $this->qreq->print_footer(); - exit; + exit(); } } @@ -630,7 +630,7 @@ function run_job(TokenInfo $tok) { Ht::submit("cancel", "Cancel"), '
'; $qreq->print_footer(); - exit; + exit(); } private function handle_in_progress(TokenInfo $tok) { @@ -654,7 +654,7 @@ private function handle_in_progress(TokenInfo $tok) { Ht::unstash_script("hotcrp.monitor_autoassignment(" . json_encode_browser($this->jobid) . ")"); } $this->qreq->print_footer(); - exit; + exit(); } private function handle_empty_assignment(TokenInfo $tok) { @@ -666,7 +666,7 @@ private function handle_empty_assignment(TokenInfo $tok) { Ht::link("Revise assignment", $this->conf->selfurl($this->qreq, $this->qreq_parameters()), ["class" => "btn btn-primary"]), ''; $this->qreq->print_footer(); - exit; + exit(); } private function handle_download_assignment(TokenInfo $tok) { @@ -677,7 +677,7 @@ private function handle_download_assignment(TokenInfo $tok) { $csvg = $this->conf->make_csvg("assignments"); $aset->make_acsv()->unparse_into($csvg); $csvg->sort(SORT_NATURAL)->emit(); - exit; + exit(); } private function handle_execute(TokenInfo $tok) { diff --git a/src/pages/p_changeemail.php b/src/pages/p_changeemail.php index ca7c081eb..6f72b53de 100644 --- a/src/pages/p_changeemail.php +++ b/src/pages/p_changeemail.php @@ -99,7 +99,7 @@ static function go(Contact $user, Qrequest $qreq) { ''; Ht::stash_script("hotcrp.focus_within(\$(\"#changeemailform\"));window.scroll(0,0)"); $qreq->print_footer(); - exit; + exit(); } } } diff --git a/src/pages/p_doc.php b/src/pages/p_doc.php index 48c9fbcfc..4afab8cb2 100644 --- a/src/pages/p_doc.php +++ b/src/pages/p_doc.php @@ -11,7 +11,7 @@ static private function error($status, $msg, $qreq) { if (str_starts_with($status, "403") && $qreq->user()->is_empty()) { $qreq->user()->escape(); - exit; + exit(); } else if (str_starts_with($status, "5")) { $navpath = $qreq->path(); error_log($qreq->conf()->dbname . ": bad doc $status " @@ -28,7 +28,7 @@ static private function error($status, $msg, $qreq) { $qreq->print_header("Download", null); $qreq->conf()->feedback_msg($ml); $qreq->print_footer(); - exit; + exit(); } } diff --git a/src/pages/p_home.php b/src/pages/p_home.php index a33f68cc0..3024f4d55 100644 --- a/src/pages/p_home.php +++ b/src/pages/p_home.php @@ -36,7 +36,7 @@ static function disabled_request(Contact $user, Qrequest $qreq) { $user->conf->warning_msg($user->conf->_i("account_disabled")); $qreq->print_header("Account disabled", "home", ["action_bar" => ""]); $qreq->print_footer(); - exit; + exit(); } } diff --git a/src/pages/p_log.php b/src/pages/p_log.php index 515377ba0..cf3df134c 100644 --- a/src/pages/p_log.php +++ b/src/pages/p_log.php @@ -256,7 +256,7 @@ function handle_download($leg) { } } $csvg->emit(); - exit; + exit(); } diff --git a/src/pages/p_search.php b/src/pages/p_search.php index 934910b99..d14607b15 100644 --- a/src/pages/p_search.php +++ b/src/pages/p_search.php @@ -479,7 +479,7 @@ static function go($user, $qreq) { $qreq->print_header("Search", "search"); $conf->error_msg($conf->_("<0>You aren’t allowed to search {submissions}")); $qreq->print_footer(); - exit; + exit(); } // paper selection From eb7c0039e7df78b67894c82a87dba324eace2626 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Thu, 9 May 2024 10:38:10 -0400 Subject: [PATCH 47/78] Copyright year nits. --- lib/phpqsession.php | 2 +- lib/qsession.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/phpqsession.php b/lib/phpqsession.php index 2f1b31ce9..2600b495f 100644 --- a/lib/phpqsession.php +++ b/lib/phpqsession.php @@ -1,6 +1,6 @@ Date: Fri, 10 May 2024 10:34:13 -0400 Subject: [PATCH 48/78] Nits. --- lib/dbl.php | 6 +++--- lib/redirect.php | 7 +------ src/conference.php | 2 +- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/dbl.php b/lib/dbl.php index 2759540ff..d8a26c23f 100644 --- a/lib/dbl.php +++ b/lib/dbl.php @@ -350,7 +350,7 @@ static private function query_args($args, $flags, $log_location) { } else if ($args[0] === null && count($args) > 1) { $argpos = 1; } - if ((($flags & self::F_RAW) && count($args) != $argpos + 1) + if ((($flags & self::F_RAW) && count($args) !== $argpos + 1) || (($flags & self::F_APPLY) && count($args) > $argpos + 2)) { trigger_error(self::landmark() . ": wrong number of arguments"); } else if (($flags & self::F_APPLY) @@ -815,7 +815,7 @@ static function is_error($result) { } static private function do_make_result($args, $flags = self::F_ERROR) { - if (count($args) == 1 && !is_string($args[0])) { + if (count($args) === 1 && !is_string($args[0])) { return $args[0]; } else { return self::do_query($args, $flags); @@ -1067,7 +1067,7 @@ function sqlq($value) { function sql_in_int_list($set) { if (empty($set)) { return " is null"; - } else if (count($set) == 1) { + } else if (count($set) === 1) { return "=" . $set[0]; } else { return " in (" . join(", ", $set) . ")"; diff --git a/lib/redirect.php b/lib/redirect.php index 35f230006..638ffcad3 100644 --- a/lib/redirect.php +++ b/lib/redirect.php @@ -39,7 +39,7 @@ function set_session_name(Conf $conf) { $_COOKIE[$sn] = $_COOKIE[$upgrade_sn]; hotcrp_setcookie($upgrade_sn, "", [ "expires" => time() - 3600, "path" => "/", - "domain" => $conf->opt("sessionUpgradeDomain") ?? ($domain ? : ""), + "domain" => $conf->opt("sessionUpgradeDomain") ?? $domain, "secure" => $secure ]); } @@ -76,11 +76,6 @@ function set_session_name(Conf $conf) { } } -/** @deprecated */ -function ensure_session() { - Qrequest::$main_request->open_session(); -} - function unlink_session() { if (($sn = session_name()) && isset($_COOKIE[$sn])) { $params = session_get_cookie_params(); diff --git a/src/conference.php b/src/conference.php index 30badebcf..2dffdb656 100644 --- a/src/conference.php +++ b/src/conference.php @@ -646,7 +646,7 @@ function refresh_options() { $this->opt["paperSite"] = substr($this->opt["paperSite"], 0, -1); } - // assert URLs (general assets, scripts, jQuery) + // asset URLs (general assets, scripts, jQuery) $baseurl = $nav->base_path_relative ?? ""; $this->_assets_url = $this->opt["assetsUrl"] ?? $this->opt["assetsURL"] ?? $baseurl; if ($this->_assets_url !== "" && !str_ends_with($this->_assets_url, "/")) { From 0ef70f9627779af6cb72b30555979b1e5a7464a0 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Fri, 10 May 2024 10:34:24 -0400 Subject: [PATCH 49/78] Avoid overreacting to session fixation. --- lib/qsession.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/qsession.php b/lib/qsession.php index 5eee152a8..c9477bed3 100644 --- a/lib/qsession.php +++ b/lib/qsession.php @@ -77,6 +77,7 @@ function handle_open() { return; } $this->sopen = true; + unset($curv["deletedat"]); foreach ($curv as $k => $v) { $this->set($k, $v); } From 8828590fc22ac50716437633367db1a8d6c4560e Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Mon, 13 May 2024 09:06:56 -0400 Subject: [PATCH 50/78] Remove support for old-style author requests. --- src/options/o_authors.php | 12 ------------ test/t_paperstatus.php | 24 +----------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/src/options/o_authors.php b/src/options/o_authors.php index 452a90ec5..a7cb53f27 100644 --- a/src/options/o_authors.php +++ b/src/options/o_authors.php @@ -106,15 +106,6 @@ function value_save(PaperValue $ov, PaperStatus $ps) { $ps->checkpoint_conflict_values(); return true; } - static private function translate_qreq(Qrequest $qreq) { - $n = 1; - while (isset($qreq["authors:email_{$n}"]) || isset($qreq["auemail{$n}"])) { - $qreq["authors:{$n}:email"] = $qreq["authors:email_{$n}"] ?? $qreq["auemail{$n}"]; - $qreq["authors:{$n}:name"] = $qreq["authors:name_{$n}"] ?? $qreq["auname{$n}"]; - $qreq["authors:{$n}:affiliation"] = $qreq["authors:affiliation_{$n}"] ?? $qreq["auaff{$n}"]; - ++$n; - } - } static private function expand_author(Author $au, PaperInfo $prow) { if ($au->email !== "" && ($aux = $prow->author_by_email($au->email))) { @@ -128,9 +119,6 @@ static private function expand_author(Author $au, PaperInfo $prow) { } } function parse_qreq(PaperInfo $prow, Qrequest $qreq) { - if (!isset($qreq["authors:1:email"])) { - self::translate_qreq($qreq); - } $v = []; $auth = new Author; for ($n = 1; true; ++$n) { diff --git a/test/t_paperstatus.php b/test/t_paperstatus.php index 57b64b75a..2fca1ca5d 100644 --- a/test/t_paperstatus.php +++ b/test/t_paperstatus.php @@ -391,7 +391,7 @@ function test_save_submit_new_paper_empty_contacts() { function test_save_draft_new_paper() { $ps = new PaperStatus($this->u_estrin); // NB old style of entries - xassert($ps->prepare_save_paper_web(new Qrequest("POST", ["title" => "New paper", "abstract" => "This is an abstract\r\n", "has_authors" => "1", "authors:name_1" => "Bobby Flay", "authors:email_1" => "flay@_.com", "has_submission" => 1]), null)); + xassert($ps->prepare_save_paper_web(new Qrequest("POST", ["title" => "New paper", "abstract" => "This is an abstract\r\n", "has_authors" => "1", "authors:1:name" => "Bobby Flay", "authors:1:email" => "flay@_.com", "has_submission" => 1]), null)); xassert_paper_status($ps); xassert($ps->has_change_at("title")); xassert($ps->has_change_at("abstract")); @@ -582,28 +582,6 @@ function test_save_options() { xassert_eqq($newpaper->option(1)->value, 10); } - function test_save_old_authors() { - $ps = new PaperStatus($this->u_estrin); - xassert($ps->prepare_save_paper_web(new Qrequest("POST", ["has_authors" => "1", "auname1" => "Robert Flay", "auemail1" => "flay@_.com"]), $this->newpaper1)); - xassert($ps->has_change_at("authors")); - xassert($ps->execute_save()); - xassert_paper_status($ps); - - $newpaper = $this->u_estrin->checked_paper_by_id($ps->paperId); - xassert($newpaper); - xassert_eqq($newpaper->title, "New paper"); - xassert_eqq($newpaper->abstract, "This is an abstract"); - xassert_eqq($newpaper->abstract(), "This is an abstract"); - xassert_eqq(count($newpaper->author_list()), 1); - $aus = $newpaper->author_list(); - xassert_eqq($aus[0]->firstName, "Robert"); - xassert_eqq($aus[0]->lastName, "Flay"); - xassert_eqq($aus[0]->email, "flay@_.com"); - xassert($newpaper->timeSubmitted <= 0); - xassert($newpaper->timeWithdrawn <= 0); - xassert_eqq($newpaper->option(1)->value, 10); - } - function test_save_new_authors() { $qreq = new Qrequest("POST", ["status:submit" => 1, "has_opt2" => "1", "opt2:1" => "new", "title" => "Paper about mantis shrimp", "has_authors" => "1", "authors:1:name" => "David Attenborough", "authors:1:email" => "atten@_.com", "authors:1:affiliation" => "BBC", "abstract" => "They see lots of colors.", "has_submission" => "1"]); $qreq->set_file("submission", ["name" => "amazing-sample.pdf", "tmp_name" => SiteLoader::find("etc/sample.pdf"), "type" => "application/pdf", "error" => UPLOAD_ERR_OK]); From 46eb2680f0e7810f4a009ee21da308fcdafa78b5 Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Mon, 13 May 2024 09:26:23 -0400 Subject: [PATCH 51/78] Row order nits, document removal nits. --- scripts/script.js | 57 ++++++++++++++++++++++------------- scripts/settings.js | 14 ++++----- src/documentinfo.php | 18 ++++++----- src/options/o_attachments.php | 2 +- src/paperoption.php | 2 +- src/settings/s_options.php | 4 +-- src/settings/s_reviewform.php | 6 ++-- 7 files changed, 61 insertions(+), 42 deletions(-) diff --git a/scripts/script.js b/scripts/script.js index f7514b048..fd1ebb8b1 100644 --- a/scripts/script.js +++ b/scripts/script.js @@ -5535,12 +5535,12 @@ handle_ui.on(".js-autoassign-prepare", function () { function row_fill(row, i, defaults, changes) { ++i; - var ipts, e, m, num = i + "."; + let e, m; + const numstr = i + "."; if ((e = row.querySelector(".row-counter")) - && e.textContent !== num) - e.replaceChildren(num); - ipts = row.querySelectorAll("input, select, textarea"); - for (e of ipts) { + && e.textContent !== numstr) + e.replaceChildren(numstr); + for (e of row.querySelectorAll("input, select, textarea")) { if (!e.name || !(m = /^(.*?)(\d+|\$)(|:.*)$/.exec(e.name)) || m[2] == i) @@ -5566,10 +5566,12 @@ function is_row_interesting(row) { } function row_add(group, before, button) { - var row, id = (button && button.getAttribute("data-row-template")) + const id = (button && button.getAttribute("data-row-template")) || group.getAttribute("data-row-template"); - if (!id || !(row = document.getElementById(id))) + let row; + if (!id || !(row = document.getElementById(id))) { return null; + } if ("content" in row) { row = row.content.cloneNode(true).firstElementChild; } else { @@ -5581,9 +5583,8 @@ function row_add(group, before, button) { } function row_order_defaults(group) { - var ipts = group.querySelectorAll("input, select, textarea"), - e, defaults = {}; - for (e of ipts) { + const defaults = {}; + for (const e of group.querySelectorAll("input, select, textarea")) { if (e.name) defaults[e.name] = input_default_value(e); } @@ -5591,9 +5592,9 @@ function row_order_defaults(group) { } function row_order_drag_confirm(group, defaults) { - var i, row, changes = []; + const changes = []; defaults = defaults || row_order_defaults(group); - for (row = group.firstElementChild, i = 0; + for (let row = group.firstElementChild, i = 0; row; row = row.nextElementSibling, ++i) { row_fill(row, i, defaults, changes); } @@ -5629,7 +5630,7 @@ function row_order_autogrow(group, defaults) { } } else { while (nr > min_rows && nr > 1 && !hasClass(row, "row-order-inserted")) { - let prev_row = row.previousElementSibling; + const prev_row = row.previousElementSibling; if (is_row_interesting(prev_row)) { break; } @@ -5639,7 +5640,7 @@ function row_order_autogrow(group, defaults) { } } } - var ndig = Math.ceil(Math.log10(nr + 1)).toString(); + const ndig = Math.ceil(Math.log10(nr + 1)).toString(); if (group.getAttribute("data-row-counter-digits") !== ndig) { group.setAttribute("data-row-counter-digits", ndig); } @@ -5658,6 +5659,7 @@ handle_ui.on("dragstart.row-order-draghandle", function (evt) { changed && row_order_drag_confirm(group); }).start(evt); }); + hotcrp.dropmenu.add_builder("row-order-draghandle", function () { const row = this.closest(".draggable"), group = row.parentElement; let details = this.closest("details"), menu; @@ -5679,8 +5681,14 @@ hotcrp.dropmenu.add_builder("row-order-draghandle", function () { attr["type"] = "button"; return $e("li", attr.disabled ? "disabled" : "has-link", $e("button", attr, text)); } - menu.append(buttonli("link ui row-order-dragmenu move-up", {disabled: !row.previousElementSibling}, "Move up")); - menu.append(buttonli("link ui row-order-dragmenu move-down", {disabled: !row.nextElementSibling}, "Move down")); + let sib = row.previousElementSibling; + menu.append(buttonli("link ui row-order-dragmenu move-up", { + disabled: !sib || hasClass(sib, "row-order-barrier") + }, "Move up")); + sib = row.nextElementSibling; + menu.append(buttonli("link ui row-order-dragmenu move-down", { + disabled: !sib || hasClass(sib, "row-order-barrier") + }, "Move down")); if (group.hasAttribute("data-row-template")) { const max_rows = +group.getAttribute("data-max-rows") || 0; if (max_rows <= 0 || row_order_count(group) < max_rows) { @@ -5690,13 +5698,19 @@ hotcrp.dropmenu.add_builder("row-order-draghandle", function () { } menu.append(buttonli("link ui row-order-dragmenu remove", {disabled: !row_order_allow_remove(group)}, "Remove")); }); + handle_ui.on("row-order-dragmenu", function () { hotcrp.dropmenu.close(this); - var row = this.closest(".draggable"), sib, group = row.parentElement, + const row = this.closest(".draggable"), group = row.parentElement, defaults = row_order_defaults(group); - if (hasClass(this, "move-up") && (sib = row.previousElementSibling)) { + let sib; + if (hasClass(this, "move-up") + && (sib = row.previousElementSibling) + && !hasClass(sib, "row-order-barrier")) { sib.before(row); - } else if (hasClass(this, "move-down") && (sib = row.nextElementSibling)) { + } else if (hasClass(this, "move-down") + && (sib = row.nextElementSibling) + && !hasClass(sib, "row-order-barrier")) { sib.after(row); } else if (hasClass(this, "remove") && row_order_allow_remove(group)) { row.remove(); @@ -5707,6 +5721,7 @@ handle_ui.on("row-order-dragmenu", function () { } row_order_drag_confirm(group, defaults); }); + handle_ui.on("row-order-append", function () { var group = document.getElementById(this.getAttribute("data-rowset")), nr, row; @@ -11973,7 +11988,7 @@ handle_ui.on("js-remove-document", function () { $doc.find(".document-stamps, .document-shortformat").removeClass("hidden"); $(this).removeClass("undelete").html("Delete"); } else { - $(hidden_input(doce.getAttribute("data-document-name") + ":remove", "1", {"class": "document-remover", "data-default-value": ""})).appendTo($doc.find(".document-actions")).trigger("change"); + $(hidden_input(doce.getAttribute("data-document-name") + ":delete", "1", {"class": "document-remover", "data-default-value": ""})).appendTo($doc.find(".document-actions")).trigger("change"); if (!$en.find("del").length) $en.wrapInner(""); $doc.find(".document-uploader").trigger("hotcrp-change-document"); @@ -12311,7 +12326,7 @@ edit_conditions.document_count = function (ec, form) { if (this.getAttribute("data-dtype") == ec.dtype) { var name = this.getAttribute("data-document-name"), preve = form.elements[name], - removee = form.elements[name + ":remove"], + removee = form.elements[name + ":delete"], filee = form.elements[name + ":file"], uploade = form.elements[name + ":upload"]; if (!removee || !removee.value) { diff --git a/scripts/settings.js b/scripts/settings.js index 961e80061..668190309 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -141,10 +141,10 @@ function settings_field_order(parentid) { continue; } ++i; - if ((e = n.querySelector(".moveup"))) { + if ((e = n.querySelector(".move-up"))) { e.disabled = movedown === null; } - if ((e = n.querySelector(".movedown"))) { + if ((e = n.querySelector(".move-down"))) { e.disabled = false; movedown = e; } @@ -307,9 +307,9 @@ function sf_order() { handle_ui.on("js-settings-sf-move", function (evt) { var sf = this.closest(".settings-sf"); - if (hasClass(this, "moveup") && sf.previousSibling) { + if (hasClass(this, "move-up") && sf.previousSibling) { sf.parentNode.insertBefore(sf, sf.previousSibling); - } else if (hasClass(this, "movedown") && sf.nextSibling) { + } else if (hasClass(this, "move-down") && sf.nextSibling) { sf.parentNode.insertBefore(sf, sf.nextSibling.nextSibling); } else if (hasClass(this, "delete")) { var msg, x; @@ -878,9 +878,9 @@ function rf_render_view(fld, example) { function rf_move() { var rf = this.closest(".settings-rf"); - if (hasClass(this, "moveup") && rf.previousSibling) { + if (hasClass(this, "move-up") && rf.previousSibling) { rf.parentNode.insertBefore(rf, rf.previousSibling); - } else if (hasClass(this, "movedown") && rf.nextSibling) { + } else if (hasClass(this, "move-down") && rf.nextSibling) { rf.parentNode.insertBefore(rf, rf.nextSibling.nextSibling); } hotcrp.tooltip.close(this); @@ -926,7 +926,7 @@ function rf_append(fld) { field_instantiate($f.children(".settings-xf-edit")[0], rffinder, rftype.name, rfproperties); $f.find(".js-settings-rf-delete").on("click", rf_delete); $f.find(".js-settings-rf-move").on("click", rf_move); - $f.find(".rf-id").val(fld.id); + $f.find(".is-id").val(fld.id); $f.appendTo("#settings-rform"); rf_fill(pos, fld, true); $f.awaken(); diff --git a/src/documentinfo.php b/src/documentinfo.php index 9a218a7ce..27b1c405b 100644 --- a/src/documentinfo.php +++ b/src/documentinfo.php @@ -605,6 +605,11 @@ function release_redundant_content() { } + /** @return PaperOption */ + function option() { + return $this->conf->option_by_id($this->documentType); + } + /** @return bool */ function content_available() { return $this->content !== null @@ -1107,11 +1112,10 @@ function save($savef = 0) { } // validate - if (!$this->filterType) { - $opt = $this->conf->option_by_id($this->documentType); - if ($opt && !$opt->validate_document($this)) { - return false; - } + if (!$this->filterType + && ($opt = $this->option()) + && !$opt->validate_document($this)) { + return false; } // store @@ -1553,7 +1557,7 @@ function export_filename($filters = null, $flags = 0) { assert(!!$this->filename); return $this->filename; } else { - $o = $this->conf->option_by_id($this->documentType); + $o = $this->option(); if ($o && $o->nonpaper && $this->paperId < 0) { $fn .= $o->dtype_name(); $oabbr = ""; @@ -1644,7 +1648,7 @@ function link_html($html = "", $flags = 0, $filters = null) { if ($this->documentType == DTYPE_FINAL || ($this->documentType > 0 - && ($o = $this->conf->option_by_id($this->documentType)) + && ($o = $this->option()) && $o->is_final())) { $suffix = "f"; } diff --git a/src/options/o_attachments.php b/src/options/o_attachments.php index 8aa305fe7..028b352ff 100644 --- a/src/options/o_attachments.php +++ b/src/options/o_attachments.php @@ -94,7 +94,7 @@ static function parse_qreq_prefix(PaperInfo $prow, Qrequest $qreq, } } } - if ($qreq["{$name}:remove"]) { + if ($qreq["{$name}:delete"] || $qreq["{$name}:remove"] /* compat */) { continue; } if (DocumentInfo::has_request_for($qreq, $name)) { diff --git a/src/paperoption.php b/src/paperoption.php index f93fe234a..b414f939e 100644 --- a/src/paperoption.php +++ b/src/paperoption.php @@ -1446,7 +1446,7 @@ function parse_qreq(PaperInfo $prow, Qrequest $qreq) { } } return $ov; - } else if ($qreq["{$fk}:remove"]) { + } else if ($qreq["{$fk}:delete"] || $qreq["{$fk}:remove"] /* compat */) { return PaperValue::make($prow, $this); } else { return null; diff --git a/src/settings/s_options.php b/src/settings/s_options.php index b7b246ddd..c3911789a 100644 --- a/src/settings/s_options.php +++ b/src/settings/s_options.php @@ -361,8 +361,8 @@ function print_actions(SettingValues $sv) { echo MessageSet::feedback_html([MessageItem::marked_note("<0>This field always appears first on the submission form and cannot be deleted.")]); } else { echo '', - Ht::button(Icons::ui_use("movearrow0"), ["class" => "btn-licon ui js-settings-sf-move moveup need-tooltip", "aria-label" => "Move up in display order"]), - Ht::button(Icons::ui_use("movearrow2"), ["class" => "btn-licon ui js-settings-sf-move movedown need-tooltip", "aria-label" => "Move down in display order"]), + Ht::button(Icons::ui_use("movearrow0"), ["class" => "btn-licon ui js-settings-sf-move move-up need-tooltip", "aria-label" => "Move up in display order"]), + Ht::button(Icons::ui_use("movearrow2"), ["class" => "btn-licon ui js-settings-sf-move move-down need-tooltip", "aria-label" => "Move down in display order"]), ''; if ($this->sfs->option_id > 0) { echo Ht::button(Icons::ui_use("trash"), ["class" => "btn-licon ui js-settings-sf-move delete need-tooltip", "aria-label" => "Delete", "data-exists-count" => $this->option_use_count($this->sfs->option_id)]); diff --git a/src/settings/s_reviewform.php b/src/settings/s_reviewform.php index 5b3a4d9c6..5ec7a4b38 100644 --- a/src/settings/s_reviewform.php +++ b/src/settings/s_reviewform.php @@ -576,12 +576,12 @@ static function print_presence(SettingValues $sv) { static function print_actions(SettingValues $sv) { echo '
', - Ht::button(Icons::ui_use("movearrow0"), ["id" => "rf/\$/moveup", "class" => "btn-licon ui js-settings-rf-move moveup need-tooltip", "aria-label" => "Move up in display order"]), - Ht::button(Icons::ui_use("movearrow2"), ["id" => "rf/\$/movedown", "class" => "btn-licon ui js-settings-rf-move movedown need-tooltip", "aria-label" => "Move down in display order"]), + Ht::button(Icons::ui_use("movearrow0"), ["id" => "rf/\$/moveup", "class" => "btn-licon ui js-settings-rf-move move-up need-tooltip", "aria-label" => "Move up in display order"]), + Ht::button(Icons::ui_use("movearrow2"), ["id" => "rf/\$/movedown", "class" => "btn-licon ui js-settings-rf-move move-down need-tooltip", "aria-label" => "Move down in display order"]), '', Ht::button(Icons::ui_use("trash"), ["class" => "btn-licon ui js-settings-rf-delete need-tooltip", "aria-label" => "Delete"]), Ht::hidden("rf/\$/order", "0", ["id" => "rf/\$/order", "class" => "is-order"]), - Ht::hidden("rf/\$/id", "", ["id" => "rf/\$/id", "class" => "rf-id"]), + Ht::hidden("rf/\$/id", "", ["id" => "rf/\$/id", "class" => "is-id"]), "
"; } From 3a3481b3b0f93943cc4c6baa8bd6d30e3368fc3e Mon Sep 17 00:00:00 2001 From: Eddie Kohler Date: Mon, 13 May 2024 11:48:34 -0400 Subject: [PATCH 52/78] Add random name to settings form? A user using Firefox is reporting problems that might be explained by FF incorrectly filling in fields on reload. --- src/pages/p_settings.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/p_settings.php b/src/pages/p_settings.php index 504ff11f7..f0af0dc0c 100644 --- a/src/pages/p_settings.php +++ b/src/pages/p_settings.php @@ -97,9 +97,11 @@ function print($group, $qreq) { echo Ht::unstash(), // clear out other script references $this->conf->make_script_file("scripts/settings.js"), "\n", - Ht::form($this->conf->hoturl("=settings", "group={$group}"), - ["id" => "f-settings", "class" => "need-diff-check need-unload-protection"]), - + Ht::form($this->conf->hoturl("=settings", "group={$group}"), [ + "id" => "f-settings", + "name" => base64_encode(random_bytes(8)), // prevent FF from autofilling on reload + "class" => "need-diff-check need-unload-protection" + ]), '