source: moodle/trunk/fuentes/grade/grading/form/rubric/rubriceditor.php @ 1331

Last change on this file since 1331 was 1331, checked in by jrpelegrina, 3 years ago

Updated to moodle 3.0.3

File size: 15.6 KB
Line 
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 * File contains definition of class MoodleQuickForm_rubriceditor
19 *
20 * @package    gradingform_rubric
21 * @copyright  2011 Marina Glancy
22 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25defined('MOODLE_INTERNAL') || die();
26
27require_once("HTML/QuickForm/input.php");
28
29/**
30 * Form element for handling rubric editor
31 *
32 * The rubric editor is defined as a separate form element. This allows us to render
33 * criteria, levels and buttons using the rubric's own renderer. Also, the required
34 * Javascript library is included, which processes, on the client, buttons needed
35 * for reordering, adding and deleting criteria.
36 *
37 * If Javascript is disabled when one of those special buttons is pressed, the form
38 * element is not validated and, instead of submitting the form, we process button presses.
39 *
40 * @package    gradingform_rubric
41 * @copyright  2011 Marina Glancy
42 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43 */
44class MoodleQuickForm_rubriceditor extends HTML_QuickForm_input {
45    /** @var string help message */
46    public $_helpbutton = '';
47    /** @var string|bool stores the result of the last validation: null - undefined, false - no errors, string - error(s) text */
48    protected $validationerrors = null;
49    /** @var bool if element has already been validated **/
50    protected $wasvalidated = false;
51    /** @var bool If non-submit (JS) button was pressed: null - unknown, true/false - button was/wasn't pressed */
52    protected $nonjsbuttonpressed = false;
53    /** @var bool Message to display in front of the editor (that there exist grades on this rubric being edited) */
54    protected $regradeconfirmation = false;
55
56    /**
57     * Constructor for rubric editor
58     *
59     * @param string $elementName
60     * @param string $elementLabel
61     * @param array $attributes
62     */
63    public function __construct($elementName=null, $elementLabel=null, $attributes=null) {
64        parent::__construct($elementName, $elementLabel, $attributes);
65    }
66
67    /**
68     * Old syntax of class constructor for backward compatibility.
69     */
70    public function MoodleQuickForm_rubriceditor($elementName=null, $elementLabel=null, $attributes=null) {
71        self::__construct($elementName, $elementLabel, $attributes);
72    }
73
74    /**
75     * get html for help button
76     *
77     * @return string html for help button
78     */
79    public function getHelpButton() {
80        return $this->_helpbutton;
81    }
82
83    /**
84     * The renderer will take care itself about different display in normal and frozen states
85     *
86     * @return string
87     */
88    public function getElementTemplateType() {
89        return 'default';
90    }
91
92    /**
93     * Specifies that confirmation about re-grading needs to be added to this rubric editor.
94     * $changelevel is saved in $this->regradeconfirmation and retrieved in toHtml()
95     *
96     * @see gradingform_rubric_controller::update_or_check_rubric()
97     * @param int $changelevel
98     */
99    public function add_regrade_confirmation($changelevel) {
100        $this->regradeconfirmation = $changelevel;
101    }
102
103    /**
104     * Returns html string to display this element
105     *
106     * @return string
107     */
108    public function toHtml() {
109        global $PAGE;
110        $html = $this->_getTabs();
111        $renderer = $PAGE->get_renderer('gradingform_rubric');
112        $data = $this->prepare_data(null, $this->wasvalidated);
113        if (!$this->_flagFrozen) {
114            $mode = gradingform_rubric_controller::DISPLAY_EDIT_FULL;
115            $module = array('name'=>'gradingform_rubriceditor', 'fullpath'=>'/grade/grading/form/rubric/js/rubriceditor.js',
116                'requires' => array('base', 'dom', 'event', 'event-touch', 'escape'),
117                'strings' => array(array('confirmdeletecriterion', 'gradingform_rubric'), array('confirmdeletelevel', 'gradingform_rubric'),
118                    array('criterionempty', 'gradingform_rubric'), array('levelempty', 'gradingform_rubric')
119                    ));
120            $PAGE->requires->js_init_call('M.gradingform_rubriceditor.init', array(
121                array('name' => $this->getName(),
122                    'criteriontemplate' => $renderer->criterion_template($mode, $data['options'], $this->getName()),
123                    'leveltemplate' => $renderer->level_template($mode, $data['options'], $this->getName())
124                   )),
125                true, $module);
126        } else {
127            // Rubric is frozen, no javascript needed
128            if ($this->_persistantFreeze) {
129                $mode = gradingform_rubric_controller::DISPLAY_EDIT_FROZEN;
130            } else {
131                $mode = gradingform_rubric_controller::DISPLAY_PREVIEW;
132            }
133        }
134        if ($this->regradeconfirmation) {
135            if (!isset($data['regrade'])) {
136                $data['regrade'] = 1;
137            }
138            $html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
139        }
140        if ($this->validationerrors) {
141            $html .= $renderer->notification($this->validationerrors);
142        }
143        $html .= $renderer->display_rubric($data['criteria'], $data['options'], $mode, $this->getName());
144        return $html;
145    }
146
147    /**
148     * Prepares the data passed in $_POST:
149     * - processes the pressed buttons 'addlevel', 'addcriterion', 'moveup', 'movedown', 'delete' (when JavaScript is disabled)
150     *   sets $this->nonjsbuttonpressed to true/false if such button was pressed
151     * - if options not passed (i.e. we create a new rubric) fills the options array with the default values
152     * - if options are passed completes the options array with unchecked checkboxes
153     * - if $withvalidation is set, adds 'error_xxx' attributes to elements that contain errors and creates an error string
154     *   and stores it in $this->validationerrors
155     *
156     * @param array $value
157     * @param boolean $withvalidation whether to enable data validation
158     * @return array
159     */
160    protected function prepare_data($value = null, $withvalidation = false) {
161        if (null === $value) {
162            $value = $this->getValue();
163        }
164        if ($this->nonjsbuttonpressed === null) {
165            $this->nonjsbuttonpressed = false;
166        }
167        $totalscore = 0;
168        $errors = array();
169        $return = array('criteria' => array(), 'options' => gradingform_rubric_controller::get_default_options());
170        if (!isset($value['criteria'])) {
171            $value['criteria'] = array();
172            $errors['err_nocriteria'] = 1;
173        }
174        // If options are present in $value, replace default values with submitted values
175        if (!empty($value['options'])) {
176            foreach (array_keys($return['options']) as $option) {
177                // special treatment for checkboxes
178                if (!empty($value['options'][$option])) {
179                    $return['options'][$option] = $value['options'][$option];
180                } else {
181                    $return['options'][$option] = null;
182                }
183            }
184        }
185        if (is_array($value)) {
186            // for other array keys of $value no special treatmeant neeeded, copy them to return value as is
187            foreach (array_keys($value) as $key) {
188                if ($key != 'options' && $key != 'criteria') {
189                    $return[$key] = $value[$key];
190                }
191            }
192        }
193
194        // iterate through criteria
195        $lastaction = null;
196        $lastid = null;
197        foreach ($value['criteria'] as $id => $criterion) {
198            if ($id == 'addcriterion') {
199                $id = $this->get_next_id(array_keys($value['criteria']));
200                $criterion = array('description' => '', 'levels' => array());
201                $i = 0;
202                // when adding new criterion copy the number of levels and their scores from the last criterion
203                if (!empty($value['criteria'][$lastid]['levels'])) {
204                    foreach ($value['criteria'][$lastid]['levels'] as $lastlevel) {
205                        $criterion['levels']['NEWID'.($i++)]['score'] = $lastlevel['score'];
206                    }
207                } else {
208                    $criterion['levels']['NEWID'.($i++)]['score'] = 0;
209                }
210                // add more levels so there are at least 3 in the new criterion. Increment by 1 the score for each next one
211                for ($i=$i; $i<3; $i++) {
212                    $criterion['levels']['NEWID'.$i]['score'] = $criterion['levels']['NEWID'.($i-1)]['score'] + 1;
213                }
214                // set other necessary fields (definition) for the levels in the new criterion
215                foreach (array_keys($criterion['levels']) as $i) {
216                    $criterion['levels'][$i]['definition'] = '';
217                }
218                $this->nonjsbuttonpressed = true;
219            }
220            $levels = array();
221            $maxscore = null;
222            if (array_key_exists('levels', $criterion)) {
223                foreach ($criterion['levels'] as $levelid => $level) {
224                    if ($levelid == 'addlevel') {
225                        $levelid = $this->get_next_id(array_keys($criterion['levels']));
226                        $level = array(
227                            'definition' => '',
228                            'score' => 0,
229                        );
230                        foreach ($criterion['levels'] as $lastlevel) {
231                            if (isset($lastlevel['score']) && $level['score'] < $lastlevel['score'] + 1) {
232                                $level['score'] = $lastlevel['score'] + 1;
233                            }
234                        }
235                        $this->nonjsbuttonpressed = true;
236                    }
237                    if (!array_key_exists('delete', $level)) {
238                        if ($withvalidation) {
239                            if (!strlen(trim($level['definition']))) {
240                                $errors['err_nodefinition'] = 1;
241                                $level['error_definition'] = true;
242                            }
243                            if (!preg_match('#^[\+]?\d*$#', trim($level['score'])) && !preg_match('#^[\+]?\d*[\.,]\d+$#', trim($level['score']))) {
244                                $errors['err_scoreformat'] = 1;
245                                $level['error_score'] = true;
246                            }
247                        }
248                        $levels[$levelid] = $level;
249                        if ($maxscore === null || (float)$level['score'] > $maxscore) {
250                            $maxscore = (float)$level['score'];
251                        }
252                    } else {
253                        $this->nonjsbuttonpressed = true;
254                    }
255                }
256            }
257            $totalscore += (float)$maxscore;
258            $criterion['levels'] = $levels;
259            if ($withvalidation && !array_key_exists('delete', $criterion)) {
260                if (count($levels)<2) {
261                    $errors['err_mintwolevels'] = 1;
262                    $criterion['error_levels'] = true;
263                }
264                if (!strlen(trim($criterion['description']))) {
265                    $errors['err_nodescription'] = 1;
266                    $criterion['error_description'] = true;
267                }
268            }
269            if (array_key_exists('moveup', $criterion) || $lastaction == 'movedown') {
270                unset($criterion['moveup']);
271                if ($lastid !== null) {
272                    $lastcriterion = $return['criteria'][$lastid];
273                    unset($return['criteria'][$lastid]);
274                    $return['criteria'][$id] = $criterion;
275                    $return['criteria'][$lastid] = $lastcriterion;
276                } else {
277                    $return['criteria'][$id] = $criterion;
278                }
279                $lastaction = null;
280                $lastid = $id;
281                $this->nonjsbuttonpressed = true;
282            } else if (array_key_exists('delete', $criterion)) {
283                $this->nonjsbuttonpressed = true;
284            } else {
285                if (array_key_exists('movedown', $criterion)) {
286                    unset($criterion['movedown']);
287                    $lastaction = 'movedown';
288                    $this->nonjsbuttonpressed = true;
289                }
290                $return['criteria'][$id] = $criterion;
291                $lastid = $id;
292            }
293        }
294
295        if ($totalscore <= 0) {
296            $errors['err_totalscore'] = 1;
297        }
298
299        // add sort order field to criteria
300        $csortorder = 1;
301        foreach (array_keys($return['criteria']) as $id) {
302            $return['criteria'][$id]['sortorder'] = $csortorder++;
303        }
304
305        // create validation error string (if needed)
306        if ($withvalidation) {
307            if (count($errors)) {
308                $rv = array();
309                foreach ($errors as $error => $v) {
310                    $rv[] = get_string($error, 'gradingform_rubric');
311                }
312                $this->validationerrors = join('<br/ >', $rv);
313            } else {
314                $this->validationerrors = false;
315            }
316            $this->wasvalidated = true;
317        }
318        return $return;
319    }
320
321    /**
322     * Scans array $ids to find the biggest element ! NEWID*, increments it by 1 and returns
323     *
324     * @param array $ids
325     * @return string
326     */
327    protected function get_next_id($ids) {
328        $maxid = 0;
329        foreach ($ids as $id) {
330            if (preg_match('/^NEWID(\d+)$/', $id, $matches) && ((int)$matches[1]) > $maxid) {
331                $maxid = (int)$matches[1];
332            }
333        }
334        return 'NEWID'.($maxid+1);
335    }
336
337    /**
338     * Checks if a submit button was pressed which is supposed to be processed on client side by JS
339     * but user seem to have disabled JS in the browser.
340     * (buttons 'add criteria', 'add level', 'move up', 'move down', etc.)
341     * In this case the form containing this element is prevented from being submitted
342     *
343     * @param array $value
344     * @return boolean true if non-submit button was pressed and not processed by JS
345     */
346    public function non_js_button_pressed($value) {
347        if ($this->nonjsbuttonpressed === null) {
348            $this->prepare_data($value);
349        }
350        return $this->nonjsbuttonpressed;
351    }
352
353    /**
354     * Validates that rubric has at least one criterion, at least two levels within one criterion,
355     * each level has a valid score, all levels have filled definitions and all criteria
356     * have filled descriptions
357     *
358     * @param array $value
359     * @return string|false error text or false if no errors found
360     */
361    public function validate($value) {
362        if (!$this->wasvalidated) {
363            $this->prepare_data($value, true);
364        }
365        return $this->validationerrors;
366    }
367
368    /**
369     * Prepares the data for saving
370     *
371     * @see prepare_data()
372     * @param array $submitValues
373     * @param boolean $assoc
374     * @return array
375     */
376    public function exportValue(&$submitValues, $assoc = false) {
377        $value =  $this->prepare_data($this->_findValue($submitValues));
378        return $this->_prepareValue($value, $assoc);
379    }
380}
Note: See TracBrowser for help on using the repository browser.