Skip to content

Commit

Permalink
API spec for trackerconfig endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
kohler committed Sep 12, 2024
1 parent 562caeb commit d809172
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 197 deletions.
1 change: 1 addition & 0 deletions batch/makedist.sh
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ src/api/api_settings.php
src/api/api_specvalidator.php
src/api/api_taganno.php
src/api/api_tags.php
src/api/api_trackerconfig.php
src/api/api_upload.php
src/api/api_user.php
src/apihelpers.php
Expand Down
2 changes: 1 addition & 1 deletion etc/apifunctions.json
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@
},
{
"name": "trackerconfig", "post": true,
"function": "MeetingTracker::trackerconfig_api"
"function": "TrackerConfig_API::run"
},
{
"name": "trackerstatus", "get": true, "post": true,
Expand Down
34 changes: 17 additions & 17 deletions scripts/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -3635,15 +3635,15 @@ function tracker_refresh() {
handle_ui.on("js-tracker", function (evt) {
let $pu, trno = 1, elapsed_timer;
function make_tracker(tr) {
const trp = "tr" + trno, ktrp = "k-tr" + trno,
const trp = "tr/" + trno, ktrp = "k-tr/" + trno,
$t = $e("fieldset", {class: "tracker-group", "data-index": trno, "data-trackerid": tr.trackerid},
$e("legend", "mb-1",
$e("input", {id: ktrp + "-name", type: "text", name: trp + "-name", size: 24, class: "want-focus need-autogrow", value: tr.name || "", placeholder: tr.is_new ? "New tracker" : "Unnamed tracker"})),
hidden_input(trp + "-id", tr.trackerid));
$e("input", {id: ktrp + "/name", type: "text", name: trp + "/name", size: 24, class: "want-focus need-autogrow", value: tr.name || "", placeholder: tr.is_new ? "New tracker" : "Unnamed tracker"})),
hidden_input(trp + "/id", tr.trackerid));
if (tr.trackerid === "new" && siteinfo.paperid)
$t.append(hidden_input(trp + "-p", siteinfo.paperid));
$t.append(hidden_input(trp + "/p", siteinfo.paperid));
if (tr.listinfo)
$t.append(hidden_input(trp + "-listinfo", tr.listinfo));
$t.append(hidden_input(trp + "/listinfo", tr.listinfo));
let vis = tr.visibility || "", vistype;
if (vis === "+none" || vis === "none") {
vistype = "none";
Expand All @@ -3655,17 +3655,17 @@ handle_ui.on("js-tracker", function (evt) {
}
const gvis = (dl.tracker && dl.tracker.global_visibility) || "",
vismap = [["", "Whole PC"], ["+", "PC members with tag"], ["-", "PC members without tag"]],
vissel = $e("select", {id: ktrp + "-vistype", name: trp + "-vistype", class: "uich js-foldup", "data-default-value": vistype});
vissel = $e("select", {id: ktrp + "/visibility_type", name: trp + "/visibility_type", class: "uich js-foldup", "data-default-value": vistype});
if (hotcrp.status.is_admin) {
vismap.push(["none", "Administrators only"]);
}
for (const v of vismap) {
vissel.append($e("option", {value: v[0], selected: v[0] === vistype, disabled: gvis === "+none" && v[0] !== "none"}, v[1]));
}
$t.append($e("div", {class: "entryi has-fold fold" + (vistype === "+" || vistype === "-" ? "o" : "c"), "data-fold-values": "+ -"},
$e("label", {for: ktrp + "-vistype"}, "PC visibility"),
$e("label", {for: ktrp + "/visibility_type"}, "PC visibility"),
$e("div", "entry", $e("span", "select", vissel),
$e("input", {type: "text", name: trp + "-vis", value: vis.substring(1), placeholder: "(tag)", class: "need-suggest need-autogrow pc-tags fx ml-2"}))));
$e("input", {type: "text", name: trp + "/visibility", value: vis.substring(1), placeholder: "(tag)", class: "need-suggest need-autogrow pc-tags fx ml-2"}))));
if (gvis) {
let gvist;
if (gvis === "+none")
Expand All @@ -3680,8 +3680,8 @@ handle_ui.on("js-tracker", function (evt) {
}
$t.append($e("div", "entryi", $e("label"),
$e("div", "entry", $e("label", "checki",
$e("span", "checkc", hidden_input("has_" + trp + "-hideconflicts", 1),
$e("input", {name: trp + "-hideconflicts", value: 1, type: "checkbox", checked: !!tr.hide_conflicts})),
$e("span", "checkc", hidden_input("has_" + trp + "/hideconflicts", 1),
$e("input", {name: trp + "/hideconflicts", value: 1, type: "checkbox", checked: !!tr.hide_conflicts})),
"Hide conflicted papers"))));
if (tr.start_at) {
$t.append($e("div", "entryi", $e("label", null, "Elapsed time"),
Expand All @@ -3701,18 +3701,18 @@ handle_ui.on("js-tracker", function (evt) {
a.push(ids.join(" "));
}
$t.append($e("div", "entryi", $e("label", null, "Order"),
$e("div", "entry", hidden_input(trp + "-p", "", {disabled: true}), ...a)));
$e("div", "entry", hidden_input(trp + "/p", "", {disabled: true}), ...a)));
}
} catch (e) {
}
if (tr.start_at) {
$t.append($e("div", "entryi", $e("label"),
$e("div", "entry",
$e("label", "checki d-inline-block mr-3",
$e("span", "checkc", $e("input", {name: trp + "-hide", value: 1, type: "checkbox", checked: !!wstor.site(true, "hotcrp-tracking-hide-" + tr.trackerid)})),
$e("span", "checkc", $e("input", {name: trp + "/hide", value: 1, type: "checkbox", checked: !!wstor.site(true, "hotcrp-tracking-hide-" + tr.trackerid)})),
"Hide on this tab"),
$e("label", "checki d-inline-block",
$e("span", "checkc", $e("input", {name: trp + "-stop", value: 1, type: "checkbox"})),
$e("span", "checkc", $e("input", {name: trp + "/stop", value: 1, type: "checkbox"})),
"Stop"))));
}
++trno;
Expand Down Expand Up @@ -3772,20 +3772,20 @@ handle_ui.on("js-tracker", function (evt) {
$pu.find(".tracker-group").each(function () {
var trno = this.getAttribute("data-index"),
id = this.getAttribute("data-trackerid"),
e = f["tr" + trno + "-hide"];
e = f["tr/" + trno + "/hide"];
if (e)
hiding[id] = e.checked;
});

// mark differences
var trd = {};
$pu.find("input, select, textarea").each(function () {
var m = this.name.match(/^tr(\d+)/);
var m = this.name.match(/^tr\/(\d+)/);
if (m && input_differs(this))
trd[m[1]] = true;
});
for (var i in trd) {
f.appendChild(hidden_input("tr" + i + "-changed", "1", {class: "tracker-changemark"}));
f.appendChild(hidden_input("tr/" + i + "/changed", "1", {class: "tracker-changemark"}));
}

$.post(hoturl("=api/trackerconfig"),
Expand Down Expand Up @@ -3846,7 +3846,7 @@ handle_ui.on("js-tracker", function (evt) {
start();
} else {
$.post(hoturl("=api/trackerconfig"),
{"tr1-id": "new", "tr1-listinfo": document.body.getAttribute("data-hotlist"), "tr1-p": siteinfo.paperid, "tr1-vis": wstor.site(false, "hotcrp-tracking-visibility")},
{"tr/1/id": "new", "tr/1/listinfo": document.body.getAttribute("data-hotlist"), "tr/1/p": siteinfo.paperid, "tr/1/visibility": wstor.site(false, "hotcrp-tracking-visibility")},
make_submit_success({}, "new"));
}
});
Expand Down
255 changes: 255 additions & 0 deletions src/api/api_trackerconfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
<?php
// api_trackerconfig.php -- HotCRP trackerconfig API calls
// Copyright (c) 2008-2024 Eddie Kohler; see LICENSE.

class TrackerConfig_API {
/** @var Conf
* @readonly */
public $conf;
/** @var Contact
* @readonly */
public $user;
/** @var Qrequest
* @readonly */
public $qreq;
/** @var bool
* @readonly */
public $translated;
/** @var Tagger */
private $tagger;
/** @var list<MessageItem> */
private $ml = [];

function __construct(Contact $user, Qrequest $qreq) {
$this->conf = $user->conf;
$this->user = $user;
$this->qreq = $qreq;
$this->tagger = new Tagger($user);
if (($this->translated = isset($qreq["tr1-id"]))) {
$this->translate_qreq();
}
}

private function translate_qreq() {
$qreq = $this->qreq;
for ($i = 1; isset($qreq["tr{$i}-id"]); ++$i) {
foreach (["id", "name", "logo", "hideconflicts", "listinfo", "p", "changed", "stop"] as $sfx) {
$qreq["tr/{$i}/{$sfx}"] = $qreq["tr{$i}-{$sfx}"];
}
if (isset($qreq["tr{$i}-vistype"])) {
$qreq["tr/{$i}/visibility_type"] = $qreq["tr{$i}-vistype"];
}
if (isset($qreq["tr{$i}-vis"])) {
$qreq["tr/{$i}/visibility"] = $qreq["tr{$i}-vis"];
}
if (isset($qreq["has_tr{$i}-hideconflicts"])) {
$qreq["has_tr/{$i}/hideconflicts"] = $qreq["has_tr{$i}-hideconflicts"];
}
}
}

/** @param int $i
* @param string $sfx
* @param string $msg */
private function error_at_sfx($i, $sfx, $msg) {
if ($this->translated) {
$field = "tr{$i}-" . ($sfx === "visibility" ? "vis" : $sfx);
} else {
$field = "tr/{$i}/{$sfx}";
}
$this->ml[] = MessageItem::error_at($field, $msg);
}

/** @param int $i
* @return ?string */
private function visibility($i) {
$qreq = $this->qreq;
$vis = $qreq["tr/{$i}/visibility"];
if (!isset($vis)) {
return null;
}
$vis = trim($vis);

$vperm = "";
if ($vis !== ""
&& ($vis[0] === "+" || $vis[0] === "-")
&& !isset($qreq["tr/{$i}/visibility_type"])) {
$vistype = $vis[0];
$vis = ltrim(substr($vis, 1));
} else {
$vistype = trim($qreq["tr/{$i}/visibility_type"] ?? "");
}
if (str_starts_with($vis, "#")) {
$vis = substr($vis, 1);
}
if (strcasecmp($vistype, "none") === 0
|| ($vistype === "+" && strcasecmp($vis, "none") === 0)) {
$vperm = "+none";
} else if ($vistype === ""
|| ($vistype === "+" && strcasecmp($vis, "pc")) === 0) {
// $vperm === ""
} else if ($vistype !== "+" && $vistype !== "-") {
$this->error_at_sfx($i, "visibility", "<0>Internal error on visibility type");
} else if ($vis === ""
|| strcasecmp($vis, "pc") === 0) {
$this->error_at_sfx($i, "visibility", "<0>PC tag required");
} else if (($vt = $this->tagger->check($vis, Tagger::NOPRIVATE | Tagger::NOVALUE))) {
if (!$this->conf->pc_tag_exists($vt)) {
$this->error_at_sfx($i, "visibility", "<0>Unknown PC tag");
}
$vperm = $vistype . $vt;
} else {
$this->error_at_sfx($i, "visibility", $this->tagger->error_ftext(true));
}
if ($vperm !== ""
&& !$this->user->privChair
&& !$this->user->has_permission($vis)) {
$this->error_at_sfx($i, "visibility", "<0>You may not configure a tracker that you wouldn’t be able to see. Try “Whole PC”.");
}
return $vperm;
}

/** @return JsonResult */
function go() {
$qreq = $this->qreq;
$tracker = MeetingTracker::lookup($this->conf);
$position_at = $tracker->next_position_at();
$changed = false;
$new_trackerid = false;
$qreq->open_session();

for ($i = 1; isset($qreq["tr/{$i}/id"]); ++$i) {
// Parse arguments
$trackerid = $qreq["tr/{$i}/id"];
if (ctype_digit($trackerid)) {
$trackerid = intval($trackerid);
}

$name = $qreq["tr/{$i}/name"];
if (isset($name)) {
$name = simplify_whitespace($name);
}

$logo = $qreq["tr/{$i}/logo"];
if (isset($logo)) {
$logo = trim($logo);
}
if ($logo === "") {
$logo = "";
}

$vperm = $this->visibility($i);

$hide_conflicts = null;
if ($qreq["tr/{$i}/hideconflicts"] || $qreq["has_tr/{$i}/hideconflicts"]) {
$hide_conflicts = !!$qreq["tr/{$i}/hideconflicts"];
}

$xlist = $permissionizer = null;
if ($qreq["tr/{$i}/listinfo"]) {
$xlist = SessionList::decode_info_string($this->user, $qreq["tr/{$i}/listinfo"], "p");
if ($xlist) {
$permissionizer = new MeetingTracker_Permissionizer($this->conf, $xlist->ids);
}
}

$p = trim($qreq["tr/{$i}/p"] ?? "");
if ($p !== "" && !ctype_digit($p)) {
$this->error_at_sfx($i, "p", "<0>Invalid submission number");
}
$position = false;
if ($p !== "" && $xlist) {
$position = array_search((int) $p, $xlist->ids);
}

$stop = $qreq->stopall || !!$qreq["tr/{$i}/stop"];

// Save tracker
if ($trackerid === "new") {
if ($stop) {
/* ignore */
} else if (!$xlist || !str_starts_with($xlist->listid, "p/")) {
$this->error_at_sfx($i, "name", "<0>Internal error");
} else if (!$permissionizer || !$permissionizer->check_admin_perm($this->user)) {
$my_tracks = [];
foreach ($this->conf->track_tags() as $tag) {
if (($perm = $this->conf->track_permission($tag, Track::ADMIN))
&& $this->user->has_permission($perm))
$my_tracks[] = "#{$tag}";
}
$this->error_at_sfx($i, "p", "<0>You can’t start a tracker on this list because you don’t administer all of its submissions. (You administer " . plural_word(count($my_tracks), "track") . " " . commajoin($my_tracks) . ".)");
} else {
do {
$new_trackerid = mt_rand(1, 9999999);
} while ($tracker->search($new_trackerid) !== false);

$tr = MeetingTracker_Config::make($this->user, $qreq, $new_trackerid, $xlist, Conf::$now, $position, $position_at);
$tr->name = $name ?? "";
if (!isset($vis) && $vperm === "") {
$vperm = $permissionizer->default_visibility();
}
$tr->visibility = $vperm;
$tr->admin_perm = $permissionizer->admin_perm();
$tr->logo = $logo ?? "";
$tr->hide_conflicts = !!($hide_conflicts ?? $this->conf->opt("trackerHideConflicts") ?? true);
$tracker->ts[] = $tr;
$changed = true;
}
} else if (($match = $tracker->search($trackerid)) !== false) {
$tr = $tracker->ts[$match];
if (($name ?? $tr->name) === $tr->name
&& ($vperm ?? $tr->visibility) === $tr->visibility
&& ($logo ?? $tr->logo) === $tr->logo
&& ($hide_conflicts ?? $tr->hide_conflicts) === $tr->hide_conflicts
&& !$stop) {
/* do nothing */
} else if (!MeetingTracker_Permissionizer::check_admin_perm_list($this->user, $tr->admin_perm)) {
if ($qreq["tr/{$i}/changed"]) {
$this->error_at_sfx($i, "name", "<0>You can’t administer this tracker");
}
} else {
$tr->name = $name ?? $tr->name;
$tr->visibility = $vperm ?? $tr->visibility;
$tr->logo = $logo ?? $tr->logo;
$tr->hide_conflicts = $hide_conflicts ?? $tr->hide_conflicts;

if ($stop) {
array_splice($tracker->ts, $match, 1);
}

$changed = true;
}
} else {
if (!$stop && $qreq["tr/{$i}/changed"]) {
$this->error_at_sfx($i, "name", "<0>This tracker no longer exists");
}
}
}

if (empty($this->ml) && $changed) {
$tracker->set_position_at($position_at);
if (!$tracker->update($tracker->next_eventid())) {
$this->ml[] = MessageItem::error("<0>Your changes were ignored because another user has changed the tracker settings. Please reload and try again.");
}
}
if (empty($this->ml)) {
$j = (object) ["ok" => true];
if ($new_trackerid !== false) {
$j->new_trackerid = $new_trackerid;
}
} else {
$j = (object) ["ok" => false, "message_list" => $this->ml];
}
MeetingTracker::my_deadlines($j, $this->user);
return new JsonResult($j);
}

/** @param Qrequest $qreq
* @return JsonResult */
static function run(Contact $user, $qreq) {
if (!$user->is_track_manager() || !$qreq->valid_post()) {
return JsonResult::make_permission_error();
}
return (new TrackerConfig_API($user, $qreq))->go();
}
}
Loading

0 comments on commit d809172

Please sign in to comment.