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