Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 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   * Contains class mod_h5pactivity\output\result
  19   *
  20   * @package   mod_h5pactivity
  21   * @copyright 2020 Ferran Recio
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace mod_h5pactivity\output;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  use renderable;
  30  use templatable;
  31  use renderer_base;
  32  use stdClass;
  33  
  34  /**
  35   * Class to display an attempt tesult in mod_h5pactivity.
  36   *
  37   * @copyright 2020 Ferran Recio
  38   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class result implements renderable, templatable {
  41  
  42      /** Correct answer state. */
  43      const CORRECT = 1;
  44  
  45      /** Incorrect answer state. */
  46      const INCORRECT = 2;
  47  
  48      /** Checked answer state. */
  49      const CHECKED = 3;
  50  
  51      /** Unchecked answer state. */
  52      const UNCHECKED = 4;
  53  
  54      /** Pass answer state. */
  55      const PASS = 5;
  56  
  57      /** Pass answer state. */
  58      const FAIL = 6;
  59  
  60      /** Unknown answer state. */
  61      const UNKNOWN = 7;
  62  
  63      /** Text answer state. */
  64      const TEXT = 8;
  65  
  66      /** @var stdClass result record */
  67      protected $result;
  68  
  69      /** @var mixed additional decoded data */
  70      protected $additionals;
  71  
  72      /** @var mixed response decoded data */
  73      protected $response;
  74  
  75      /** @var mixed correctpattern decoded data */
  76      protected $correctpattern = [];
  77  
  78      /**
  79       * Constructor.
  80       *
  81       * @param stdClass $result a h5pactivity_attempts_results record
  82       */
  83      protected function __construct(stdClass $result) {
  84          $this->result = $result;
  85          if (empty($result->additionals)) {
  86              $this->additionals = new stdClass();
  87          } else {
  88              $this->additionals = json_decode($result->additionals);
  89          }
  90          $this->response = $this->decode_response($result->response);
  91          if (!empty($result->correctpattern)) {
  92              $correctpattern = json_decode($result->correctpattern);
  93              foreach ($correctpattern as $pattern) {
  94                  $this->correctpattern[] = $this->decode_response($pattern);
  95              }
  96          }
  97      }
  98  
  99      /**
 100       * return the correct result output depending on the interactiontype
 101       *
 102       * @param stdClass $result h5pactivity_attempts_results record
 103       * @return result|null the result output class if any
 104       */
 105      public static function create_from_record(stdClass $result): ?self {
 106          // Compound result track is omitted from the report.
 107          if ($result->interactiontype == 'compound') {
 108              return null;
 109          }
 110          $classname = "mod_h5pactivity\\output\\result\\{$result->interactiontype}";
 111          $classname = str_replace('-', '', $classname);
 112          if (class_exists($classname)) {
 113              return new $classname($result);
 114          }
 115          return new self($result);
 116      }
 117  
 118      /**
 119       * Return a decoded response structure.
 120       *
 121       * @param string $value the current response structure
 122       * @return array an array of reponses
 123       */
 124      private function decode_response(string $value): array {
 125          // If [,] means a list of elements.
 126          $list = explode('[,]', $value);
 127          // Inside a list element [.] means sublist (pair) and [:] a range.
 128          foreach ($list as $key => $item) {
 129              if (strpos($item, '[.]') !== false) {
 130                  $list[$key] = explode('[.]', $item);
 131              } else if (strpos($item, '[:]') !== false) {
 132                  $list[$key] = explode('[:]', $item);
 133              }
 134          }
 135          return $list;
 136      }
 137  
 138      /**
 139       * Export this data so it can be used as the context for a mustache template.
 140       *
 141       * @param renderer_base $output
 142       * @return stdClass
 143       */
 144      public function export_for_template(renderer_base $output): stdClass {
 145          $result = $this->result;
 146  
 147          $data = (object)[
 148              'id' => $result->id,
 149              'attemptid' => $result->attemptid,
 150              'subcontent' => $result->subcontent,
 151              'timecreated' => $result->timecreated,
 152              'interactiontype' => $result->interactiontype,
 153              'description' => strip_tags($result->description),
 154              'rawscore' => $result->rawscore,
 155              'maxscore' => $result->maxscore,
 156              'duration' => $result->duration,
 157              'completion' => $result->completion,
 158              'success' => $result->success,
 159          ];
 160          $result;
 161  
 162          $options = $this->export_options();
 163  
 164          if (!empty($options)) {
 165              $data->hasoptions = true;
 166              $data->optionslabel = $this->get_optionslabel();
 167              $data->correctlabel = $this->get_correctlabel();
 168              $data->answerlabel = $this->get_answerlabel();
 169              $data->options = array_values($options);
 170              $data->track = true;
 171          }
 172  
 173          if (!empty($result->maxscore)) {
 174              $data->score = get_string('score_out_of', 'mod_h5pactivity', $result);
 175          }
 176          return $data;
 177      }
 178  
 179      /**
 180       * Return the options data structure.
 181       *
 182       * Result types have to override this method generate a specific options report.
 183       *
 184       * An option is an object with:
 185       *   - id: the option ID
 186       *   - description: option description text
 187       *   - useranswer (optional): what the user answer (see get_answer method)
 188       *   - correctanswer (optional): the correct answer (see get_answer method)
 189       *
 190       * @return array of options
 191       */
 192      protected function export_options(): ?array {
 193          return [];
 194      }
 195  
 196      /**
 197       * Return a label for result user options/choices.
 198       *
 199       * Specific result types can override this method to customize
 200       * the result options table header.
 201       *
 202       * @return string to use in options table
 203       */
 204      protected function get_optionslabel(): string {
 205          return get_string('choice', 'mod_h5pactivity');
 206      }
 207  
 208      /**
 209       * Return a label for result user correct answer.
 210       *
 211       * Specific result types can override this method to customize
 212       * the result options table header.
 213       *
 214       * @return string to use in options table
 215       */
 216      protected function get_correctlabel(): string {
 217          return get_string('correct_answer', 'mod_h5pactivity');
 218      }
 219  
 220      /**
 221       * Return a label for result user attempt answer.
 222       *
 223       * Specific result types can override this method to customize
 224       * the result options table header.
 225       *
 226       * @return string to use in options table
 227       */
 228      protected function get_answerlabel(): string {
 229          return get_string('attempt_answer', 'mod_h5pactivity');
 230      }
 231  
 232      /**
 233       * Extract descriptions from array.
 234       *
 235       * @param array $data additional attribute to parse
 236       * @return string[] the resulting strings
 237       */
 238      protected function get_descriptions(array $data): array {
 239          $result = [];
 240          foreach ($data as $key => $value) {
 241              $description = $this->get_description($value);
 242              $index = $value->id ?? $key;
 243              $index = trim($index);
 244              if (is_numeric($index)) {
 245                  $index = intval($index);
 246              }
 247              $result[$index] = (object)['description' => $description, 'id' => $index];
 248          }
 249          ksort($result);
 250          return $result;
 251      }
 252  
 253      /**
 254       * Extract description from data element.
 255       *
 256       * @param stdClass $data additional attribute to parse
 257       * @return string the resulting string
 258       */
 259      protected function get_description(stdClass $data): string {
 260          if (!isset($data->description)) {
 261              return '';
 262          }
 263          $translations = (array) $data->description;
 264          if (empty($translations)) {
 265              return '';
 266          }
 267          // By default, H5P packages only send "en-US" descriptions.
 268          $result = $translations['en-US'] ?? array_shift($translations);
 269          return trim($result);
 270      }
 271  
 272      /**
 273       * Return an answer data to show results.
 274       *
 275       * @param int $state the answer state
 276       * @param string $answer the extra text to display (default null)
 277       * @return stdClass with "answer" text and the state attribute to be displayed
 278       */
 279      protected function get_answer(int $state, string $answer = null): stdClass {
 280          $states = [
 281              self::CORRECT => 'correct',
 282              self::INCORRECT => 'incorrect',
 283              self::CHECKED => 'checked',
 284              self::UNCHECKED => 'unchecked',
 285              self::PASS => 'pass',
 286              self::FAIL => 'fail',
 287              self::UNKNOWN => 'unknown',
 288              self::TEXT => 'text',
 289          ];
 290          $state = $states[$state] ?? self::UNKNOWN;
 291          if ($answer === null) {
 292              $answer = get_string('answer_'.$state, 'mod_h5pactivity');
 293          }
 294          $result = (object)[
 295              'answer' => $answer,
 296              $state => true,
 297          ];
 298          return $result;
 299      }
 300  }