Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 402 and 403]

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * This file defines a class with "Number of errors" grading strategy logic
  20   *
  21   * @package    workshopform_numerrors
  22   * @copyright  2009 David Mudrak <david.mudrak@gmail.com>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once (__DIR__ . '/../lib.php');        // Interface definition.
  29  require_once($CFG->libdir . '/gradelib.php'); // To handle float vs decimal issues.
  30  
  31  /**
  32   * Server workshop files
  33   *
  34   * @category files
  35   * @param stdClass $course course object
  36   * @param stdClass $cm course module object
  37   * @param stdClass $context context object
  38   * @param string $filearea file area
  39   * @param array $args extra arguments
  40   * @param bool $forcedownload whether or not force download
  41   * @param array $options additional options affecting the file serving
  42   * @return bool
  43   */
  44  function workshopform_numerrors_pluginfile($course, $cm, $context, $filearea, array $args, $forcedownload, array $options=array()) {
  45      global $DB;
  46  
  47      if ($context->contextlevel != CONTEXT_MODULE) {
  48          return false;
  49      }
  50  
  51      require_login($course, true, $cm);
  52  
  53      if ($filearea !== 'description') {
  54          return false;
  55      }
  56  
  57      $itemid = (int)array_shift($args); // the id of the assessment form dimension
  58      if (!$workshop = $DB->get_record('workshop', array('id' => $cm->instance))) {
  59          send_file_not_found();
  60      }
  61  
  62      if (!$dimension = $DB->get_record('workshopform_numerrors', array('id' => $itemid ,'workshopid' => $workshop->id))) {
  63          send_file_not_found();
  64      }
  65  
  66      // TODO now make sure the user is allowed to see the file
  67      // (media embedded into the dimension description)
  68      $fs = get_file_storage();
  69      $relativepath = implode('/', $args);
  70      $fullpath = "/$context->id/workshopform_numerrors/$filearea/$itemid/$relativepath";
  71      if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
  72          return false;
  73      }
  74  
  75      // finally send the file
  76      send_stored_file($file, 0, 0, $forcedownload, $options);
  77  }
  78  
  79  /**
  80   * "Number of errors" grading strategy logic.
  81   */
  82  class workshop_numerrors_strategy implements workshop_strategy {
  83  
  84      /** @const default number of dimensions to show */
  85      const MINDIMS = 3;
  86  
  87      /** @const number of dimensions to add */
  88      const ADDDIMS = 2;
  89  
  90      /** @var workshop the parent workshop instance */
  91      protected $workshop;
  92  
  93      /** @var array definition of the assessment form fields */
  94      protected $dimensions = null;
  95  
  96      /** @var array mapping of the number of errors to a grade */
  97      protected $mappings = null;
  98  
  99      /** @var array options for dimension description fields */
 100      protected $descriptionopts;
 101  
 102      /**
 103       * Constructor
 104       *
 105       * @param workshop $workshop The workshop instance record
 106       * @return void
 107       */
 108      public function __construct(workshop $workshop) {
 109          $this->workshop         = $workshop;
 110          $this->dimensions       = $this->load_fields();
 111          $this->mappings         = $this->load_mappings();
 112          $this->descriptionopts  = array('trusttext' => true, 'subdirs' => false, 'maxfiles' => -1);
 113      }
 114  
 115      /**
 116       * Factory method returning an instance of an assessment form editor class
 117       *
 118       * @param $actionurl URL of form handler, defaults to auto detect the current url
 119       */
 120      public function get_edit_strategy_form($actionurl=null) {
 121          global $CFG;    // needed because the included files use it
 122          global $PAGE;
 123  
 124          require_once (__DIR__ . '/edit_form.php');
 125  
 126          $fields             = $this->prepare_form_fields($this->dimensions, $this->mappings);
 127          $nodimensions       = count($this->dimensions);
 128          $norepeatsdefault   = max($nodimensions + self::ADDDIMS, self::MINDIMS);
 129          $norepeats          = optional_param('norepeats', $norepeatsdefault, PARAM_INT);    // number of dimensions
 130          $noadddims          = optional_param('noadddims', '', PARAM_ALPHA);                 // shall we add more?
 131          if ($noadddims) {
 132              $norepeats += self::ADDDIMS;
 133          }
 134  
 135          // Append editor context to editor options, giving preference to existing context.
 136          $this->descriptionopts = array_merge(array('context' => $PAGE->context), $this->descriptionopts);
 137  
 138          // prepare the embeded files
 139          for ($i = 0; $i < $nodimensions; $i++) {
 140              // prepare all editor elements
 141              $fields = file_prepare_standard_editor($fields, 'description__idx_'.$i, $this->descriptionopts,
 142                  $PAGE->context, 'workshopform_numerrors', 'description', $fields->{'dimensionid__idx_'.$i});
 143          }
 144  
 145          $customdata = array();
 146          $customdata['workshop'] = $this->workshop;
 147          $customdata['strategy'] = $this;
 148          $customdata['norepeats'] = $norepeats;
 149          $customdata['nodimensions'] = $nodimensions;
 150          $customdata['descriptionopts'] = $this->descriptionopts;
 151          $customdata['current']  = $fields;
 152          $attributes = array('class' => 'editstrategyform');
 153  
 154          return new workshop_edit_numerrors_strategy_form($actionurl, $customdata, 'post', '', $attributes);
 155      }
 156  
 157      /**
 158       * Save the assessment dimensions into database
 159       *
 160       * Saves data into the main strategy form table. If the record->id is null or zero,
 161       * new record is created. If the record->id is not empty, the existing record is updated. Records with
 162       * empty 'description' field are removed from database.
 163       * The passed data object are the raw data returned by the get_data().
 164       *
 165       * @uses $DB
 166       * @param stdClass $data Raw data returned by the dimension editor form
 167       * @return void
 168       */
 169      public function save_edit_strategy_form(stdclass $data) {
 170          global $DB, $PAGE;
 171  
 172          $workshopid = $data->workshopid;
 173          $norepeats  = $data->norepeats;
 174  
 175          $data       = $this->prepare_database_fields($data);
 176          $records    = $data->numerrors; // data to be saved into {workshopform_numerrors}
 177          $mappings   = $data->mappings;  // data to be saved into {workshopform_numerrors_map}
 178          $todelete   = array();          // dimension ids to be deleted
 179          $maxnonegative = 0;             // maximum number of (weighted) negative responses
 180  
 181          for ($i=0; $i < $norepeats; $i++) {
 182              $record = $records[$i];
 183              if (0 == strlen(trim($record->description_editor['text']))) {
 184                  if (!empty($record->id)) {
 185                      // existing dimension record with empty description - to be deleted
 186                      $todelete[] = $record->id;
 187                  }
 188                  continue;
 189              }
 190              if (empty($record->id)) {
 191                  // new field
 192                  $record->id = $DB->insert_record('workshopform_numerrors', $record);
 193              } else {
 194                  // exiting field
 195                  $DB->update_record('workshopform_numerrors', $record);
 196              }
 197              $maxnonegative += $record->weight;
 198              // re-save with correct path to embeded media files
 199              $record = file_postupdate_standard_editor($record, 'description', $this->descriptionopts, $PAGE->context,
 200                                                        'workshopform_numerrors', 'description', $record->id);
 201              $DB->update_record('workshopform_numerrors', $record);
 202          }
 203          $this->delete_dimensions($todelete);
 204  
 205          // re-save the mappings
 206          $todelete = array();
 207          foreach ($data->mappings as $nonegative => $grade) {
 208              if (is_null($grade)) {
 209                  // no grade set for this number of negative responses
 210                  $todelete[] = $nonegative;
 211                  continue;
 212              }
 213              if (isset($this->mappings[$nonegative])) {
 214                  $DB->set_field('workshopform_numerrors_map', 'grade', $grade,
 215                              array('workshopid' => $this->workshop->id, 'nonegative' => $nonegative));
 216              } else {
 217                  $DB->insert_record('workshopform_numerrors_map',
 218                              (object)array('workshopid' => $this->workshop->id, 'nonegative' => $nonegative, 'grade' => $grade));
 219              }
 220          }
 221          // clear mappings that are not valid any more
 222          if (!empty($todelete)) {
 223              list($insql, $params) = $DB->get_in_or_equal($todelete, SQL_PARAMS_NAMED);
 224              $insql = "nonegative $insql OR ";
 225          } else {
 226              $insql = '';
 227          }
 228          $sql = "DELETE FROM {workshopform_numerrors_map}
 229                        WHERE (($insql nonegative > :maxnonegative) AND (workshopid = :workshopid))";
 230          $params['maxnonegative'] = $maxnonegative;
 231          $params['workshopid']   = $this->workshop->id;
 232          $DB->execute($sql, $params);
 233      }
 234  
 235      /**
 236       * Factory method returning an instance of an assessment form
 237       *
 238       * @param moodle_url $actionurl URL of form handler, defaults to auto detect the current url
 239       * @param string $mode          Mode to open the form in: preview/assessment
 240       * @param stdClass $assessment
 241       * @param bool $editable
 242       * @param array $options
 243       */
 244      public function get_assessment_form(moodle_url $actionurl=null, $mode='preview', stdclass $assessment=null, $editable=true, $options=array()) {
 245          global $CFG;    // needed because the included files use it
 246          global $PAGE;
 247          global $DB;
 248          require_once (__DIR__ . '/assessment_form.php');
 249  
 250          $fields         = $this->prepare_form_fields($this->dimensions, $this->mappings);
 251          $nodimensions   = count($this->dimensions);
 252  
 253          // rewrite URLs to the embeded files
 254          for ($i = 0; $i < $nodimensions; $i++) {
 255              $fields->{'description__idx_'.$i} = file_rewrite_pluginfile_urls($fields->{'description__idx_'.$i},
 256                  'pluginfile.php', $PAGE->context->id, 'workshopform_numerrors', 'description', $fields->{'dimensionid__idx_'.$i});
 257          }
 258  
 259          if ('assessment' === $mode and !empty($assessment)) {
 260              // load the previously saved assessment data
 261              $grades = $this->get_current_assessment_data($assessment);
 262              $current = new stdclass();
 263              for ($i = 0; $i < $nodimensions; $i++) {
 264                  $dimid = $fields->{'dimensionid__idx_'.$i};
 265                  if (isset($grades[$dimid])) {
 266                      $current->{'gradeid__idx_'.$i}      = $grades[$dimid]->id;
 267                      $current->{'grade__idx_'.$i}        = ($grades[$dimid]->grade == 0 ? -1 : 1);
 268                      $current->{'peercomment__idx_'.$i}  = $grades[$dimid]->peercomment;
 269                  }
 270              }
 271          }
 272  
 273          // set up the required custom data common for all strategies
 274          $customdata['workshop'] = $this->workshop;
 275          $customdata['strategy'] = $this;
 276          $customdata['mode']     = $mode;
 277          $customdata['options']  = $options;
 278  
 279          // set up strategy-specific custom data
 280          $customdata['nodims']   = $nodimensions;
 281          $customdata['fields']   = $fields;
 282          $customdata['current']  = isset($current) ? $current : null;
 283          $attributes = array('class' => 'assessmentform numerrors');
 284  
 285          return new workshop_numerrors_assessment_form($actionurl, $customdata, 'post', '', $attributes, $editable);
 286      }
 287  
 288      /**
 289       * Saves the filled assessment
 290       *
 291       * This method processes data submitted using the form returned by {@link get_assessment_form()}
 292       *
 293       * @param stdClass $assessment Assessment being filled
 294       * @param stdClass $data       Raw data as returned by the assessment form
 295       * @return float|null          Raw grade (from 0.00000 to 100.00000) for submission as suggested by the peer
 296       */
 297      public function save_assessment(stdclass $assessment, stdclass $data) {
 298          global $DB;
 299  
 300          if (!isset($data->nodims)) {
 301              throw new coding_exception('You did not send me the number of assessment dimensions to process');
 302          }
 303          for ($i = 0; $i < $data->nodims; $i++) {
 304              $grade = new stdclass();
 305              $grade->id                  = $data->{'gradeid__idx_' . $i};
 306              $grade->assessmentid        = $assessment->id;
 307              $grade->strategy            = 'numerrors';
 308              $grade->dimensionid         = $data->{'dimensionid__idx_' . $i};
 309              $grade->grade               = ($data->{'grade__idx_' . $i} <= 0 ? 0 : 1);
 310              $grade->peercomment         = $data->{'peercomment__idx_' . $i};
 311              $grade->peercommentformat   = FORMAT_HTML;
 312              if (empty($grade->id)) {
 313                  // new grade
 314                  $grade->id = $DB->insert_record('workshop_grades', $grade);
 315              } else {
 316                  // updated grade
 317                  $DB->update_record('workshop_grades', $grade);
 318              }
 319          }
 320          return $this->update_peer_grade($assessment);
 321      }
 322  
 323      /**
 324       * Has the assessment form been defined and is ready to be used by the reviewers?
 325       *
 326       * @return boolean
 327       */
 328      public function form_ready() {
 329          if (count($this->dimensions) > 0) {
 330              return true;
 331          }
 332          return false;
 333      }
 334  
 335      /**
 336       * @see parent::get_assessments_recordset()
 337       */
 338      public function get_assessments_recordset($restrict=null) {
 339         global $DB;
 340  
 341          $sql = 'SELECT s.id AS submissionid,
 342                         a.id AS assessmentid, a.weight AS assessmentweight, a.reviewerid, a.gradinggrade,
 343                         g.dimensionid, g.grade
 344                    FROM {workshop_submissions} s
 345                    JOIN {workshop_assessments} a ON (a.submissionid = s.id)
 346                    JOIN {workshop_grades} g ON (g.assessmentid = a.id AND g.strategy = :strategy)
 347                   WHERE s.example=0 AND s.workshopid=:workshopid'; // to be cont.
 348          $params = array('workshopid' => $this->workshop->id, 'strategy' => $this->workshop->strategy);
 349  
 350          if (is_null($restrict)) {
 351              // update all users - no more conditions
 352          } elseif (!empty($restrict)) {
 353              list($usql, $uparams) = $DB->get_in_or_equal($restrict, SQL_PARAMS_NAMED);
 354              $sql .= " AND a.reviewerid $usql";
 355              $params = array_merge($params, $uparams);
 356          } else {
 357              throw new coding_exception('Empty value is not a valid parameter here');
 358          }
 359  
 360          $sql .= ' ORDER BY s.id'; // this is important for bulk processing
 361  
 362          return $DB->get_recordset_sql($sql, $params);
 363  
 364      }
 365  
 366      /**
 367       * @see parent::get_dimensions_info()
 368       */
 369      public function get_dimensions_info() {
 370          global $DB;
 371  
 372          $params = array('workshopid' => $this->workshop->id);
 373          $dimrecords = $DB->get_records('workshopform_numerrors', array('workshopid' => $this->workshop->id), 'sort', 'id,weight');
 374          foreach ($dimrecords as $dimid => $dimrecord) {
 375              $dimrecords[$dimid]->min = 0;
 376              $dimrecords[$dimid]->max = 1;
 377          }
 378          return $dimrecords;
 379      }
 380  
 381      /**
 382       * Is a given scale used by the instance of workshop?
 383       *
 384       * This grading strategy does not use scales.
 385       *
 386       * @param int $scaleid id of the scale to check
 387       * @param int|null $workshopid id of workshop instance to check, checks all in case of null
 388       * @return bool
 389       */
 390      public static function scale_used($scaleid, $workshopid=null) {
 391          return false;
 392      }
 393  
 394      /**
 395       * Delete all data related to a given workshop module instance
 396       *
 397       * @see workshop_delete_instance()
 398       * @param int $workshopid id of the workshop module instance being deleted
 399       * @return void
 400       */
 401      public static function delete_instance($workshopid) {
 402          global $DB;
 403  
 404          $DB->delete_records('workshopform_numerrors', array('workshopid' => $workshopid));
 405          $DB->delete_records('workshopform_numerrors_map', array('workshopid' => $workshopid));
 406      }
 407  
 408      ////////////////////////////////////////////////////////////////////////////////
 409      // Internal methods                                                           //
 410      ////////////////////////////////////////////////////////////////////////////////
 411  
 412      /**
 413       * Loads the fields of the assessment form currently used in this workshop
 414       *
 415       * @return array definition of assessment dimensions
 416       */
 417      protected function load_fields() {
 418          global $DB;
 419  
 420          $sql = 'SELECT *
 421                    FROM {workshopform_numerrors}
 422                   WHERE workshopid = :workshopid
 423                   ORDER BY sort';
 424          $params = array('workshopid' => $this->workshop->id);
 425  
 426          return $DB->get_records_sql($sql, $params);
 427      }
 428  
 429      /**
 430       * Loads the mappings of the number of errors to the grade
 431       *
 432       * @return array of records
 433       */
 434      protected function load_mappings() {
 435          global $DB;
 436          return $DB->get_records('workshopform_numerrors_map', array('workshopid' => $this->workshop->id), 'nonegative',
 437                                  'nonegative,grade'); // we can use nonegative as key here as it must be unique within workshop
 438      }
 439  
 440      /**
 441       * Prepares the database data to be used by the mform
 442       *
 443       * @param array $dims Array of raw dimension records as returned by {@link load_fields()}
 444       * @param array $maps Array of raw mapping records as returned by {@link load_mappings()}
 445       * @return array Array of fields data to be used by the mform set_data
 446       */
 447      protected function prepare_form_fields(array $dims, array $maps) {
 448  
 449          $formdata = new stdclass();
 450          $key = 0;
 451          foreach ($dims as $dimension) {
 452              $formdata->{'dimensionid__idx_' . $key}             = $dimension->id;
 453              $formdata->{'description__idx_' . $key}             = $dimension->description;
 454              $formdata->{'description__idx_' . $key.'format'}    = $dimension->descriptionformat;
 455              $formdata->{'grade0__idx_' . $key}                  = $dimension->grade0;
 456              $formdata->{'grade1__idx_' . $key}                  = $dimension->grade1;
 457              $formdata->{'weight__idx_' . $key}                  = $dimension->weight;
 458              $key++;
 459          }
 460  
 461          foreach ($maps as $nonegative => $map) {
 462              $formdata->{'map__idx_' . $nonegative} = $map->grade;
 463          }
 464  
 465          return $formdata;
 466      }
 467  
 468      /**
 469       * Deletes dimensions and removes embedded media from its descriptions
 470       *
 471       * todo we may check that there are no assessments done using these dimensions and probably remove them
 472       *
 473       * @param array $ids list to delete
 474       * @return void
 475       */
 476      protected function delete_dimensions(array $ids) {
 477          global $DB, $PAGE;
 478  
 479          $fs         = get_file_storage();
 480          foreach ($ids as $id) {
 481              $fs->delete_area_files($PAGE->context->id, 'workshopform_numerrors', 'description', $id);
 482          }
 483          $DB->delete_records_list('workshopform_numerrors', 'id', $ids);
 484      }
 485  
 486      /**
 487       * Prepares data returned by {@link workshop_edit_numerrors_strategy_form} so they can be saved into database
 488       *
 489       * It automatically adds some columns into every record. The sorting is
 490       * done by the order of the returned array and starts with 1.
 491       * Called internally from {@link save_edit_strategy_form()} only. Could be private but
 492       * keeping protected for unit testing purposes.
 493       *
 494       * @param stdClass $raw Raw data returned by mform
 495       * @return array Array of objects to be inserted/updated in DB
 496       */
 497      protected function prepare_database_fields(stdclass $raw) {
 498          global $PAGE;
 499  
 500          $cook               = new stdclass();   // to be returned
 501          $cook->numerrors    = array();          // to be stored in {workshopform_numerrors}
 502          $cook->mappings     = array();          // to be stored in {workshopform_numerrors_map}
 503  
 504          for ($i = 0; $i < $raw->norepeats; $i++) {
 505              $cook->numerrors[$i]                        = new stdclass();
 506              $cook->numerrors[$i]->id                    = $raw->{'dimensionid__idx_'.$i};
 507              $cook->numerrors[$i]->workshopid            = $this->workshop->id;
 508              $cook->numerrors[$i]->sort                  = $i + 1;
 509              $cook->numerrors[$i]->description_editor    = $raw->{'description__idx_'.$i.'_editor'};
 510              $cook->numerrors[$i]->grade0                = $raw->{'grade0__idx_'.$i};
 511              $cook->numerrors[$i]->grade1                = $raw->{'grade1__idx_'.$i};
 512              $cook->numerrors[$i]->weight                = $raw->{'weight__idx_'.$i};
 513          }
 514  
 515          $i = 1;
 516          while (isset($raw->{'map__idx_'.$i})) {
 517              if (is_numeric($raw->{'map__idx_'.$i})) {
 518                  $cook->mappings[$i] = $raw->{'map__idx_'.$i}; // should be a value from 0 to 100
 519              } else {
 520                  $cook->mappings[$i] = null; // the user did not set anything
 521              }
 522              $i++;
 523          }
 524  
 525          return $cook;
 526      }
 527  
 528      /**
 529       * Returns the list of current grades filled by the reviewer
 530       *
 531       * @param stdClass $assessment Assessment record
 532       * @return array of filtered records from the table workshop_grades
 533       */
 534      protected function get_current_assessment_data(stdclass $assessment) {
 535          global $DB;
 536  
 537          if (empty($this->dimensions)) {
 538              return array();
 539          }
 540          list($dimsql, $dimparams) = $DB->get_in_or_equal(array_keys($this->dimensions), SQL_PARAMS_NAMED);
 541          // beware! the caller may rely on the returned array is indexed by dimensionid
 542          $sql = "SELECT dimensionid, wg.*
 543                    FROM {workshop_grades} wg
 544                   WHERE assessmentid = :assessmentid AND strategy= :strategy AND dimensionid $dimsql";
 545          $params = array('assessmentid' => $assessment->id, 'strategy' => 'numerrors');
 546          $params = array_merge($params, $dimparams);
 547  
 548          return $DB->get_records_sql($sql, $params);
 549      }
 550  
 551      /**
 552       * Aggregates the assessment form data and sets the grade for the submission given by the peer
 553       *
 554       * @param stdClass $assessment Assessment record
 555       * @return float|null          Raw grade (0.00000 to 100.00000) for submission as suggested by the peer
 556       */
 557      protected function update_peer_grade(stdclass $assessment) {
 558          $grades     = $this->get_current_assessment_data($assessment);
 559          $suggested  = $this->calculate_peer_grade($grades);
 560          if (!is_null($suggested)) {
 561              $this->workshop->set_peer_grade($assessment->id, $suggested);
 562          }
 563          return $suggested;
 564      }
 565  
 566      /**
 567       * Calculates the aggregated grade given by the reviewer
 568       *
 569       * @param array $grades Grade records as returned by {@link get_current_assessment_data}
 570       * @return float|null   Raw grade (0.00000 to 100.00000) for submission as suggested by the peer
 571       */
 572      protected function calculate_peer_grade(array $grades) {
 573          if (empty($grades)) {
 574              return null;
 575          }
 576          $sumerrors  = 0;    // sum of the weighted errors (i.e. the negative responses)
 577          foreach ($grades as $grade) {
 578              if (grade_floats_different($grade->grade, 1.00000)) {
 579                  // negative reviewer's response
 580                  $sumerrors += $this->dimensions[$grade->dimensionid]->weight;
 581              }
 582          }
 583          return $this->errors_to_grade($sumerrors);
 584      }
 585  
 586      /**
 587       * Returns a grade 0.00000 to 100.00000 for the given number of errors
 588       *
 589       * This is where we use the mapping table defined by the teacher. If a grade for the given
 590       * number of errors (negative assertions) is not defined, the most recently defined one is used.
 591       * Example of the defined mapping:
 592       * Number of errors | Grade
 593       *                0 | 100%  (always)
 594       *                1 | -     (not defined)
 595       *                2 | 80%
 596       *                3 | 60%
 597       *                4 | -
 598       *                5 | 30%
 599       *                6 | 0%
 600       * With this mapping, one error is mapped to 100% grade and 4 errors is mapped to 60%.
 601       *
 602       * @param mixed $numerrors Number of errors
 603       * @return float          Raw grade (0.00000 to 100.00000) for the given number of negative assertions
 604       */
 605      protected function errors_to_grade($numerrors) {
 606          $grade = 100.00000;
 607          for ($i = 1; $i <= $numerrors; $i++) {
 608              if (isset($this->mappings[$i])) {
 609                  $grade = $this->mappings[$i]->grade;
 610              }
 611          }
 612          if ($grade > 100.00000) {
 613              $grade = 100.00000;
 614          }
 615          if ($grade < 0.00000) {
 616              $grade = 0.00000;
 617          }
 618          return grade_floatval($grade);
 619      }
 620  }