Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * H5P activity attempt object 19 * 20 * @package mod_h5pactivity 21 * @since Moodle 3.9 22 * @copyright 2020 Ferran Recio <ferran@moodle.com> 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 namespace mod_h5pactivity\local; 27 28 use core_xapi\handler; 29 use stdClass; 30 use core_xapi\local\statement; 31 32 /** 33 * Class attempt for H5P activity 34 * 35 * @package mod_h5pactivity 36 * @since Moodle 3.9 37 * @copyright 2020 Ferran Recio <ferran@moodle.com> 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 class attempt { 41 42 /** @var stdClass the h5pactivity_attempts record. */ 43 private $record; 44 45 /** @var boolean if the DB statement has been updated. */ 46 private $scoreupdated = false; 47 48 /** 49 * Create a new attempt object. 50 * 51 * @param stdClass $record the h5pactivity_attempts record 52 */ 53 public function __construct(stdClass $record) { 54 $this->record = $record; 55 } 56 57 /** 58 * Create a new user attempt in a specific H5P activity. 59 * 60 * @param stdClass $user a user record 61 * @param stdClass $cm a course_module record 62 * @return attempt|null a new attempt object or null if fail 63 */ 64 public static function new_attempt(stdClass $user, stdClass $cm): ?attempt { 65 global $DB; 66 $record = new stdClass(); 67 $record->h5pactivityid = $cm->instance; 68 $record->userid = $user->id; 69 $record->timecreated = time(); 70 $record->timemodified = $record->timecreated; 71 $record->rawscore = 0; 72 $record->maxscore = 0; 73 $record->duration = 0; 74 $record->completion = null; 75 $record->success = null; 76 77 // Get last attempt number. 78 $conditions = ['h5pactivityid' => $cm->instance, 'userid' => $user->id]; 79 $countattempts = $DB->count_records('h5pactivity_attempts', $conditions); 80 $record->attempt = $countattempts + 1; 81 82 $record->id = $DB->insert_record('h5pactivity_attempts', $record); 83 if (!$record->id) { 84 return null; 85 } 86 // Remove any xAPI State associated to this attempt. 87 $context = \context_module::instance($cm->id); 88 $xapihandler = handler::create('mod_h5pactivity'); 89 $xapihandler->wipe_states($context->id, $user->id); 90 91 return new attempt($record); 92 } 93 94 /** 95 * Get the last user attempt in a specific H5P activity. 96 * 97 * If no previous attempt exists, it generates a new one. 98 * 99 * @param stdClass $user a user record 100 * @param stdClass $cm a course_module record 101 * @return attempt|null a new attempt object or null if some problem accured 102 */ 103 public static function last_attempt(stdClass $user, stdClass $cm): ?attempt { 104 global $DB; 105 $conditions = ['h5pactivityid' => $cm->instance, 'userid' => $user->id]; 106 $records = $DB->get_records('h5pactivity_attempts', $conditions, 'attempt DESC', '*', 0, 1); 107 if (empty($records)) { 108 return self::new_attempt($user, $cm); 109 } 110 return new attempt(array_shift($records)); 111 } 112 113 /** 114 * Wipe all attempt data for specific course_module and an optional user. 115 * 116 * @param stdClass $cm a course_module record 117 * @param stdClass $user a user record 118 */ 119 public static function delete_all_attempts(stdClass $cm, stdClass $user = null): void { 120 global $DB; 121 122 $where = 'a.h5pactivityid = :h5pactivityid'; 123 $conditions = ['h5pactivityid' => $cm->instance]; 124 if (!empty($user)) { 125 $where .= ' AND a.userid = :userid'; 126 $conditions['userid'] = $user->id; 127 } 128 129 $DB->delete_records_select('h5pactivity_attempts_results', "attemptid IN ( 130 SELECT a.id 131 FROM {h5pactivity_attempts} a 132 WHERE $where)", $conditions); 133 134 $DB->delete_records('h5pactivity_attempts', $conditions); 135 } 136 137 /** 138 * Delete a specific attempt. 139 * 140 * @param attempt $attempt the attempt object to delete 141 */ 142 public static function delete_attempt(attempt $attempt): void { 143 global $DB; 144 $attempt->delete_results(); 145 $DB->delete_records('h5pactivity_attempts', ['id' => $attempt->get_id()]); 146 } 147 148 /** 149 * Save a new result statement into the attempt. 150 * 151 * It also updates the rawscore and maxscore if necessary. 152 * 153 * @param statement $statement the xAPI statement object 154 * @param string $subcontent = '' optional subcontent identifier 155 * @return bool if it can save the statement into db 156 */ 157 public function save_statement(statement $statement, string $subcontent = ''): bool { 158 global $DB; 159 160 // Check statement data. 161 $xapiobject = $statement->get_object(); 162 if (empty($xapiobject)) { 163 return false; 164 } 165 $xapiresult = $statement->get_result(); 166 $xapidefinition = $xapiobject->get_definition(); 167 if (empty($xapidefinition) || empty($xapiresult)) { 168 return false; 169 } 170 171 $xapicontext = $statement->get_context(); 172 if ($xapicontext) { 173 $context = $xapicontext->get_data(); 174 } else { 175 $context = new stdClass(); 176 } 177 $definition = $xapidefinition->get_data(); 178 $result = $xapiresult->get_data(); 179 $duration = $xapiresult->get_duration(); 180 181 // Insert attempt_results record. 182 $record = new stdClass(); 183 $record->attemptid = $this->record->id; 184 $record->subcontent = $subcontent; 185 $record->timecreated = time(); 186 $record->interactiontype = $definition->interactionType ?? 'other'; 187 $record->description = $this->get_description_from_definition($definition); 188 $record->correctpattern = $this->get_correctpattern_from_definition($definition); 189 $record->response = $result->response ?? ''; 190 $record->additionals = $this->get_additionals($definition, $context); 191 $record->rawscore = 0; 192 $record->maxscore = 0; 193 if (isset($result->score)) { 194 $record->rawscore = $result->score->raw ?? 0; 195 $record->maxscore = $result->score->max ?? 0; 196 } 197 $record->duration = $duration; 198 if (isset($result->completion)) { 199 $record->completion = ($result->completion) ? 1 : 0; 200 } 201 if (isset($result->success)) { 202 $record->success = ($result->success) ? 1 : 0; 203 } 204 if (!$DB->insert_record('h5pactivity_attempts_results', $record)) { 205 return false; 206 } 207 208 // If no subcontent provided, results are propagated to the attempt itself. 209 if (empty($subcontent)) { 210 $this->set_duration($record->duration); 211 $this->set_completion($record->completion ?? null); 212 $this->set_success($record->success ?? null); 213 // If Maxscore is not empty means that the rawscore is valid (even if it's 0) 214 // and scaled score can be calculated. 215 if ($record->maxscore) { 216 $this->set_score($record->rawscore, $record->maxscore); 217 } 218 } 219 // Refresh current attempt. 220 return $this->save(); 221 } 222 223 /** 224 * Update the current attempt record into DB. 225 * 226 * @return bool true if update is succesful 227 */ 228 public function save(): bool { 229 global $DB; 230 $this->record->timemodified = time(); 231 // Calculate scaled score. 232 if ($this->scoreupdated) { 233 if (empty($this->record->maxscore)) { 234 $this->record->scaled = 0; 235 } else { 236 $this->record->scaled = $this->record->rawscore / $this->record->maxscore; 237 } 238 } 239 return $DB->update_record('h5pactivity_attempts', $this->record); 240 } 241 242 /** 243 * Set the attempt score. 244 * 245 * @param int|null $rawscore the attempt rawscore 246 * @param int|null $maxscore the attempt maxscore 247 */ 248 public function set_score(?int $rawscore, ?int $maxscore): void { 249 $this->record->rawscore = $rawscore; 250 $this->record->maxscore = $maxscore; 251 $this->scoreupdated = true; 252 } 253 254 /** 255 * Set the attempt duration. 256 * 257 * @param int|null $duration the attempt duration 258 */ 259 public function set_duration(?int $duration): void { 260 $this->record->duration = $duration; 261 } 262 263 /** 264 * Set the attempt completion. 265 * 266 * @param int|null $completion the attempt completion 267 */ 268 public function set_completion(?int $completion): void { 269 $this->record->completion = $completion; 270 } 271 272 /** 273 * Set the attempt success. 274 * 275 * @param int|null $success the attempt success 276 */ 277 public function set_success(?int $success): void { 278 $this->record->success = $success; 279 } 280 281 /** 282 * Delete the current attempt results from the DB. 283 */ 284 public function delete_results(): void { 285 global $DB; 286 $conditions = ['attemptid' => $this->record->id]; 287 $DB->delete_records('h5pactivity_attempts_results', $conditions); 288 } 289 290 /** 291 * Return de number of results stored in this attempt. 292 * 293 * @return int the number of results stored in this attempt. 294 */ 295 public function count_results(): int { 296 global $DB; 297 $conditions = ['attemptid' => $this->record->id]; 298 return $DB->count_records('h5pactivity_attempts_results', $conditions); 299 } 300 301 /** 302 * Return all results stored in this attempt. 303 * 304 * @return stdClass[] results records. 305 */ 306 public function get_results(): array { 307 global $DB; 308 $conditions = ['attemptid' => $this->record->id]; 309 return $DB->get_records('h5pactivity_attempts_results', $conditions, 'id ASC'); 310 } 311 312 /** 313 * Get additional data for some interaction types. 314 * 315 * @param stdClass $definition the statement object definition data 316 * @param stdClass $context the statement optional context 317 * @return string JSON encoded additional information 318 */ 319 private function get_additionals(stdClass $definition, stdClass $context): string { 320 $additionals = []; 321 $interactiontype = $definition->interactionType ?? 'other'; 322 switch ($interactiontype) { 323 case 'choice': 324 case 'sequencing': 325 $additionals['choices'] = $definition->choices ?? []; 326 break; 327 328 case 'matching': 329 $additionals['source'] = $definition->source ?? []; 330 $additionals['target'] = $definition->target ?? []; 331 break; 332 333 case 'likert': 334 $additionals['scale'] = $definition->scale ?? []; 335 break; 336 337 case 'performance': 338 $additionals['steps'] = $definition->steps ?? []; 339 break; 340 } 341 342 $additionals['extensions'] = $definition->extensions ?? new stdClass(); 343 344 // Add context extensions. 345 $additionals['contextExtensions'] = $context->extensions ?? new stdClass(); 346 347 if (empty($additionals)) { 348 return ''; 349 } 350 return json_encode($additionals); 351 } 352 353 /** 354 * Extract the result description from statement object definition. 355 * 356 * In principle, H5P package can send a multilang description but the reality 357 * is that most activities only send the "en_US" description if any and the 358 * activity does not have any control over it. 359 * 360 * @param stdClass $definition the statement object definition 361 * @return string The available description if any 362 */ 363 private function get_description_from_definition(stdClass $definition): string { 364 if (!isset($definition->description)) { 365 return ''; 366 } 367 $translations = (array) $definition->description; 368 if (empty($translations)) { 369 return ''; 370 } 371 // By default, H5P packages only send "en-US" descriptions. 372 return $translations['en-US'] ?? array_shift($translations); 373 } 374 375 /** 376 * Extract the correct pattern from statement object definition. 377 * 378 * The correct pattern depends on the type of content and the plugin 379 * has no control over it so we just store it in case that the statement 380 * data have it. 381 * 382 * @param stdClass $definition the statement object definition 383 * @return string The correct pattern if any 384 */ 385 private function get_correctpattern_from_definition(stdClass $definition): string { 386 if (!isset($definition->correctResponsesPattern)) { 387 return ''; 388 } 389 // Only arrays are allowed. 390 if (is_array($definition->correctResponsesPattern)) { 391 return json_encode($definition->correctResponsesPattern); 392 } 393 return ''; 394 } 395 396 /** 397 * Return the attempt number. 398 * 399 * @return int the attempt number 400 */ 401 public function get_attempt(): int { 402 return $this->record->attempt; 403 } 404 405 /** 406 * Return the attempt ID. 407 * 408 * @return int the attempt id 409 */ 410 public function get_id(): int { 411 return $this->record->id; 412 } 413 414 /** 415 * Return the attempt user ID. 416 * 417 * @return int the attempt userid 418 */ 419 public function get_userid(): int { 420 return $this->record->userid; 421 } 422 423 /** 424 * Return the attempt H5P timecreated. 425 * 426 * @return int the attempt timecreated 427 */ 428 public function get_timecreated(): int { 429 return $this->record->timecreated; 430 } 431 432 /** 433 * Return the attempt H5P timemodified. 434 * 435 * @return int the attempt timemodified 436 */ 437 public function get_timemodified(): int { 438 return $this->record->timemodified; 439 } 440 441 /** 442 * Return the attempt H5P activity ID. 443 * 444 * @return int the attempt userid 445 */ 446 public function get_h5pactivityid(): int { 447 return $this->record->h5pactivityid; 448 } 449 450 /** 451 * Return the attempt maxscore. 452 * 453 * @return int the maxscore value 454 */ 455 public function get_maxscore(): int { 456 return $this->record->maxscore; 457 } 458 459 /** 460 * Return the attempt rawscore. 461 * 462 * @return int the rawscore value 463 */ 464 public function get_rawscore(): int { 465 return $this->record->rawscore; 466 } 467 468 /** 469 * Return the attempt duration. 470 * 471 * @return int|null the duration value 472 */ 473 public function get_duration(): ?int { 474 return $this->record->duration; 475 } 476 477 /** 478 * Return the attempt completion. 479 * 480 * @return int|null the completion value 481 */ 482 public function get_completion(): ?int { 483 return $this->record->completion; 484 } 485 486 /** 487 * Return the attempt success. 488 * 489 * @return int|null the success value 490 */ 491 public function get_success(): ?int { 492 return $this->record->success; 493 } 494 495 /** 496 * Return the attempt scaled. 497 * 498 * @return int|null the scaled value 499 */ 500 public function get_scaled(): ?int { 501 return is_null($this->record->scaled) ? $this->record->scaled : (int)$this->record->scaled; 502 } 503 504 /** 505 * Return if the attempt has been modified. 506 * 507 * Note: adding a result only add track information unless the statement does 508 * not specify subcontent. In this case this will update also the statement. 509 * 510 * @return bool if the attempt score have been modified 511 */ 512 public function get_scoreupdated(): bool { 513 return $this->scoreupdated; 514 } 515 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body