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