diff --git a/batch/makedist.sh b/batch/makedist.sh index 3b55852fc..bd753ee68 100755 --- a/batch/makedist.sh +++ b/batch/makedist.sh @@ -220,6 +220,7 @@ src/assigners/a_preference.php src/assigners/a_review.php src/assigners/a_status.php src/assigners/a_tag.php +src/assigners/a_taganno.php src/assigners/a_unsubmitreview.php src/assignmentcountset.php src/assignmentset.php diff --git a/src/assigners/a_copytag.php b/src/assigners/a_copytag.php index 51faa4569..f4be91872 100644 --- a/src/assigners/a_copytag.php +++ b/src/assigners/a_copytag.php @@ -10,10 +10,15 @@ function __construct(Conf $conf, $aj) { $this->move = $aj->move ?? false; } function load_state(AssignmentState $state) { - Tag_AssignmentParser::load_tag_state($state); + Tag_Assignable::load($state); + TagAnno_Assignable::load($state); + } + function paper_universe($req, AssignmentState $state) { + return "reqpost"; } function allow_paper(PaperInfo $prow, AssignmentState $state) { - if (($whyNot = $state->user->perm_edit_some_tag($prow))) { + if ($prow->paperId > 0 + && ($whyNot = $state->user->perm_edit_some_tag($prow))) { return new AssignmentError($whyNot); } else { return true; @@ -35,6 +40,7 @@ function apply(PaperInfo $prow, Contact $contact, $req, AssignmentState $state) $state->error($tagger->error_ftext(true)); return false; } + $ltag = strtolower($tag); $new_tag = $tagger->check($req["new_tag"] ?? "", Tagger::NOVALUE); if (!$new_tag) { if ($tagger->error_code() === Tagger::EEMPTY) { @@ -46,7 +52,8 @@ function apply(PaperInfo $prow, Contact $contact, $req, AssignmentState $state) } // if you can't view the tag, you can't copy or move the tag - if (!$state->user->can_view_tag($prow, $tag)) { + if ($prow->paperId > 0 + && !$state->user->can_view_tag($prow, $tag)) { return Tag_AssignmentParser::cannot_view_error($prow, $tag, $state); } @@ -60,18 +67,39 @@ function apply(PaperInfo $prow, Contact $contact, $req, AssignmentState $state) return false; } - $ltag = strtolower($tag); - $res = $state->query(new Tag_Assignable($prow->paperId, $ltag)); - if (!$res) { - return true; + // real paper: change/move tag + if ($prow->paperId > 0) { + $res = $state->query(new Tag_Assignable($prow->paperId, $ltag)); + if (!$res) { + return true; + } + assert(count($res) === 1); + + $lnew_tag = strtolower($new_tag); + $state->add(new Tag_Assignable($prow->paperId, $lnew_tag, $new_tag, $res[0]->_index)); + if ($this->move) { + $state->remove($res[0]); + } } - assert(count($res) === 1); - $lnew_tag = strtolower($new_tag); - $state->add(new Tag_Assignable($prow->paperId, $lnew_tag, $new_tag, $res[0]->_index)); - if ($this->move) { - $state->remove($res[0]); + // placeholder: change/move tag annotations + if ($prow->paperId < 0 + && $state->user->can_edit_tag_anno($tag) + && $state->user->can_edit_tag_anno($new_tag) + && ($ares = $state->query(new TagAnno_Assignable($tag, null)))) { + if (!$state->query(new TagAnno_Assignable($new_tag, null))) { + foreach ($ares as $taa) { + $state->add($taa->with_tag($new_tag)); + } + } + if ($this->move + && !$state->query(new Tag_Assignable(null, $ltag))) { + foreach ($ares as $taa) { + $x = $state->remove($taa); + } + } } + return true; } } diff --git a/src/assigners/a_status.php b/src/assigners/a_status.php index 0ecaed7a8..90855660b 100644 --- a/src/assigners/a_status.php +++ b/src/assigners/a_status.php @@ -105,7 +105,7 @@ function apply(PaperInfo $prow, Contact $contact, $req, AssignmentState $state) $res->_withdrawn = Conf::$now; $res->_submitted = -$res->_submitted; if ($state->conf->tags()->has(TagInfo::TFM_VOTES)) { - Tag_AssignmentParser::load_tag_state($state); + Tag_Assignable::load($state); $state->register_preapply_function("withdraw {$prow->paperId}", new Withdraw_PreapplyFunction($prow->paperId)); } } diff --git a/src/assigners/a_tag.php b/src/assigners/a_tag.php index 0b4f12ec9..0d2a6e30e 100644 --- a/src/assigners/a_tag.php +++ b/src/assigners/a_tag.php @@ -36,6 +36,16 @@ function match($q) { && ($q->ltag ?? $this->ltag) === $this->ltag && ($q->_index ?? $this->_index) === $this->_index; } + static function load(AssignmentState $state) { + if (!$state->mark_type("tag", ["pid", "ltag"], "Tag_Assigner::make")) { + return; + } + $result = $state->conf->qe("select paperId, tag, tagIndex from PaperTag where paperId?a", $state->paper_ids()); + while (($row = $result->fetch_row())) { + $state->load(new Tag_Assignable(+$row[0], strtolower($row[1]), $row[1], (float) $row[2])); + } + Dbl::free($result); + } } class NextTagAssigner implements AssignmentPreapplyFunction { @@ -78,7 +88,7 @@ function preapply(AssignmentState $state) { $ltag = strtolower($this->tag); foreach ($this->pidindex as $pid => $index) { if ($index >= $this->first_index && $index < $this->next_index) { - $x = $state->query_unmodified(new Tag_Assignable($pid, $ltag)); + $x = $state->query_unedited(new Tag_Assignable($pid, $ltag)); if (!empty($x)) { $state->add(new Tag_Assignable($pid, $ltag, $this->tag, $this->next_index($this->isseq), true)); } @@ -110,18 +120,8 @@ function __construct(Conf $conf, $aj) { function expand_papers($req, AssignmentState $state) { return $this->itype ? "ALL" : (string) $req["paper"]; } - static function load_tag_state(AssignmentState $state) { - if (!$state->mark_type("tag", ["pid", "ltag"], "Tag_Assigner::make")) { - return; - } - $result = $state->conf->qe("select paperId, tag, tagIndex from PaperTag where paperId?a", $state->paper_ids()); - while (($row = $result->fetch_row())) { - $state->load(new Tag_Assignable(+$row[0], strtolower($row[1]), $row[1], (float) $row[2])); - } - Dbl::free($result); - } function load_state(AssignmentState $state) { - self::load_tag_state($state); + Tag_Assignable::load($state); } function allow_paper(PaperInfo $prow, AssignmentState $state) { if (($whyNot = $state->user->perm_edit_some_tag($prow))) { diff --git a/src/assigners/a_taganno.php b/src/assigners/a_taganno.php new file mode 100644 index 000000000..e2a39efcd --- /dev/null +++ b/src/assigners/a_taganno.php @@ -0,0 +1,91 @@ +type = "taganno"; + $this->pid = 0; + $this->ltag = strtolower($tag); + $this->annoId = $annoId; + $this->_tag = $tag; + $this->_tagIndex = $index; + $this->_heading = $heading; + $this->_infoJson = $infoJson; + } + /** @return self */ + function fresh() { + return new TagAnno_Assignable($this->_tag, $this->annoId); + } + /** @param string $t + * @return self */ + function with_tag($t) { + $x = clone $this; + $x->ltag = strtolower($t); + $x->_tag = $t; + return $x; + } + /** @param Assignable $q + * @return bool */ + function match($q) { + '@phan-var-force TagAnno_Assignable $q'; + return ($q->ltag ?? $this->ltag) === $this->ltag + && ($q->annoId ?? $this->annoId) === $this->annoId; + } + static function load(AssignmentState $state) { + if (!$state->mark_type("taganno", ["ltag", "annoId"], "TagAnno_Assigner::make")) { + return; + } + $result = $state->conf->qe("select tag, annoId, tagIndex, heading, infoJson from PaperTagAnno"); + while (($row = $result->fetch_row())) { + $state->load(new TagAnno_Assignable($row[0], +$row[1], +$row[2], $row[3], $row[4])); + } + Dbl::free($result); + } +} + +class TagAnno_Assigner extends Assigner { + function __construct(AssignmentItem $item, AssignmentState $state) { + parent::__construct($item, $state); + } + static function make(AssignmentItem $item, AssignmentState $state) { + if (!$state->user->can_edit_tag_anno($item["ltag"])) { + throw new AssignmentError("<0>You can’t edit this tag’s annotations"); + } + return new TagAnno_Assigner($item, $state); + } + function unparse_description() { + return "tag annotation"; + } + function add_locks(AssignmentSet $aset, &$locks) { + $locks["PaperTagAnno"] = "write"; + } + function execute(AssignmentSet $aset) { + if ($this->item->deleted()) { + $aset->stage_qe("delete from PaperTagAnno where tag=? and annoId=?", + $this->item->pre("_tag"), $this->item->pre("annoId")); + } else { + $aset->stage_qe("insert into PaperTagAnno set tag=?, annoId=?, tagIndex=?, heading=?, infoJson=? ?U on duplicate key update tagIndex=?U(tagIndex), heading=?U(heading), infoJson=?U(infoJson)", + $this->item->pre("_tag"), $this->item->pre("annoId"), + $this->item->post("_tagIndex"), $this->item->post("_heading"), + $this->item->post("_infoJson")); + } + } +} diff --git a/src/assignmentset.php b/src/assignmentset.php index 280a215d8..63a5df821 100644 --- a/src/assignmentset.php +++ b/src/assignmentset.php @@ -83,9 +83,16 @@ function deleted() { return $this->deleted; } /** @return bool */ - function modified() { + function edited() { return !!$this->after; } + /** @return bool */ + function changed() { + return $this->after + && ($this->deleted + ? $this->existed + : !$this->existed || !$this->after->match($this->before)); + } /** @param bool $pre * @param string $offset */ function get($pre, $offset) { @@ -324,10 +331,10 @@ function query($q) { /** @template T * @param T $q * @return list */ - function query_unmodified($q) { + function query_unedited($q) { $res = []; foreach ($this->query_items($q) as $item) { - if (!$item->modified()) + if (!$item->edited()) $res[] = $item->before; } return $res; @@ -387,8 +394,7 @@ function diff() { $diff = []; foreach ($this->st as $pid => $st) { foreach ($st->items as $item) { - if ($item->after - && (!$item->existed() || !$item->after->match($item->before))) + if ($item->changed()) $diff[$pid][] = $item; } } @@ -737,11 +743,13 @@ function __construct($type) { $this->type = $type; } // Return a descriptor of the set of papers relevant for this action. - // Returns `""` or `"none"`. + // `"req"`, the default, means call `apply` for each requested paper. + // `"none"` means call `apply` exactly once with a placeholder paper. + // `"reqpost"` means each requested paper, then once with a placeholder. /** @param CsvRow $req - * @return ''|'none' */ + * @return 'req'|'none'|'reqpost' */ function paper_universe($req, AssignmentState $state) { - return ""; + return "req"; } // Optionally expand the set of interesting papers. Returns a search // expression, such as "ALL", or false. @@ -1608,25 +1616,27 @@ private function apply_req(AssignmentParser $aparser = null, $req) { $this->astate->paper_exact_match = $pfield_straight; // check conflicts and perform assignment - if ($paper_universe === "none") { - $prow = $this->astate->placeholder_prow(); - $any_success = $this->apply_paper($prow, $contacts, $aparser, $req) === 1; - } else { - $any_success = false; - foreach ($pids as $p) { - $prow = $this->astate->prow($p); - if (!$prow) { - $this->error("<5>" . $this->user->no_paper_whynot($p)->unparse_html()); - } else { - $ret = $this->apply_paper($prow, $contacts, $aparser, $req); - if ($ret === 1) { - $any_success = true; - } else if ($ret < 0) { - break; - } + $any_success = false; + foreach ($pids as $p) { + $prow = $this->astate->prow($p); + if (!$prow) { + $this->error("<5>" . $this->user->no_paper_whynot($p)->unparse_html()); + } else { + $ret = $this->apply_paper($prow, $contacts, $aparser, $req); + if ($ret === 1) { + $any_success = true; + } else if ($ret < 0) { + break; } } } + if ($paper_universe === "none" || $paper_universe === "reqpost") { + $prow = $this->astate->placeholder_prow(); + $ret = $this->apply_paper($prow, $contacts, $aparser, $req); + if ($ret === 1) { + $any_success = true; + } + } if (!$any_success) { $this->astate->mark_matching_errors(); diff --git a/test/t_tags.php b/test/t_tags.php index 709bfe7fc..be4ee04ef 100644 --- a/test/t_tags.php +++ b/test/t_tags.php @@ -236,4 +236,51 @@ function test_tag_anno() { $this->conf->qe("delete from PaperTagAnno where tag='t'"); } + + function test_copy_tag_anno() { + $v = []; + foreach ([0, 10, 10, 10, 30, 31, 32, 50] as $i => $n) { + $v[] = ["t", $i + 1, $n, "H" . ($i + 1)]; + } + $this->conf->qe("insert into PaperTagAnno (tag,annoId,tagIndex,heading) values ?v", $v); + + $dt = $this->conf->tags()->ensure("t"); + $dtt = $this->conf->tags()->ensure("tt"); + + $dt->invalidate_order_anno(); + xassert($dt->has_order_anno()); + $dtt->invalidate_order_anno(); + xassert(!$dtt->has_order_anno()); + + xassert_assign($this->u_chair, "action,paper,tag,new_tag\ncopytag,all,t,tt\n"); + + $dt->invalidate_order_anno(); + xassert($dt->has_order_anno()); + $dtt->invalidate_order_anno(); + xassert($dtt->has_order_anno()); + + $sv = [[-1, null], [0, 1], [1, 1], [10, 4], [10, 4], [12, 4], [30, 5], [31, 6], [32, 7], [33, 7], [49, 7], [50, 8], [60, 8]]; + foreach ($sv as $m) { + xassert_eqq($dt->order_anno_search($m[0])->annoId ?? null, $m[1]); + xassert_eqq($dtt->order_anno_search($m[0])->annoId ?? null, $m[1]); + } + + xassert_assign($this->u_chair, "action,paper,tag,new_tag\nmovetag,all,tt,tu\n"); + + $dtu = $this->conf->tags()->ensure("tu"); + $dt->invalidate_order_anno(); + xassert($dt->has_order_anno()); + $dtt->invalidate_order_anno(); + xassert(!$dtt->has_order_anno()); + $dtu->invalidate_order_anno(); + xassert($dtu->has_order_anno()); + + $sv = [[-1, null], [0, 1], [1, 1], [10, 4], [10, 4], [12, 4], [30, 5], [31, 6], [32, 7], [33, 7], [49, 7], [50, 8], [60, 8]]; + foreach ($sv as $m) { + xassert_eqq($dt->order_anno_search($m[0])->annoId ?? null, $m[1]); + xassert_eqq($dtu->order_anno_search($m[0])->annoId ?? null, $m[1]); + } + + $this->conf->qe("delete from PaperTagAnno where tag in ('t','tt','tu')"); + } }