Skip to content

Commit

Permalink
Introduce ViewOptionList and ViewOptionSchema
Browse files Browse the repository at this point in the history
- Methods talking about “decorations” are renamed to discuss
  view options.
- Columns define a “view option schema”, which normalizes aliases
  and can handle defaults.
- ViewOptionList class encapsulates a list of view options.

Also rewrite Contact::name_for to not cache the generated name.
Just cache the tags. Might regret this but I doubt Text::nameo
is expensive?
  • Loading branch information
kohler committed Oct 10, 2024
1 parent 52d19b6 commit b860431
Show file tree
Hide file tree
Showing 26 changed files with 679 additions and 450 deletions.
2 changes: 2 additions & 0 deletions batch/makedist.sh
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,8 @@ src/userinfo/u_developer.php
src/userinfo/u_security.php
src/userstatus.php
src/viewcommand.php
src/viewoptionlist.php
src/viewoptionschema.php
src/xtparams.php
devel/hotcrp.vim
Expand Down
136 changes: 54 additions & 82 deletions lib/column.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ class Column {
public $is_visible = false;
/** @var bool */
public $has_content = false;
/** @var ?list<string> */
protected $decorations;
/** @var ?ViewOptionList */
protected $view_options;

/** @param object|array $arg */
function __construct($arg) {
Expand Down Expand Up @@ -73,80 +73,75 @@ function __construct($arg) {
$this->__source_order = $arg["__source_order"] ?? null;
}

/** @return list<string> */
function decorations() {
return $this->decorations ?? [];
/** @return ?ViewOptionList */
function view_options() {
return $this->view_options;
}

/** @param string $decor
* @return int|false */
function decoration_index($decor) {
$l = strlen($decor);
foreach ($this->decorations ?? [] as $i => $s) {
if (str_starts_with($s, $decor)
&& (strlen($s) === $l || $s[$l] === "=")) {
return $i;
}
}
return false;
/** @param string $n
* @return mixed */
function view_option($n) {
return $this->view_options ? $this->view_options->get($n) : null;
}

/** @return bool */
function has_decoration($decor) {
return $this->decoration_index($decor) !== false;
/** @return list<string|object> */
function view_option_schema() {
return [];
}

/** @return ?string */
function decoration_value($decor) {
if (($i = $this->decoration_index($decor)) !== false) {
/** @phan-suppress-next-line PhanTypeArraySuspiciousNullable */
$s = $this->decorations[$i];
$l = strlen($decor);
return strlen($s) === $l ? "" : substr($s, $l + 1);
} else {
return null;
/** @var ViewOptionSchema */
static private $base_schema;

/** @param ?ViewOptionlist $volist
* @return $this */
function add_view_options($volist) {
if (!$volist || $volist->is_empty()) {
return $this;
}
}

/** @param string $decor
* @return bool */
function add_decoration($decor) {
if ($decor === "row" || $decor === "column") {
/** @phan-suppress-next-line PhanAccessReadOnlyProperty */
$this->as_row = $decor === "row";
$dd = $this->prefer_row !== $this->as_row ? $decor : null;
return $this->__add_decoration($dd, ["column", "row"]);
// get schema
if (self::$base_schema === null) {
self::$base_schema = new ViewOptionSchema;
self::$base_schema->define("display=row col,column");
self::$base_schema->define("sort=asc,ascending,up desc,descending,down forward reverse");
}
$schema = self::$base_schema;
foreach ($this->view_option_schema() as $x) {
if ($schema === self::$base_schema) {
$schema = clone $schema;
}
$schema->define($x);
}

$sp = array_search($decor, [
"up", "asc", "ascending", "down", "desc", "descending", "forward", "reverse"
]);
if ($sp !== false) {
if ($sp < 3) {
// add options
$this->view_options = $this->view_options ?? new ViewOptionList;
$this->view_options->append_validate($volist, $schema);

// analyze options
if (($v = $this->view_option("display")) !== null) {
/** @phan-suppress-next-line PhanAccessReadOnlyProperty */
$this->as_row = $v === "row";
if ($this->as_row === $this->prefer_row) {
$this->view_options->remove("display");
}
}
if (($v = $this->view_option("sort")) !== null) {
if ($v === "asc") {
$this->sort_descending = false;
} else if ($sp < 6) {
} else if ($v === "desc") {
$this->sort_descending = true;
} else if ($sp === 6) {
} else if ($v === "forward") {
$this->sort_descending = $this->default_sort_descending();
} else {
$this->sort_descending = !$this->default_sort_descending();
}
return $this->__add_decoration($this->sort_decoration(), ["asc", "desc"]);
}

if ($decor === "by") {
return true;
} else {
return false;
if (($ss = $this->sort_option())) {
$this->view_options->add("sort", $ss);
} else {
$this->view_options->remove("sort");
}
}
}

/** @param ?list<string> $decors
* @return $this */
function add_decorations($decors) {
foreach ($decors ?? [] as $decor) {
$this->add_decoration($decor);
}
return $this;
}

Expand All @@ -156,33 +151,10 @@ function default_sort_descending() {
}

/** @return string */
function sort_decoration() {
function sort_option() {
if ($this->sort_descending === $this->default_sort_descending()) {
return "";
}
return $this->sort_descending ? "desc" : "asc";
}

/** @param ?string $add
* @param ?list<?string> $remove
* @return true */
protected function __add_decoration($add, $remove = []) {
foreach ($remove as $s) {
if ($s !== null && ($i = $this->decoration_index($s)) !== false) {
array_splice($this->decorations, $i, 1);
}
}
if ($add !== null && $add !== "") {
$addx = $add;
if (($eq = strpos($add, "=")) !== false) {
$addx = substr($add, 0, $eq);
}
if (($i = $this->decoration_index($addx)) !== false) {
$this->decorations[$i] = $add;
} else {
$this->decorations[] = $add;
}
}
return true;
}
}
11 changes: 4 additions & 7 deletions lib/scoreinfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,16 @@ class ScoreInfo {
static private $score_sorts = [
"counts", "average", "median", "variance", "maxmin", "my"
];
static private $score_sort_parser = "0 C,0 M,0 count,0 counts,1 A,1 average,1 avg,1 av,1 ave,2 E,2 median,2 med,3 V,3 variance,3 var,4 D,4 maxmin,4 max-min,5 Y,5 my,5 myscore,";
/** @readonly */
static public $score_sort_enum = "counts,C,M,count average,A,avg,av,ave,mean median,E,med variance,V,var maxmin,D,max-min my,Y,myscore";

/** @param ?string $x
* @return null|'count'|'average'|'median'|'variance'|'maxmin'|'my' */
* @return null|'counts'|'average'|'median'|'variance'|'maxmin'|'my' */
static function parse_score_sort($x) {
if ($x === null || in_array($x, self::$score_sorts)) {
return $x;
} else if (($p = strpos(self::$score_sort_parser, " {$x},")) !== false
&& strpos($x, " ") === false) {
return self::$score_sorts[(int) self::$score_sort_parser[$p - 1]];
} else {
return null;
}
return ViewOptionSchema::validate_enum($x, self::$score_sort_enum);
}

/** @return list<string> */
Expand Down
124 changes: 75 additions & 49 deletions src/contact.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@
// contact.php -- HotCRP helper class representing system users
// Copyright (c) 2006-2024 Eddie Kohler; see LICENSE.

class ContactDecorations {
/** @var string */
public $colors;
/** @var string */
public $decorations;

/** @param string $colors
* @param string $decorations */
function __construct($colors, $decorations) {
$this->colors = $colors;
$this->decorations = $decorations;
}
}

class Contact implements JsonSerializable {
/** @var int */
static public $rights_version = 1;
Expand Down Expand Up @@ -124,8 +138,8 @@ class Contact implements JsonSerializable {

/** @var ?array<int,int> */
private $_topic_interest_map;
/** @var array<string,string> */
private $_name_for_map = [];
/** @var array<int,ContactDecorations> */
private $_name_decorations_map = [];

// Roles
const ROLE_PC = 0x0001; // value matters
Expand Down Expand Up @@ -562,9 +576,12 @@ function confirmed_orcid() {
// A unit of 1 === first, 2 === last, 3 === email, 4 === affiliation.
// Least significant bits === most important sort.

/** @param ?list<string> $args
const SORTSPEC_FIRST = 0321;
const SORTSPEC_LAST = 0312;

/** @param ?string ...$args
* @return int */
static function parse_sortspec(Conf $conf, $args) {
static function parse_sortspec(Conf $conf, ...$args) {
$r = $seen = $shift = 0;
while (!empty($args)) {
$w = array_shift($args);
Expand All @@ -590,7 +607,7 @@ static function parse_sortspec(Conf $conf, $args) {
}
}
if ($r === 0) { // default
$r = $conf->sort_by_last ? 0312 : 0321;
$r = $conf->sort_by_last ? self::SORTSPEC_LAST : self::SORTSPEC_FIRST;
} else if (($seen & 016) === 002) { // first -> first last email
$r |= 032 << $shift;
} else if (($seen & 016) === 004) { // last -> last first email
Expand Down Expand Up @@ -1201,69 +1218,78 @@ function completion_items() {
return $items;
}

/** @param ''|'t'|'r'|'ra'|'rn' $pfx
* @param Author|Contact $user */
private function calculate_name_for($pfx, $user) {
$flags = NAME_P;
if (($user->nameAmbiguous ?? false) || $pfx === "ra") {
$flags |= NAME_E;
}
$n = Text::nameo($user, $flags);
if ($pfx !== "t") {
$n = htmlspecialchars($n);
}
if (($pfx === "r" || $pfx === "ra" || $pfx === "rn")
&& ($this->isPC || $this->tracker_kiosk_state > 0)) {
$dt = $this->conf->tags();
$ctflags = ($dt->has_role_decoration ? self::CTFLAG_ROLES : 0)
| ($pfx !== "rn" ? self::CTFLAG_DISABLED : 0);
$act = self::all_user_tags_for($user, $ctflags);
if ($act !== ""
&& $this->can_view_user_tags()
&& ($viewable = $dt->censor(TagMap::CENSOR_VIEW, $act, $this, null))) {
if (($colors = $dt->color_classes($viewable))) {
$n = "<span class=\"{$colors} taghh\">{$n}</span>";
}
if ($dt->has(TagInfo::TFM_DECORATION)) {
$n .= (new Tagger($this))->unparse_decoration_html($viewable, Tagger::DECOR_USER);
}
}
}
return $n;
}

/** @param ''|'t'|'r'|'ra'|'rn' $pfx
/** @param 'n'|'t'|'r'|'ra'|'rn' $type
* @param ReviewInfo|Contact|int $x
* @param int $flags
* @return mixed */
function name_for($pfx, $x) {
function name_for($type, $x, $flags = 0) {
$uid = is_int($x) ? $x : $x->contactId;
$key = $pfx . $uid;
if (isset($this->_name_for_map[$key])) {
return $this->_name_for_map[$key];
}

if ($uid === $this->contactId) {
$u = $this;
} else if (is_int($x)) {
$u = $this->conf->user_by_id($uid, USER_SLICE);
} else if ($x instanceof ReviewInfo) {
$u = $x->reviewer();
if ($x->nameAmbiguous && $pfx === "r" && !$u->nameAmbiguous) {
return $this->name_for("ra", $u);
if ($x->nameAmbiguous && $type === "r" && !$u->nameAmbiguous) {
$type = "ra";
}
} else {
$u = $x;
}

$n = $u ? $this->calculate_name_for($pfx, $u) : "";
$this->_name_for_map[$key] = $n;
if (!$u) {
return "";
}

$userdecor = null;
if ($type[0] === "r"
&& ($this->isPC || $this->tracker_kiosk_state > 0)
&& $this->can_view_user_tags()) {
if (array_key_exists($uid, $this->_name_decorations_map)) {
$userdecor = $this->_name_decorations_map[$uid];
} else {
$dt = $this->conf->tags();
$ctflags = ($dt->has_role_decoration ? self::CTFLAG_ROLES : 0)
| ($type !== "rn" ? self::CTFLAG_DISABLED : 0);
$act = self::all_user_tags_for($u, $ctflags);
if ($act !== ""
&& ($viewable = $dt->censor(TagMap::CENSOR_VIEW, $act, $this, null))) {
$colors = $dt->color_classes($viewable);
if ($dt->has(TagInfo::TFM_DECORATION)) {
$decorations = (new Tagger($this))->unparse_decoration_html($viewable, Tagger::DECOR_USER);
} else {
$decorations = "";
}
if ($colors !== "" || $decorations !== "") {
$userdecor = new ContactDecorations($colors, $decorations);
}
}
$this->_name_decorations_map[$uid] = $userdecor;
}
}

$flags |= NAME_P;
if (($u->nameAmbiguous ?? false) || $type === "ra") {
$flags |= NAME_E;
}

$n = Text::nameo($u, $flags);
if ($type !== "t") {
$n = htmlspecialchars($n);
}
if ($userdecor !== null) {
if ($userdecor->colors !== "") {
$n = "<span class=\"{$userdecor->colors} taghh\">{$n}</span>";
}
$n .= $userdecor->decorations;
}
return $n;
}

/** @param int|Contact|ReviewInfo $x
* @return string */
function name_html_for($x) {
return $this->name_for("", $x);
return $this->name_for("n", $x);
}

/** @param int|Contact|ReviewInfo $x
Expand All @@ -1275,7 +1301,7 @@ function name_text_for($x) {
/** @param int|Contact|ReviewInfo $x
* @return string */
function reviewer_html_for($x) {
return $this->name_for($this->isPC ? "r" : "", $x);
return $this->name_for($this->isPC ? "r" : "n", $x);
}

/** @param int|Contact|ReviewInfo $x
Expand Down
Loading

0 comments on commit b860431

Please sign in to comment.