Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 400 and 401]

   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   * Data generator.
  19   *
  20   * @package    mod_h5pactivity
  21   * @copyright 2020 Ferran Recio <ferran@moodle.com>
  22   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  use mod_h5pactivity\local\manager;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  
  30  /**
  31   * h5pactivity module data generator class.
  32   *
  33   * @package    mod_h5pactivity
  34   * @copyright 2020 Ferran Recio <ferran@moodle.com>
  35   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class mod_h5pactivity_generator extends testing_module_generator {
  38  
  39      /**
  40       * Creates new h5pactivity module instance. By default it contains a short
  41       * text file.
  42       *
  43       * @param array|stdClass $record data for module being generated. Requires 'course' key
  44       *     (an id or the full object). Also can have any fields from add module form.
  45       * @param null|array $options general options for course module. Since 2.6 it is
  46       *     possible to omit this argument by merging options into $record
  47       * @return stdClass record from module-defined table with additional field
  48       *     cmid (corresponding id in course_modules table)
  49       */
  50      public function create_instance($record = null, array $options = null): stdClass {
  51          global $CFG, $USER;
  52          // Ensure the record can be modified without affecting calling code.
  53          $record = (object)(array)$record;
  54  
  55          // Fill in optional values if not specified.
  56          if (!isset($record->packagefilepath)) {
  57              $record->packagefilepath = $CFG->dirroot.'/h5p/tests/fixtures/h5ptest.zip';
  58          } else if (strpos($record->packagefilepath, $CFG->dirroot) !== 0) {
  59              $record->packagefilepath = "{$CFG->dirroot}/{$record->packagefilepath}";
  60          }
  61          if (!isset($record->grade)) {
  62              $record->grade = 100;
  63          }
  64          if (!isset($record->displayoptions)) {
  65              $factory = new \core_h5p\factory();
  66              $core = $factory->get_core();
  67              $config = \core_h5p\helper::decode_display_options($core);
  68              $record->displayoptions = \core_h5p\helper::get_display_options($core, $config);
  69          }
  70          if (!isset($record->enabletracking)) {
  71              $record->enabletracking = 1;
  72          }
  73          if (!isset($record->grademethod)) {
  74              $record->grademethod = manager::GRADEHIGHESTATTEMPT;
  75          }
  76          if (!isset($record->reviewmode)) {
  77              $record->reviewmode = manager::REVIEWCOMPLETION;
  78          }
  79  
  80          // The 'packagefile' value corresponds to the draft file area ID. If not specified, create from packagefilepath.
  81          if (empty($record->packagefile)) {
  82              if (!isloggedin() || isguestuser()) {
  83                  throw new coding_exception('H5P activity generator requires a current user');
  84              }
  85              if (!file_exists($record->packagefilepath)) {
  86                  throw new coding_exception("File {$record->packagefilepath} does not exist");
  87              }
  88              $usercontext = context_user::instance($USER->id);
  89  
  90              // Pick a random context id for specified user.
  91              $record->packagefile = file_get_unused_draft_itemid();
  92  
  93              // Add actual file there.
  94              $filerecord = ['component' => 'user', 'filearea' => 'draft',
  95                      'contextid' => $usercontext->id, 'itemid' => $record->packagefile,
  96                      'filename' => basename($record->packagefilepath), 'filepath' => '/'];
  97              $fs = get_file_storage();
  98              $fs->create_file_from_pathname($filerecord, $record->packagefilepath);
  99          }
 100  
 101          // Do work to actually add the instance.
 102          return parent::create_instance($record, (array)$options);
 103      }
 104  
 105      /**
 106       * Creata a fake attempt
 107       * @param stdClass $instance object returned from create_instance() call
 108       * @param stdClass|array $record
 109       * @return stdClass generated object
 110       * @throws coding_exception if function is not implemented by module
 111       */
 112      public function create_content($instance, $record = []) {
 113          global $DB, $USER;
 114  
 115          $currenttime = time();
 116          $cmid = $record['cmid'];
 117          $userid = $record['userid'] ?? $USER->id;
 118          $conditions = ['h5pactivityid' => $instance->id, 'userid' => $userid];
 119          $attemptnum = $DB->count_records('h5pactivity_attempts', $conditions) + 1;
 120          $attempt = (object)[
 121                  'h5pactivityid' => $instance->id,
 122                  'userid' => $userid,
 123                  'timecreated' => $currenttime,
 124                  'timemodified' => $currenttime,
 125                  'attempt' => $attemptnum,
 126                  'rawscore' => 3,
 127                  'maxscore' => 5,
 128                  'completion' => 1,
 129                  'success' => 1,
 130                  'scaled' => 0.6,
 131              ];
 132          $attempt->id = $DB->insert_record('h5pactivity_attempts', $attempt);
 133  
 134          // Create 3 diferent tracking results.
 135          $result = (object)[
 136                  'attemptid' => $attempt->id,
 137                  'subcontent' => '',
 138                  'timecreated' => $currenttime,
 139                  'interactiontype' => 'compound',
 140                  'description' => 'description for '.$userid,
 141                  'correctpattern' => '',
 142                  'response' => '',
 143                  'additionals' => '{"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":'.
 144                          $cmid.'},"contextExtensions":{}}',
 145                  'rawscore' => 3,
 146                  'maxscore' => 5,
 147                  'completion' => 1,
 148                  'success' => 1,
 149                  'scaled' => 0.6,
 150              ];
 151          $DB->insert_record('h5pactivity_attempts_results', $result);
 152  
 153          $result->subcontent = 'bd03477a-90a1-486d-890b-0657d6e80ffd';
 154          $result->interactiontype = 'compound';
 155          $result->response = '0[,]5[,]2[,]3';
 156          $result->additionals = '{"choices":[{"id":"0","description":{"en-US":"Blueberry\n"}},'.
 157                  '{"id":"1","description":{"en-US":"Raspberry\n"}},{"id":"5","description":'.
 158                  '{"en-US":"Strawberry\n"}},{"id":"2","description":{"en-US":"Cloudberry\n"}},'.
 159                  '{"id":"3","description":{"en-US":"Halle Berry\n"}},'.
 160                  '{"id":"4","description":{"en-US":"Cocktail cherry\n"}}],'.
 161                  '"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":'.$cmid.
 162                  ',"http:\/\/h5p.org\/x-api\/h5p-subContentId":"'.$result->interactiontype.
 163                  '"},"contextExtensions":{}}';
 164          $result->rawscore = 1;
 165          $result->scaled = 0.2;
 166          $DB->insert_record('h5pactivity_attempts_results', $result);
 167  
 168          $result->subcontent = '14fcc986-728b-47f3-915b-'.$userid;
 169          $result->interactiontype = 'matching';
 170          $result->correctpattern = '["0[.]1[,]1[.]0[,]2[.]2"]';
 171          $result->response = '1[.]0[,]0[.]1[,]2[.]2';
 172          $result->additionals = '{"source":[{"id":"0","description":{"en-US":"A berry"}}'.
 173                  ',{"id":"1","description":{"en-US":"An orange berry"}},'.
 174                  '{"id":"2","description":{"en-US":"A red berry"}}],'.
 175                  '"target":[{"id":"0","description":{"en-US":"Cloudberry"}},'.
 176                  '{"id":"1","description":{"en-US":"Blueberry"}},'.
 177                  '{"id":"2","description":{"en-US":"Redcurrant\n"}}],'.
 178                  '"contextExtensions":{}}';
 179          $result->rawscore = 2;
 180          $result->scaled = 0.4;
 181          $DB->insert_record('h5pactivity_attempts_results', $result);
 182  
 183          return $attempt;
 184      }
 185  
 186      /**
 187       * Create a H5P attempt.
 188       *
 189       * This method is user by behat generator.
 190       *
 191       * @param array $data the attempts data array
 192       */
 193      public function create_attempt(array $data): void {
 194          global $DB;
 195  
 196          if (!isset($data['h5pactivityid'])) {
 197              throw new coding_exception('Must specify h5pactivityid when creating a H5P attempt.');
 198          }
 199  
 200          if (!isset($data['userid'])) {
 201              throw new coding_exception('Must specify userid when creating a H5P attempt.');
 202          }
 203  
 204          // Defaults.
 205          $data['attempt'] = $data['attempt'] ?? 1;
 206          $data['rawscore'] = $data['rawscore'] ?? 0;
 207          $data['maxscore'] = $data['maxscore'] ?? 0;
 208          $data['duration'] = $data['duration'] ?? 0;
 209          $data['completion'] = $data['completion'] ?? 1;
 210          $data['success'] = $data['success'] ?? 0;
 211  
 212          $data['attemptid'] = $this->get_attempt_object($data);
 213  
 214          // Check interaction type and create a valid record for it.
 215          $data['interactiontype'] = $data['interactiontype'] ?? 'compound';
 216          $method = 'get_attempt_result_' . str_replace('-', '', $data['interactiontype']);
 217          if (!method_exists($this, $method)) {
 218              throw new Exception("Cannot create a {$data['interactiontype']} interaction statement");
 219          }
 220  
 221          $this->insert_statement($data, $this->$method($data));
 222  
 223          // If the activity has tracking enabled, try to recalculate grades.
 224          $activity = $DB->get_record('h5pactivity', ['id' => $data['h5pactivityid']]);
 225          if ($activity->enabletracking) {
 226              h5pactivity_update_grades($activity, $data['userid']);
 227          }
 228      }
 229  
 230      /**
 231       * Get or create an H5P attempt using the data array.
 232       *
 233       * @param array $attemptinfo the generator provided data
 234       * @return int the attempt id
 235       */
 236      private function get_attempt_object($attemptinfo): int {
 237          global $DB;
 238          $result = $DB->get_record('h5pactivity_attempts', [
 239              'userid' => $attemptinfo['userid'],
 240              'h5pactivityid' => $attemptinfo['h5pactivityid'],
 241              'attempt' => $attemptinfo['attempt'],
 242          ]);
 243          if ($result) {
 244              return $result->id;
 245          }
 246          return $this->new_user_attempt($attemptinfo);
 247      }
 248  
 249      /**
 250       * Creates a user attempt.
 251       *
 252       * @param array $attemptinfo the current attempt information.
 253       * @return int the h5pactivity_attempt ID
 254       */
 255      private function new_user_attempt(array $attemptinfo): int {
 256          global $DB;
 257          $record = (object)[
 258              'h5pactivityid' => $attemptinfo['h5pactivityid'],
 259              'userid' => $attemptinfo['userid'],
 260              'timecreated' => time(),
 261              'timemodified' => time(),
 262              'attempt' => $attemptinfo['attempt'],
 263              'rawscore' => $attemptinfo['rawscore'],
 264              'maxscore' => $attemptinfo['maxscore'],
 265              'duration' => $attemptinfo['duration'],
 266              'completion' => $attemptinfo['completion'],
 267              'success' => $attemptinfo['success'],
 268          ];
 269          if (empty($record->maxscore)) {
 270              $record->scaled = 0;
 271          } else {
 272              $record->scaled = $record->rawscore / $record->maxscore;
 273          }
 274          return $DB->insert_record('h5pactivity_attempts', $record);
 275      }
 276  
 277      /**
 278       * Insert a new statement into an attempt.
 279       *
 280       * If the interaction type is "compound" it will also update the attempt general result.
 281       *
 282       * @param array $attemptinfo the current attempt information
 283       * @param array $statement the statement tracking information
 284       * @return int the h5pactivity_attempt_result ID
 285       */
 286      private function insert_statement(array $attemptinfo, array $statement): int {
 287          global $DB;
 288          $record = $statement + [
 289              'attemptid' => $attemptinfo['attemptid'],
 290              'interactiontype' => $attemptinfo['interactiontype'] ?? 'compound',
 291              'timecreated' => time(),
 292              'rawscore' => $attemptinfo['rawscore'],
 293              'maxscore' => $attemptinfo['maxscore'],
 294              'duration' => $attemptinfo['duration'],
 295              'completion' => $attemptinfo['completion'],
 296              'success' => $attemptinfo['success'],
 297          ];
 298          $result = $DB->insert_record('h5pactivity_attempts_results', $record);
 299          if ($record['interactiontype'] == 'compound') {
 300              $attempt = (object)[
 301                  'id' => $attemptinfo['attemptid'],
 302                  'rawscore' => $record['rawscore'],
 303                  'maxscore' => $record['maxscore'],
 304                  'duration' => $record['duration'],
 305                  'completion' => $record['completion'],
 306                  'success' => $record['success'],
 307              ];
 308              $DB->update_record('h5pactivity_attempts', $attempt);
 309          }
 310          return $result;
 311      }
 312  
 313      /**
 314       * Generates a valid compound tracking result.
 315       *
 316       * @param array $attemptinfo the current attempt information.
 317       * @return array with the required statement data
 318       */
 319      private function get_attempt_result_compound(array $attemptinfo): array {
 320          $additionals = (object)[
 321              "extensions" => (object)[
 322                  "http://h5p.org/x-api/h5p-local-content-id" => 1,
 323              ],
 324              "contextExtensions" => (object)[],
 325          ];
 326  
 327          return [
 328              'subcontent' => '',
 329              'description' => '',
 330              'correctpattern' => '',
 331              'response' => '',
 332              'additionals' => json_encode($additionals),
 333          ];
 334      }
 335  
 336      /**
 337       * Generates a valid choice tracking result.
 338       *
 339       * @param array $attemptinfo the current attempt information.
 340       * @return array with the required statement data
 341       */
 342      private function get_attempt_result_choice(array $attemptinfo): array {
 343  
 344          $response = ($attemptinfo['rawscore']) ? '1[,]0' : '2[,]3';
 345  
 346          $additionals = (object)[
 347              "choices" => [
 348                  (object)[
 349                      "id" => "3",
 350                      "description" => (object)[
 351                          "en-US" => "Another wrong answer\n",
 352                      ],
 353                  ],
 354                  (object)[
 355                      "id" => "2",
 356                      "description" => (object)[
 357                          "en-US" => "Wrong answer\n",
 358                      ],
 359                  ],
 360                  (object)[
 361                      "id" => "1",
 362                      "description" => (object)[
 363                          "en-US" => "This is also a correct answer\n",
 364                      ],
 365                  ],
 366                  (object)[
 367                      "id" => "0",
 368                      "description" => (object)[
 369                          "en-US" => "This is a correct answer\n",
 370                      ],
 371                  ],
 372              ],
 373              "extensions" => (object)[
 374                  "http://h5p.org/x-api/h5p-local-content-id" => 1,
 375                  "http://h5p.org/x-api/h5p-subContentId" => "4367a919-ec47-43c9-b521-c22d9c0c0d8d",
 376              ],
 377              "contextExtensions" => (object)[],
 378          ];
 379  
 380          return [
 381              'subcontent' => microtime(),
 382              'description' => 'Select the correct answers',
 383              'correctpattern' => '["1[,]0"]',
 384              'response' => $response,
 385              'additionals' => json_encode($additionals),
 386          ];
 387      }
 388  
 389      /**
 390       * Generates a valid matching tracking result.
 391       *
 392       * @param array $attemptinfo the current attempt information.
 393       * @return array with the required statement data
 394       */
 395      private function get_attempt_result_matching(array $attemptinfo): array {
 396  
 397          $response = ($attemptinfo['rawscore']) ? '0[.]0[,]1[.]1' : '1[.]0[,]0[.]1';
 398  
 399          $additionals = (object)[
 400              "source" => [
 401                  (object)[
 402                      "id" => "0",
 403                      "description" => (object)[
 404                          "en-US" => "Drop item A\n",
 405                      ],
 406                  ],
 407                  (object)[
 408                      "id" => "1",
 409                      "description" => (object)[
 410                          "en-US" => "Drop item B\n",
 411                      ],
 412                  ],
 413              ],
 414              "target" => [
 415                  (object)[
 416                      "id" => "0",
 417                      "description" => (object)[
 418                          "en-US" => "Drop zone A\n",
 419                      ],
 420                  ],
 421                  (object)[
 422                      "id" => "1",
 423                      "description" => (object)[
 424                          "en-US" => "Drop zone B\n",
 425                      ],
 426                  ],
 427              ],
 428              "extensions" => [
 429                  "http://h5p.org/x-api/h5p-local-content-id" => 1,
 430                  "http://h5p.org/x-api/h5p-subContentId" => "682f1c74-c819-4e9d-8c36-12d9dc5fcdbc",
 431              ],
 432              "contextExtensions" => (object)[],
 433          ];
 434  
 435          return [
 436              'subcontent' => microtime(),
 437              'description' => 'Drag and Drop example 1',
 438              'correctpattern' => '["0[.]0[,]1[.]1"]',
 439              'response' => $response,
 440              'additionals' => json_encode($additionals),
 441          ];
 442      }
 443  
 444      /**
 445       * Generates a valid fill-in tracking result.
 446       *
 447       * @param array $attemptinfo the current attempt information.
 448       * @return array with the required statement data
 449       */
 450      private function get_attempt_result_fillin(array $attemptinfo): array {
 451  
 452          $response = ($attemptinfo['rawscore']) ? 'first[,]second' : 'something[,]else';
 453  
 454          $additionals = (object)[
 455              "extensions" => (object)[
 456                  "http://h5p.org/x-api/h5p-local-content-id" => 1,
 457                  "http://h5p.org/x-api/h5p-subContentId" => "1a3febd5-7edc-4336-8112-12756b945b62",
 458                  "https://h5p.org/x-api/case-sensitivity" => true,
 459                  "https://h5p.org/x-api/alternatives" => [
 460                      ["first"],
 461                      ["second"],
 462                  ],
 463              ],
 464              "contextExtensions" => (object)[
 465                  "https://h5p.org/x-api/h5p-reporting-version" => "1.1.0",
 466              ],
 467          ];
 468  
 469          return [
 470              'subcontent' => microtime(),
 471              'description' => '<p>This an example of missing word text.</p>
 472  
 473                      <p>The first answer if "first": the first answer is __________.</p>
 474  
 475                      <p>The second is second is "second": the secons answer is __________</p>',
 476              'correctpattern' => '["{case_matters=true}first[,]second"]',
 477              'response' => $response,
 478              'additionals' => json_encode($additionals),
 479          ];
 480      }
 481  
 482      /**
 483       * Generates a valid true-false tracking result.
 484       *
 485       * @param array $attemptinfo the current attempt information.
 486       * @return array with the required statement data
 487       */
 488      private function get_attempt_result_truefalse(array $attemptinfo): array {
 489  
 490          $response = ($attemptinfo['rawscore']) ? 'true' : 'false';
 491  
 492          $additionals = (object)[
 493              "extensions" => (object)[
 494                  "http://h5p.org/x-api/h5p-local-content-id" => 1,
 495                  "http://h5p.org/x-api/h5p-subContentId" => "5de9fb1e-aa03-4c9a-8cf0-3870b3f012ca",
 496              ],
 497              "contextExtensions" => (object)[],
 498          ];
 499  
 500          return [
 501              'subcontent' => microtime(),
 502              'description' => 'The correct answer is true.',
 503              'correctpattern' => '["true"]',
 504              'response' => $response,
 505              'additionals' => json_encode($additionals),
 506          ];
 507      }
 508  
 509      /**
 510       * Generates a valid long-fill-in tracking result.
 511       *
 512       * @param array $attemptinfo the current attempt information.
 513       * @return array with the required statement data
 514       */
 515      private function get_attempt_result_longfillin(array $attemptinfo): array {
 516  
 517          $response = ($attemptinfo['rawscore']) ? 'The Hobbit is book' : 'Who cares?';
 518  
 519          $additionals = (object)[
 520              "extensions" => (object)[
 521                  "http://h5p.org/x-api/h5p-local-content-id" => 1,
 522                  "http://h5p.org/x-api/h5p-subContentId" => "5de9fb1e-aa03-4c9a-8cf0-3870b3f012ca",
 523              ],
 524              "contextExtensions" => (object)[],
 525          ];
 526  
 527          return [
 528              'subcontent' => microtime(),
 529              'description' => '<p>Please describe the novel The Hobbit',
 530              'correctpattern' => '',
 531              'response' => $response,
 532              'additionals' => json_encode($additionals),
 533          ];
 534      }
 535  
 536      /**
 537       * Generates a valid sequencing tracking result.
 538       *
 539       * @param array $attemptinfo the current attempt information.
 540       * @return array with the required statement data
 541       */
 542      private function get_attempt_result_sequencing(array $attemptinfo): array {
 543  
 544          $response = ($attemptinfo['rawscore']) ? 'true' : 'false';
 545  
 546          $additionals = (object)[
 547              "extensions" => (object)[
 548                  "http://h5p.org/x-api/h5p-local-content-id" => 1,
 549                  "http://h5p.org/x-api/h5p-subContentId" => "5de9fb1e-aa03-4c9a-8cf0-3870b3f012ca",
 550              ],
 551              "contextExtensions" => (object)[],
 552          ];
 553  
 554          return [
 555              'subcontent' => microtime(),
 556              'description' => 'The correct answer is true.',
 557              'correctpattern' => '["{case_matters=true}first[,]second"]',
 558              'response' => $response,
 559              'additionals' => json_encode($additionals),
 560          ];
 561      }
 562  
 563      /**
 564       * Generates a valid other tracking result.
 565       *
 566       * @param array $attemptinfo the current attempt information.
 567       * @return array with the required statement data
 568       */
 569      private function get_attempt_result_other(array $attemptinfo): array {
 570  
 571          $additionals = (object)[
 572              "extensions" => (object)[
 573                  "http://h5p.org/x-api/h5p-local-content-id" => 1,
 574              ],
 575              "contextExtensions" => (object)[],
 576          ];
 577  
 578          return [
 579              'subcontent' => microtime(),
 580              'description' => '',
 581              'correctpattern' => '',
 582              'response' => '',
 583              'additionals' => json_encode($additionals),
 584          ];
 585      }
 586  }