1<?php
2// a_preference.php -- HotCRP assignment helper classes
3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE.
4
5class Preference_AssignmentParser extends AssignmentParser {
6    function __construct() {
7        parent::__construct("pref");
8    }
9    function load_state(AssignmentState $state) {
10        if (!$state->mark_type("pref", ["pid", "cid"], "Preference_Assigner::make"))
11            return;
12        $result = $state->conf->qe("select paperId, contactId, preference, expertise from PaperReviewPreference where paperId?a", $state->paper_ids());
13        while (($row = edb_row($result)))
14            $state->load(["type" => "pref", "pid" => +$row[0], "cid" => +$row[1], "_pref" => +$row[2], "_exp" => self::make_exp($row[3])]);
15        Dbl::free($result);
16    }
17    function allow_paper(PaperInfo $prow, AssignmentState $state) {
18        return true;
19    }
20    function expand_any_user(PaperInfo $prow, &$req, AssignmentState $state) {
21        return array_filter($state->pc_users(),
22            function ($u) use ($prow) {
23                return $u->can_become_reviewer_ignore_conflict($prow);
24            });
25    }
26    function expand_missing_user(PaperInfo $prow, &$req, AssignmentState $state) {
27        return $state->reviewer->isPC ? [$state->reviewer] : false;
28    }
29    function allow_contact(PaperInfo $prow, Contact $contact, &$req, AssignmentState $state) {
30        if (!$contact->contactId)
31            return false;
32        else if ($contact->contactId !== $state->user->contactId
33                 && !$state->user->can_administer($prow))
34            return "Can’t change other users’ preferences for #{$prow->paperId}.";
35        else if (!$contact->can_become_reviewer_ignore_conflict($prow)) {
36            if ($contact->contactId !== $state->user->contactId)
37                return Text::user_html_nolink($contact) . " can’t enter preferences for #{$prow->paperId}.";
38            else
39                return "Can’t enter preferences for #{$prow->paperId}.";
40        } else
41            return true;
42    }
43    static private function make_exp($exp) {
44        return $exp === null ? "N" : +$exp;
45    }
46    static function parse($str) {
47        if ($str === "" || strcasecmp($str, "none") == 0)
48            return [0, null];
49        else if (is_numeric($str)) {
50            if ($str <= 1000000)
51                return [(int) round($str), null];
52            else
53                return null;
54        }
55
56        $str = rtrim(preg_replace('{(?:\A\s*[\"\'`]\s*|\s*[\"\'`]\s*\z|\s+(?=[-+\d.xyz]))}i', "", $str));
57        if ($str === "" || strcasecmp($str, "none") == 0 || strcasecmp($str, "n/a") == 0)
58            return [0, null];
59        else if (strspn($str, "-") === strlen($str))
60            return [-strlen($str), null];
61        else if (strspn($str, "+") === strlen($str))
62            return [strlen($str), null];
63        else if (preg_match('{\A(?:--?(?=-[\d.])|\+(?=\+?[\d.])|)([-+]?(?:\d+(?:\.\d*)?|\.\d+)|)([xyz]?)(?:[-+]|)\z}i', $str, $m)) {
64            if ($m[1] === "")
65                $p = 0;
66            else if ($m[1] <= 1000000)
67                $p = (int) round($m[1]);
68            else
69                return null;
70            if ($m[2] === "")
71                $e = null;
72            else
73                $e = 9 - (ord($m[2]) & 15);
74            return [$p, $e];
75        } else if (strcasecmp($str, "conflict") == 0)
76            return [-100, null];
77        else {
78            $str2 = str_replace(["\xE2\x88\x92", "–", "—"], ["-", "-", "-"], $str);
79            return $str === $str2 ? null : self::parse($str2);
80        }
81    }
82    function apply(PaperInfo $prow, Contact $contact, &$req, AssignmentState $state) {
83        foreach (array("preference", "pref", "revpref") as $k)
84            if (($pref = get($req, $k)) !== null)
85                break;
86        if ($pref === null)
87            return "Missing preference.";
88        $ppref = self::parse($pref);
89        if ($ppref === null) {
90            if (preg_match('/([+-]?)\s*(\d+)\s*([xyz]?)/i', $pref, $m)) {
91                $msg = $state->conf->_("“%s” isn’t a valid preference. Did you mean “%s”?", htmlspecialchars($pref), $m[1] . $m[2] . strtoupper($m[3]));
92            } else {
93                $msg = $state->conf->_("“%s” isn’t a valid preference.", htmlspecialchars($pref));
94            }
95            $state->user_error($msg);
96            return false;
97        }
98
99        foreach (array("expertise", "revexp") as $k)
100            if (($exp = get($req, $k)) !== null)
101                break;
102        if ($exp && ($exp = trim($exp)) !== "") {
103            if (($pexp = self::parse($exp)) === null || $pexp[0])
104                return "Invalid expertise “" . htmlspecialchars($exp) . "”.";
105            $ppref[1] = $pexp[1];
106        }
107
108        $state->remove(array("type" => "pref", "pid" => $prow->paperId, "cid" => $contact->contactId));
109        if ($ppref[0] || $ppref[1] !== null)
110            $state->add(array("type" => "pref", "pid" => $prow->paperId, "cid" => $contact->contactId, "_pref" => $ppref[0], "_exp" => self::make_exp($ppref[1])));
111        return true;
112    }
113}
114
115class Preference_Assigner extends Assigner {
116    function __construct(AssignmentItem $item, AssignmentState $state) {
117        parent::__construct($item, $state);
118    }
119    static function make(AssignmentItem $item, AssignmentState $state) {
120        return new Preference_Assigner($item, $state);
121    }
122    function unparse_description() {
123        return "preference";
124    }
125    private function preference_data($before) {
126        $p = [$this->item->get($before, "_pref"),
127              $this->item->get($before, "_exp")];
128        if ($p[1] === "N")
129            $p[1] = null;
130        return $p[0] || $p[1] !== null ? $p : null;
131    }
132    function unparse_display(AssignmentSet $aset) {
133        if (!$this->cid)
134            return "remove all preferences";
135        $t = $aset->user->reviewer_html_for($this->contact);
136        if (($p = $this->preference_data(true)))
137            $t .= " <del>" . unparse_preference_span($p, true) . "</del>";
138        if (($p = $this->preference_data(false)))
139            $t .= " <ins>" . unparse_preference_span($p, true) . "</ins>";
140        return $t;
141    }
142    function unparse_csv(AssignmentSet $aset, AssignmentCsv $acsv) {
143        $p = $this->preference_data(false);
144        $pref = $p ? unparse_preference($p[0], $p[1]) : "none";
145        return ["pid" => $this->pid, "action" => "preference",
146                "email" => $this->contact->email, "name" => $this->contact->name_text(),
147                "preference" => $pref];
148    }
149    function add_locks(AssignmentSet $aset, &$locks) {
150        $locks["PaperReviewPreference"] = "write";
151    }
152    function execute(AssignmentSet $aset) {
153        if (($p = $this->preference_data(false)))
154            $aset->stage_qe("insert into PaperReviewPreference
155                set paperId=?, contactId=?, preference=?, expertise=?
156                on duplicate key update preference=values(preference), expertise=values(expertise)",
157                    $this->pid, $this->cid, $p[0], $p[1]);
158        else
159            $aset->stage_qe("delete from PaperReviewPreference where paperId=? and contactId=?", $this->pid, $this->cid);
160    }
161}
162