MDL-47752 grading: fixed bugs with the modgrade element
[moodle.git] / lib / form / modgrade.php
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/>.
18 /**
19  * Drop down form element to select the grade
20  *
21  * Contains HTML class for a drop down element to select the grade for an activity,
22  * used in mod update form
23  *
24  * @package   core_form
25  * @copyright 2006 Jamie Pratt <me@jamiep.org>
26  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27  */
29 global $CFG;
30 require_once "$CFG->libdir/form/select.php";
31 require_once("HTML/QuickForm/element.php");
32 require_once($CFG->dirroot.'/lib/form/group.php');
33 require_once($CFG->dirroot.'/lib/grade/grade_scale.php');
35 /**
36  * Drop down form element to select the grade
37  *
38  * HTML class for a drop down element to select the grade for an activity,
39  * used in mod update form
40  *
41  * @package   core_form
42  * @category  form
43  * @copyright 2006 Jamie Pratt <me@jamiep.org>
44  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
45  */
46 class MoodleQuickForm_modgrade extends MoodleQuickForm_group{
48     /**
49      * Constructor
50      *
51      * @param string $elementname Element's name
52      * @param mixed $elementlabel Label(s) for an element
53      * @param array $options Options to control the element's display. Not used.
54      * @param mixed $attributes Either a typical HTML attribute string or an associative array
55      */
56     public function MoodleQuickForm_modgrade($elementname = null, $elementlabel = null, $options = array(), $attributes = null) {
57         $this->HTML_QuickForm_element($elementname, $elementlabel, $attributes);
58         $this->_persistantFreeze = true;
59         $this->_appendName = true;
60         $this->_type = 'modgrade';
61     }
63     /**
64      * Create elements for this group.
65      */
66     public function _createElements() {
67         global $COURSE, $CFG;
68         $attributes = $this->getAttributes();
69         if (is_null($attributes)) {
70             $attributes = array();
71         }
73         $this->_elements = array();
75         // Create main elements
76         // We have to create the scale and point elements first, as we need their IDs.
78         // Grade scale select box.
79         $scales = get_scales_menu($COURSE->id);
80         $langscale = get_string('modgradetypescale', 'grades');
81         $scaleselect = @MoodleQuickForm::createElement('select', 'modgrade_scale', $langscale, $scales, $attributes);
82         $scaleselect->setHiddenLabel = false;
83         $scaleselect->_generateId();
84         $scaleselectid = $scaleselect->getAttribute('id');
86         // Maximum grade textbox.
87         $langmaxgrade = get_string('modgrademaxgrade', 'grades');
88         $maxgrade = @MoodleQuickForm::createElement('text', 'modgrade_point', $langmaxgrade, array());
89         $maxgrade->setHiddenLabel = false;
90         $maxgrade->_generateId();
91         $maxgradeid = $maxgrade->getAttribute('id');
93         // Grade type select box.
94         $gradetype = array(
95             'none' => get_string('modgradetypenone', 'grades'),
96             'scale' => get_string('modgradetypescale', 'grades'),
97             'point' => get_string('modgradetypepoint', 'grades'),
98         );
99         $langtype = get_string('modgradetype', 'grades');
100         $typeselect = @MoodleQuickForm::createElement('select', 'modgrade_type', $langtype, $gradetype, $attributes, true);
101         $typeselect->setHiddenLabel = false;
102         $typeselect->_generateId();
104         // Add elements.
106         // Grade type select box.
107         $label = html_writer::tag('label', $typeselect->getLabel(), array('for' => $typeselect->getAttribute('id')));
108         $this->_elements[] = @MoodleQuickForm::createElement('static', 'gradetypelabel', '', '&nbsp;'.$label);
109         $this->_elements[] = $typeselect;
110         $this->_elements[] = @MoodleQuickForm::createElement('static', 'gradetypespacer', '', '<br />');
112         // Grade scale select box.
113         $label = html_writer::tag('label', $scaleselect->getLabel(), array('for' => $scaleselectid));
114         $this->_elements[] = @MoodleQuickForm::createElement('static', 'scalelabel', '', $label);
115         $this->_elements[] = $scaleselect;
116         $this->_elements[] = @MoodleQuickForm::createElement('static', 'scalespacer', '', '<br />');
118         // Maximum grade textbox.
119         $label = html_writer::tag('label', $maxgrade->getLabel(), array('for' => $maxgradeid));
120         $this->_elements[] = @MoodleQuickForm::createElement('static', 'pointlabel', '', $label);
121         $this->_elements[] = $maxgrade;
122         $this->_elements[] = @MoodleQuickForm::createElement('static', 'pointspacer', '', '<br />');
123     }
125     /**
126      * Calculate the output value for the element as a whole.
127      *
128      * @param array $submitvalues The incoming values from the form.
129      * @param bool $notused Not used.
130      * @return array Return value for the element, formatted like field name => value.
131      */
132     public function exportValue(&$submitvalues, $notused = false) {
133         global $COURSE;
135         // Get the values from all the child elements.
136         $vals = array();
137         foreach ($this->_elements as $element) {
138             $thisexport = $element->exportValue($submitvalues[$this->getName()], true);
139             if (!is_null($thisexport)) {
140                 $vals += $thisexport;
141             }
142         }
144         $type = (isset($vals['modgrade_type'])) ? $vals['modgrade_type'] : 'none';
145         $point = (isset($vals['modgrade_point'])) ? $vals['modgrade_point'] : null;
146         $scale = (isset($vals['modgrade_scale'])) ? $vals['modgrade_scale'] : null;
147         $return = $this->process_value($type, $scale, $point);
148         return array($this->getName() => $return);
149     }
151     /**
152      * Process the value for the group based on the selected grade type, and the input for the scale and point elements.
153      *
154      * @param  string $type The value of the grade type select box. Can be 'none', 'scale', or 'point'
155      * @param  string|int $scale The value of the scale select box.
156      * @param  string|int $point The value of the point grade textbox.
157      * @return int The resulting value
158      */
159     protected function process_value($type='none', $scale=null, $point=null) {
160         global $COURSE;
161         $val = 0;
162         switch ($type) {
163             case 'point':
164                 if ($this->validate_point($point) === true) {
165                     $val = (int)$point;
166                 }
167                 break;
169             case 'scale':
170                 if ($this->validate_scale($scale)) {
171                     $val = (int)(-$scale);
172                 }
173                 break;
174         }
175         return $val;
176     }
178     /**
179      * Determines whether a given value is a valid scale selection.
180      *
181      * @param string|int $val The value to test.
182      * @return bool Valid or invalid
183      */
184     protected function validate_scale($val) {
185         global $COURSE;
186         $scales = get_scales_menu($COURSE->id);
187         return (!empty($val) && isset($scales[(int)$val])) ? true : false;
188     }
190     /**
191      * Determines whether a given value is a valid point selection.
192      *
193      * @param string|int $val The value to test.
194      * @return bool Valid or invalid
195      */
196     protected function validate_point($val) {
197         if (empty($val)) {
198             return false;
199         }
200         $maxgrade = (int)get_config('core', 'gradepointmax');
201         $isintlike = ((string)(int)$val === $val) ? true : false;
202         return ($isintlike === true && $val > 0 && $val <= $maxgrade) ? true : false;
203     }
205     /**
206      * Called by HTML_QuickForm whenever form event is made on this element.
207      *
208      * @param string $event Name of event
209      * @param mixed $arg event arguments
210      * @param moodleform $caller calling object
211      * @return mixed
212      */
213     public function onQuickFormEvent($event, $arg, &$caller) {
214         switch ($event) {
215             case 'createElement':
216                 // The first argument is the name.
217                 $name = $arg[0];
219                 // Set disable actions.
220                 $caller->disabledIf($name.'[modgrade_scale]', $name.'[modgrade_type]', 'neq', 'scale');
221                 $caller->disabledIf($name.'[modgrade_point]', $name.'[modgrade_type]', 'neq', 'point');
223                 // Set validation rules for the sub-elements belonging to this element.
224                 // A handy note: the parent scope of a closure is the function in which the closure was declared.
225                 // Because of this using $this is safe despite the closures being called statically.
226                 // A nasty magic hack!
227                 $checkmaxgrade = function($val) {
228                     // Closure to validate a max points value. See the note above about scope if this confuses you.
229                     if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'point') {
230                         if (!isset($val['modgrade_point'])) {
231                             return false;
232                         }
233                         return $this->validate_point($val['modgrade_point']);
234                     }
235                     return true;
236                 };
237                 $checkvalidscale = function($val) {
238                     // Closure to validate a scale value. See the note above about scope if this confuses you.
239                     if (isset($val['modgrade_type']) && $val['modgrade_type'] === 'scale') {
240                         if (!isset($val['modgrade_scale'])) {
241                             return false;
242                         }
243                         return $this->validate_scale($val['modgrade_scale']);
244                     }
245                     return true;
246                 };
248                 $maxgradeexceeded = get_string('modgradeerrorbadpoint', 'grades', get_config('core', 'gradepointmax'));
249                 $invalidscale = get_string('modgradeerrorbadscale', 'grades');
250                 // When creating the rules the sixth arg is $force, we set it to true because otherwise the form
251                 // will attempt to validate the existence of the element, we don't want this because the element
252                 // is being created right now and doesn't actually exist as a registered element yet.
253                 $caller->addRule($name, $maxgradeexceeded, 'callback', $checkmaxgrade, 'server', false, true);
254                 $caller->addRule($name, $invalidscale, 'callback', $checkvalidscale, 'server', false, true);
256                 break;
258             case 'updateValue':
259                 // As this is a group element with no value of its own we are only interested in situations where the
260                 // default value or a constant value are being provided to the actual element.
261                 // In this case we expect an int that is going to translate to a scale if negative, or to max points
262                 // if positive.
264                 // A constant value should be given as an int.
265                 // The default value should be an int and should really be $CFG->gradepointdefault.
266                 $value = $this->_findValue($caller->_constantValues);
267                 if (null === $value) {
268                     if ($caller->isSubmitted()) {
269                         break;
270                     }
271                     $value = $this->_findValue($caller->_defaultValues);
272                 }
274                 if (!is_null($value) && !is_scalar($value)) {
275                     // Something unexpected (likely an array of subelement values) has been given - this will be dealt
276                     // with somewhere else - where exactly... likely the subelements.
277                     debugging('An invalid value (type '.gettype($value).') has arrived at '.__METHOD__, DEBUG_DEVELOPER);
278                     break;
279                 }
281                 // Set element state for existing data.
282                 // This is really a pretty hacky thing to do, when data is being set the group element is called
283                 // with the data first and the subelements called afterwards.
284                 // This means that the subelements data (inc const and default values) can be overridden by form code.
285                 // So - when we call this code really we can't be sure that will be the end value for the element.
286                 if (!empty($this->_elements)) {
287                     if (!empty($value)) {
288                         if ($value < 0) {
289                             $this->_elements[1]->setValue('scale');
290                             $this->_elements[4]->setValue(($value * -1));
291                         } else if ($value > 0) {
292                             $this->_elements[1]->setValue('point');
293                             $this->_elements[7]->setValue($value);
294                         }
295                     } else {
296                         $this->_elements[1]->setValue('none');
297                         $this->_elements[7]->setValue('');
298                     }
299                 }
300                 break;
301         }
303         // Always let the parent do its thing!
304         return parent::onQuickFormEvent($event, $arg, $caller);
305     }