Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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