Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
    /** @var default number of dimensions to show */
85
    const MINDIMS = 3;
86
 
87
    /** @var 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
}