2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * This file contains the marking guide editor element
20 * @package gradingform_guide
21 * @copyright 2012 Dan Marsden <dan@danmarsden.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 defined('MOODLE_INTERNAL') || die();
27 require_once("HTML/QuickForm/input.php");
30 * The editor for the marking guide advanced grading plugin.
32 * @package gradingform_guide
33 * @copyright 2012 Dan Marsden <dan@danmarsden.com>
34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36 class moodlequickform_guideeditor extends HTML_QuickForm_input {
37 /** @var string help message */
38 public $_helpbutton = '';
39 /** @var null|false|string stores the result of the last validation: null - undefined, false - no errors,
40 * string - error(s) text */
41 protected $validationerrors = null;
42 /** @var bool if element has already been validated **/
43 protected $wasvalidated = false;
44 /** @var null|bool If non-submit (JS) button was pressed: null - unknown, true/false - button was/wasn't pressed */
45 protected $nonjsbuttonpressed = false;
46 /** @var string|false Message to display in front of the editor (that there exist grades on this guide being edited) */
47 protected $regradeconfirmation = false;
52 * @param string $elementname
53 * @param string $elementlabel
54 * @param array $attributes
56 public function __construct($elementname=null, $elementlabel=null, $attributes=null) {
57 parent::__construct($elementname, $elementlabel, $attributes);
61 * Old syntax of class constructor. Deprecated in PHP7.
63 * @deprecated since Moodle 3.1
65 public function moodlequickform_guideeditor($elementname=null, $elementlabel=null, $attributes=null) {
66 debugging('Use of class name as constructor is deprecated', DEBUG_DEVELOPER);
67 self::__construct($elementname, $elementlabel, $attributes);
71 * get html for help button
73 * @return string html for help button
75 public function getHelpButton() {
76 return $this->_helpbutton;
80 * The renderer will take care itself about different display in normal and frozen states
84 public function getElementTemplateType() {
89 * Specifies that confirmation about re-grading needs to be added to this rubric editor.
90 * $changelevel is saved in $this->regradeconfirmation and retrieved in toHtml()
92 * @see gradingform_rubric_controller::update_or_check_rubric()
93 * @param int $changelevel
95 public function add_regrade_confirmation($changelevel) {
96 $this->regradeconfirmation = $changelevel;
100 * Returns html string to display this element
104 public function toHtml() {
106 $html = $this->_getTabs();
107 $renderer = $PAGE->get_renderer('gradingform_guide');
108 $data = $this->prepare_data(null, $this->wasvalidated);
109 if (!$this->_flagFrozen) {
110 $mode = gradingform_guide_controller::DISPLAY_EDIT_FULL;
111 $module = array('name'=>'gradingform_guideeditor',
112 'fullpath'=>'/grade/grading/form/guide/js/guideeditor.js',
113 'requires' => array('base', 'dom', 'event', 'event-touch', 'escape'),
115 array('confirmdeletecriterion', 'gradingform_guide'),
116 array('clicktoedit', 'gradingform_guide'),
117 array('clicktoeditname', 'gradingform_guide')
119 $PAGE->requires->js_init_call('M.gradingform_guideeditor.init', array(
120 array('name' => $this->getName(),
121 'criteriontemplate' => $renderer->criterion_template($mode, $data['options'], $this->getName()),
122 'commenttemplate' => $renderer->comment_template($mode, $this->getName())
126 // Guide is frozen, no javascript needed.
127 if ($this->_persistantFreeze) {
128 $mode = gradingform_guide_controller::DISPLAY_EDIT_FROZEN;
130 $mode = gradingform_guide_controller::DISPLAY_PREVIEW;
133 if ($this->regradeconfirmation) {
134 if (!isset($data['regrade'])) {
135 $data['regrade'] = 1;
137 $html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
139 if ($this->validationerrors) {
140 $html .= html_writer::div($renderer->notification($this->validationerrors, 'error'), '', array('role' => 'alert'));
142 $html .= $renderer->display_guide($data['criteria'], $data['comments'], $data['options'], $mode, $this->getName());
146 * Prepares the data passed in $_POST:
147 * - processes the pressed buttons 'addlevel', 'addcriterion', 'moveup', 'movedown', 'delete' (when JavaScript is disabled)
148 * sets $this->nonjsbuttonpressed to true/false if such button was pressed
149 * - if options not passed (i.e. we create a new guide) fills the options array with the default values
150 * - if options are passed completes the options array with unchecked checkboxes
151 * - if $withvalidation is set, adds 'error_xxx' attributes to elements that contain errors and creates an error string
152 * and stores it in $this->validationerrors
154 * @param array $value
155 * @param boolean $withvalidation whether to enable data validation
158 protected function prepare_data($value = null, $withvalidation = false) {
159 if (null === $value) {
160 $value = $this->getValue();
162 if ($this->nonjsbuttonpressed === null) {
163 $this->nonjsbuttonpressed = false;
167 $return = array('criteria' => array(), 'options' => gradingform_guide_controller::get_default_options(),
168 'comments' => array());
169 if (!isset($value['criteria'])) {
170 $value['criteria'] = array();
171 $errors['err_nocriteria'] = 1;
173 // If options are present in $value, replace default values with submitted values.
174 if (!empty($value['options'])) {
175 foreach (array_keys($return['options']) as $option) {
176 // Special treatment for checkboxes.
177 if (!empty($value['options'][$option])) {
178 $return['options'][$option] = $value['options'][$option];
180 $return['options'][$option] = null;
186 if (is_array($value)) {
187 // For other array keys of $value no special treatmeant neeeded, copy them to return value as is.
188 foreach (array_keys($value) as $key) {
189 if ($key != 'options' && $key != 'criteria' && $key != 'comments') {
190 $return[$key] = $value[$key];
195 // Iterate through criteria.
198 foreach ($value['criteria'] as $id => $criterion) {
199 if ($id == 'addcriterion') {
200 $id = $this->get_next_id(array_keys($value['criteria']));
201 $criterion = array('description' => '');
202 $this->nonjsbuttonpressed = true;
205 if ($withvalidation && !array_key_exists('delete', $criterion)) {
206 if (!strlen(trim($criterion['shortname']))) {
207 $errors['err_noshortname'] = 1;
208 $criterion['error_description'] = true;
210 if (strlen(trim($criterion['shortname'])) > 255) {
211 $errors['err_shortnametoolong'] = 1;
212 $criterion['error_description'] = true;
214 if (!strlen(trim($criterion['maxscore']))) {
215 $errors['err_nomaxscore'] = 1;
216 $criterion['error_description'] = true;
217 } else if (!is_numeric($criterion['maxscore'])) {
218 $errors['err_maxscorenotnumeric'] = 1;
219 $criterion['error_description'] = true;
220 } else if ($criterion['maxscore'] < 0) {
221 $errors['err_maxscoreisnegative'] = 1;
222 $criterion['error_description'] = true;
225 if (array_key_exists('moveup', $criterion) || $lastaction == 'movedown') {
226 unset($criterion['moveup']);
227 if ($lastid !== null) {
228 $lastcriterion = $return['criteria'][$lastid];
229 unset($return['criteria'][$lastid]);
230 $return['criteria'][$id] = $criterion;
231 $return['criteria'][$lastid] = $lastcriterion;
233 $return['criteria'][$id] = $criterion;
237 $this->nonjsbuttonpressed = true;
238 } else if (array_key_exists('delete', $criterion)) {
239 $this->nonjsbuttonpressed = true;
241 if (array_key_exists('movedown', $criterion)) {
242 unset($criterion['movedown']);
243 $lastaction = 'movedown';
244 $this->nonjsbuttonpressed = true;
246 $return['criteria'][$id] = $criterion;
251 // Add sort order field to criteria.
253 foreach (array_keys($return['criteria']) as $id) {
254 $return['criteria'][$id]['sortorder'] = $csortorder++;
257 // Iterate through comments.
260 if (!empty($value['comments'])) {
261 foreach ($value['comments'] as $id => $comment) {
262 if ($id == 'addcomment') {
263 $id = $this->get_next_id(array_keys($value['comments']));
264 $comment = array('description' => '');
265 $this->nonjsbuttonpressed = true;
268 if (array_key_exists('moveup', $comment) || $lastaction == 'movedown') {
269 unset($comment['moveup']);
270 if ($lastid !== null) {
271 $lastcomment = $return['comments'][$lastid];
272 unset($return['comments'][$lastid]);
273 $return['comments'][$id] = $comment;
274 $return['comments'][$lastid] = $lastcomment;
276 $return['comments'][$id] = $comment;
280 $this->nonjsbuttonpressed = true;
281 } else if (array_key_exists('delete', $comment)) {
282 $this->nonjsbuttonpressed = true;
284 if (array_key_exists('movedown', $comment)) {
285 unset($comment['movedown']);
286 $lastaction = 'movedown';
287 $this->nonjsbuttonpressed = true;
289 $return['comments'][$id] = $comment;
293 // Add sort order field to comments.
295 foreach (array_keys($return['comments']) as $id) {
296 $return['comments'][$id]['sortorder'] = $csortorder++;
299 // Create validation error string (if needed).
300 if ($withvalidation) {
301 if (count($errors)) {
303 foreach ($errors as $error => $v) {
304 $rv[] = get_string($error, 'gradingform_guide');
306 $this->validationerrors = join('<br/ >', $rv);
308 $this->validationerrors = false;
310 $this->wasvalidated = true;
317 * Scans array $ids to find the biggest element ! NEWID*, increments it by 1 and returns
322 protected function get_next_id($ids) {
324 foreach ($ids as $id) {
325 if (preg_match('/^NEWID(\d+)$/', $id, $matches) && ((int)$matches[1]) > $maxid) {
326 $maxid = (int)$matches[1];
329 return 'NEWID'.($maxid+1);
333 * Checks if a submit button was pressed which is supposed to be processed on client side by JS
334 * but user seem to have disabled JS in the browser.
335 * (buttons 'add criteria', 'add level', 'move up', 'move down', 'add comment')
336 * In this case the form containing this element is prevented from being submitted
338 * @param array $value
339 * @return boolean true if non-submit button was pressed and not processed by JS
341 public function non_js_button_pressed($value) {
342 if ($this->nonjsbuttonpressed === null) {
343 $this->prepare_data($value);
345 return $this->nonjsbuttonpressed;
349 * Validates that guide has at least one criterion, filled definitions and all criteria
350 * have filled descriptions
352 * @param array $value
353 * @return string|false error text or false if no errors found
355 public function validate($value) {
356 if (!$this->wasvalidated) {
357 $this->prepare_data($value, true);
359 return $this->validationerrors;
363 * Prepares the data for saving
364 * @see prepare_data()
366 * @param array $submitvalues
367 * @param boolean $assoc
370 public function exportValue(&$submitvalues, $assoc = false) {
371 $value = $this->prepare_data($this->_findValue($submitvalues));
372 return $this->_prepareValue($value, $assoc);