1<?php 2// reviewinfo.php -- HotCRP class representing reviews 3// Copyright (c) 2006-2018 Eddie Kohler; see LICENSE. 4 5class ReviewInfo { 6 public $conf; 7 public $paperId; 8 public $reviewId; 9 public $contactId; 10 public $reviewToken; 11 public $reviewType; 12 public $reviewRound; 13 public $requestedBy; 14 //public $timeRequested; 15 //public $timeRequestNotified; 16 public $reviewBlind; 17 public $reviewModified; 18 //public $reviewAuthorModified; 19 public $reviewSubmitted; 20 //public $reviewNotified; 21 //public $reviewAuthorNotified; 22 public $reviewAuthorSeen; 23 public $reviewOrdinal; 24 //public $timeDisplayed; 25 public $timeApprovalRequested; 26 //public $reviewEditVersion; 27 public $reviewNeedsSubmit; 28 // ... scores ... 29 //public $reviewWordCount; 30 //public $reviewFormat; 31 32 static public $text_field_map = [ 33 "paperSummary" => "t01", "commentsToAuthor" => "t02", 34 "commentsToPC" => "t03", "commentsToAddress" => "t04", 35 "weaknessOfPaper" => "t05", "strengthOfPaper" => "t06", 36 "textField7" => "t07", "textField8" => "t08" 37 ]; 38 static private $new_text_fields = [ 39 null, "paperSummary", "commentsToAuthor", "commentsToPC", 40 "commentsToAddress", "weaknessOfPaper", "strengthOfPaper", 41 "textField7", "textField8" 42 ]; 43 static private $score_field_map = [ 44 "overAllMerit" => "s01", "reviewerQualification" => "s02", 45 "novelty" => "s03", "technicalMerit" => "s04", 46 "interestToCommunity" => "s05", "longevity" => "s06", "grammar" => "s07", 47 "likelyPresentation" => "s08", "suitableForShort" => "s09", 48 "potential" => "s10", "fixability" => "s11" 49 ]; 50 static private $new_score_fields = [ 51 null, "overAllMerit", "reviewerQualification", "novelty", 52 "technicalMerit", "interestToCommunity", "longevity", "grammar", 53 "likelyPresentation", "suitableForShort", "potential", "fixability" 54 ]; 55 const MIN_SFIELD = 12; 56 57 const RATING_GOODMASK = 1; 58 const RATING_BADMASK = 126; 59 // See also script.js:unparse_ratings 60 static public $rating_options = [ 61 1 => "good review", 2 => "needs work", 62 4 => "too short", 8 => "too vague", 16 => "too narrow", 63 32 => "not constructive", 64 => "not correct" 64 ]; 65 static public $rating_bits = [ 66 1 => "good", 2 => "bad", 4 => "short", 8 => "vague", 67 16 => "narrow", 32 => "not-constructive", 64 => "wrong" 68 ]; 69 70 static private $type_map = [ 71 "meta" => REVIEW_META, 72 "primary" => REVIEW_PRIMARY, "pri" => REVIEW_PRIMARY, 73 "secondary" => REVIEW_SECONDARY, "sec" => REVIEW_SECONDARY, 74 "optional" => REVIEW_PC, "opt" => REVIEW_PC, "pc" => REVIEW_PC, 75 "external" => REVIEW_EXTERNAL, "ext" => REVIEW_EXTERNAL 76 ]; 77 static private $type_revmap = [ 78 REVIEW_EXTERNAL => "review", REVIEW_PC => "pcreview", 79 REVIEW_SECONDARY => "secondary", REVIEW_PRIMARY => "primary", 80 REVIEW_META => "metareview" 81 ]; 82 83 static function parse_type($str) { 84 $str = strtolower($str); 85 if ($str === "review" || $str === "" || $str === "all" || $str === "any") 86 return null; 87 if (str_ends_with($str, "review")) 88 $str = substr($str, 0, -6); 89 return get(self::$type_map, $str, false); 90 } 91 static function unparse_assigner_action($type) { 92 return get(self::$type_revmap, $type, "clearreview"); 93 } 94 95 private function merge(Conf $conf) { 96 $this->conf = $conf; 97 foreach (["paperId", "reviewId", "contactId", "reviewType", 98 "reviewRound", "requestedBy", "reviewBlind", 99 "reviewOrdinal", "reviewNeedsSubmit"] as $k) { 100 assert($this->$k !== null, "null $k"); 101 $this->$k = (int) $this->$k; 102 } 103 foreach (["reviewModified", "reviewSubmitted", "reviewAuthorSeen"] as $k) 104 if (isset($this->$k)) 105 $this->$k = (int) $this->$k; 106 if (isset($this->tfields) && ($x = json_decode($this->tfields, true))) { 107 foreach ($x as $k => $v) 108 $this->$k = $v; 109 } 110 if (isset($this->sfields) && ($x = json_decode($this->sfields, true))) { 111 foreach ($x as $k => $v) 112 $this->$k = $v; 113 } 114 if ($conf->sversion < 176) { 115 foreach (self::$text_field_map as $kmain => $kjson) 116 if (isset($this->$kmain) && !isset($this->$kjson)) 117 $this->$kjson = $this->$kmain; 118 } 119 } 120 static function fetch($result, Conf $conf) { 121 $rrow = $result ? $result->fetch_object("ReviewInfo") : null; 122 if ($rrow) 123 $rrow->merge($conf); 124 return $rrow; 125 } 126 static function review_signature_sql() { 127 return "group_concat(r.reviewId, ' ', r.contactId, ' ', r.reviewToken, ' ', r.reviewType, ' ', " 128 . "r.reviewRound, ' ', r.requestedBy, ' ', r.reviewBlind, ' ', r.reviewModified, ' ', " 129 . "coalesce(r.reviewSubmitted,0), ' ', coalesce(r.reviewAuthorSeen,0), ' ', " 130 . "r.reviewOrdinal, ' ', r.timeApprovalRequested, ' ', r.reviewNeedsSubmit order by r.reviewId)"; 131 } 132 static function make_signature(PaperInfo $prow, $signature) { 133 $rrow = new ReviewInfo; 134 $rrow->paperId = $prow->paperId; 135 list($rrow->reviewId, $rrow->contactId, $rrow->reviewToken, $rrow->reviewType, 136 $rrow->reviewRound, $rrow->requestedBy, $rrow->reviewBlind, $rrow->reviewModified, 137 $rrow->reviewSubmitted, $rrow->reviewAuthorSeen, 138 $rrow->reviewOrdinal, $rrow->timeApprovalRequested, $rrow->reviewNeedsSubmit) 139 = explode(" ", $signature); 140 $rrow->merge($prow->conf); 141 return $rrow; 142 } 143 144 function round_name() { 145 return $this->reviewRound ? $this->conf->round_name($this->reviewRound) : ""; 146 } 147 148 function assign_name($c) { 149 $this->firstName = $c->firstName; 150 $this->lastName = $c->lastName; 151 $this->email = $c->email; 152 $this->sorter = $c->sorter; 153 } 154 155 static function field_info($id, Conf $conf) { 156 $sversion = $conf->sversion; 157 if (strlen($id) === 3 && ctype_digit(substr($id, 1))) { 158 $n = intval(substr($id, 1), 10); 159 $json_storage = $sversion >= 174 ? $id : null; 160 if ($id[0] === "t") { 161 if (isset(self::$new_text_fields[$n]) && $sversion < 175) 162 return new ReviewFieldInfo($id, $id, false, self::$new_text_fields[$n], $json_storage); 163 else if ($json_storage) 164 return new ReviewFieldInfo($id, $id, false, null, $json_storage); 165 else 166 return false; 167 } else if ($id[0] === "s") { 168 if (isset(self::$new_score_fields[$n])) { 169 $fid = self::$new_score_fields[$n]; 170 return new ReviewFieldInfo($fid, $id, true, $fid, null); 171 } else if ($json_storage) 172 return new ReviewFieldInfo($id, $id, true, null, $json_storage); 173 else 174 return false; 175 } else 176 return false; 177 } else if (isset(self::$text_field_map[$id])) { 178 $short_id = self::$text_field_map[$id]; 179 $main_storage = $sversion < 175 ? $id : null; 180 $json_storage = $sversion >= 174 ? $short_id : null; 181 return new ReviewFieldInfo($short_id, $short_id, false, $main_storage, $json_storage); 182 } else if (isset(self::$score_field_map[$id])) { 183 $short_id = self::$score_field_map[$id]; 184 return new ReviewFieldInfo($id, $short_id, true, $id, null); 185 } else 186 return false; 187 } 188 189 function field_match_pregexes($reg, $field) { 190 $data = $this->$field; 191 $field_deaccent = $field . "_deaccent"; 192 if (!isset($this->$field_deaccent)) { 193 if (preg_match('/[\x80-\xFF]/', $data)) 194 $this->$field_deaccent = UnicodeHelper::deaccent($data); 195 else 196 $this->$field_deaccent = false; 197 } 198 return Text::match_pregexes($reg, $data, $this->$field_deaccent); 199 } 200 201 function unparse_sfields() { 202 $data = null; 203 foreach (get_object_vars($this) as $k => $v) 204 if (strlen($k) === 3 205 && $k[0] === "s" 206 && (int) $v !== 0 207 && ($n = cvtint(substr($k, 1))) >= self::MIN_SFIELD) 208 $data[$k] = (int) $v; 209 if ($data === null) 210 return null; 211 return json_encode_db($data); 212 } 213 function unparse_tfields() { 214 global $Conf; 215 $data = null; 216 foreach (get_object_vars($this) as $k => $v) 217 if (strlen($k) === 3 218 && $k[0] === "t" 219 && $v !== null 220 && $v !== "") 221 $data[$k] = $v; 222 if ($data === null) 223 return null; 224 $json = json_encode_db($data); 225 if ($json === null) 226 error_log(($Conf ? "{$Conf->dbname}: " : "") . "review #{$this->paperId}/{$this->reviewId}: text fields cannot be converted to JSON"); 227 return $json; 228 } 229 230 static function compare($a, $b) { 231 // 1. different papers 232 if ($a->paperId != $b->paperId) 233 return (int) $a->paperId < (int) $b->paperId ? -1 : 1; 234 // 2. different ordinals (both have ordinals) 235 if ($a->reviewOrdinal 236 && $b->reviewOrdinal 237 && $a->reviewOrdinal != $b->reviewOrdinal) 238 return (int) $a->reviewOrdinal < (int) $b->reviewOrdinal ? -1 : 1; 239 // 3. some submitted reviews have no ordinal (ordinal is reserved for 240 // user-visible reviews) 241 $asub = (int) $a->reviewSubmitted; 242 $bsub = (int) $b->reviewSubmitted; 243 if (($asub > 0) != ($bsub > 0)) 244 return $asub > 0 ? -1 : 1; 245 if ($asub !== $bsub) 246 return $asub < $bsub ? -1 : 1; 247 // 4. submission class 248 $asclass = self::submission_class($a); 249 $bsclass = self::submission_class($b); 250 if ($asclass !== $bsclass) 251 return $asclass < $bsclass ? 1 : -1; 252 // 5. reviewer 253 if (isset($a->sorter) 254 && isset($b->sorter) 255 && ($x = strcmp($a->sorter, $b->sorter)) != 0) 256 return $x; 257 // 6. review id 258 if ($a->reviewId != $b->reviewId) 259 return (int) $a->reviewId < (int) $b->reviewId ? -1 : 1; 260 return 0; 261 } 262 263 static function compare_id($a, $b) { 264 if ($a->paperId != $b->paperId) 265 return (int) $a->paperId < (int) $b->paperId ? -1 : 1; 266 if ($a->reviewId != $b->reviewId) 267 return (int) $a->reviewId < (int) $b->reviewId ? -1 : 1; 268 return 0; 269 } 270 271 static function submission_class($rr) { 272 if ($rr->reviewSubmitted > 0) 273 return 5; 274 else if ($rr->reviewType == REVIEW_SECONDARY && $rr->reviewNeedsSubmit <= 0) 275 return 4; 276 else if ($rr->reviewModified > 1 && $rr->timeApprovalRequested > 0) 277 return 3; 278 else if ($rr->reviewModified > 1) 279 return 2; 280 else if ($rr->reviewModified > 0) 281 return 1; 282 else 283 return 0; 284 } 285 286 function ratings() { 287 $ratings = []; 288 if ((string) $this->allRatings !== "") { 289 foreach (explode(",", $this->allRatings) as $rx) { 290 list($cid, $rating) = explode(" ", $rx); 291 $ratings[(int) $cid] = intval($rating); 292 } 293 } 294 return $ratings; 295 } 296 297 function rating_of_user($user) { 298 $cid = is_object($user) ? $user->contactId : $user; 299 $str = ",$cid "; 300 $pos = strpos("," . $this->allRatings, $str); 301 if ($pos !== false) 302 return intval(substr($this->allRatings, $pos + strlen($str) - 1)); 303 return null; 304 } 305 306 static function unparse_rating($rating) { 307 if (isset(self::$rating_bits[$rating])) 308 return self::$rating_bits[$rating]; 309 else if (!$rating) 310 return "none"; 311 else { 312 $a = []; 313 foreach (self::$rating_bits as $k => $v) 314 if ($rating & $k) 315 $a[] = $v; 316 return join(" ", $a); 317 } 318 } 319 320 static function parse_rating($s) { 321 if (ctype_digit($s)) { 322 $n = intval($s); 323 if ($n >= 0 && $n < 127) 324 return $n ? : null; 325 } 326 $n = 0; 327 foreach (preg_split('/\s+/', $s) as $word) { 328 if (($k = array_search($word, ReviewInfo::$rating_bits)) !== false) 329 $n |= $k; 330 else if ($word !== "" && $word !== "none") 331 return false; 332 } 333 return $n; 334 } 335} 336