MDL-30085 core_grade: added a grade web service
[moodle.git] / lib / grade / externallib.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  * External grade API
20  *
21  * @package    core_grade
22  * @category   external
23  * @copyright  2012 Andrew Davis
24  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25  */
27 require_once("$CFG->libdir/externallib.php");
29 /**
30  * Grade external functions
31  *
32  * @package    core_grade
33  * @category   external
34  * @copyright  2012 Andrew Davis
35  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36  * @since Moodle 2.6
37  */
38 class core_grade_external extends external_api {
39     /**
40      * Returns description of method parameters
41      *
42      * @return external_function_parameters
43      * @since Moodle 2.6
44      */
45     public static function get_grades_parameters() {
46         return new external_function_parameters(
47             array(
48                 'courseid' => new external_value(PARAM_INT, 'id of course'),
49                 'component' => new external_value(PARAM_COMPONENT, 'A component, for example mod_forum or mod_quiz', VALUE_DEFAULT, ''),
50                 'activityid' => new external_value(PARAM_INT, 'The activity ID', VALUE_DEFAULT, null),
51                 'userids' => new external_multiple_structure(
52                     new external_value(PARAM_INT, 'user ID'), 'An array of user IDs, leave empty to just retrieve grade item information', VALUE_DEFAULT, array()
53                 )
54             )
55         );
56     }
58     /**
59      * Retrieve grade items and, optionally, student grades
60      *
61      * @param array $grades array of grade information
62      * @return array of newly created groups
63      * @since Moodle 2.6
64      */
65     public static function get_grades($courseid, $component = null, $activityid = null, $userids = array()) {
66         global $CFG, $USER, $DB;
67         require_once("$CFG->libdir/gradelib.php");
69         $params = self::validate_parameters(self::get_grades_parameters(),
70             array('courseid' => $courseid, 'component' => $component, 'activityid' => $activityid, 'userids' => $userids));
72         $coursecontext = context_course::instance($params['courseid']);
74         try {
75             self::validate_context($coursecontext);
76         } catch (Exception $e) {
77             $exceptionparam = new stdClass();
78             $exceptionparam->message = $e->getMessage();
79             $exceptionparam->courseid = $params['courseid'];
80             throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam);
81         }
83         $course = $DB->get_record('course', array('id' => $params['courseid']));
85         $access = false;
86         if (has_capability('moodle/grade:viewall', $coursecontext)) {
87             // Can view all user's grades in this course.
88             $access = true;
90         } else if ($course->showgrades && count($params['userids']) == 1) {
91             // Course showgrades == students/parents can access grades.
93             if ($params['userids'][0] == $USER->id and has_capability('moodle/grade:view', $coursecontext)) {
94                 // Student can view their own grades in this course.
95                 $access = true;
97             } else if (has_capability('moodle/grade:viewall', context_user::instance($params['userids'][0]))) {
98                 // User can view the grades of this user. Parent most probably.
99                 $access = true;
100             }
101         }
103         if (!$access) {
104             throw new moodle_exception('nopermissiontoviewgrades', 'error');
105         }
107         $itemtype = null;
108         $itemmodule = null;
109         if (!empty($params['component'])) {
110             list($itemtype, $itemmodule) = normalize_component($params['component']);
111         }
113         $cm = null;
114         if (!empty($itemmodule) && !empty($activityid)) {
115             if (! $cm = get_coursemodule_from_id($itemmodule, $activityid)) {
116                 throw new moodle_exception('invalidcoursemodule');
117             }
118         }
120         $cminstanceid = null;
121         if (!empty($cm)) {
122             $cminstanceid = $cm->instance;
123         }
124         $grades = grade_get_grades($params['courseid'], $itemtype, $itemmodule, $cminstanceid, $params['userids']);
126         $activity_instances = null;
127         if (empty($cm)) {
128             // If we're dealing with multiple activites load all the module info.
129             $modinfo = get_fast_modinfo($params['courseid']);
130             $activity_instances = $modinfo->get_instances();
131         }
133         foreach ($grades->items as $gradeitem) {
134             if (!empty($cm)) {
135                 // If they only requested one activity we will already have the cm.
136                 $modulecm = $cm;
137             } else if (!empty($gradeitem->itemmodule)) {
138                 $modulecm = $activity_instances[$gradeitem->itemmodule][$gradeitem->iteminstance];
139             } else {
140                 // Course grade item.
141                 continue;
142             }
144             // Make student feedback ready for output.
145             foreach ($gradeitem->grades as $studentgrade) {
146                 if (!empty($studentgrade->feedback)) {
147                     list($studentgrade->feedback, $categoryinfo->feedbackformat) =
148                         external_format_text($studentgrade->feedback, $studentgrade->feedbackformat,
149                         $modulecm->id, $params['component'], 'feedback', null);
150                 }
151             }
152         }
154         // Convert from objects to arrays so all web service clients are supported.
155         // While we're doing that we also remove grades the current user can't see due to hiding
156         $grades_array = array();
157         $canviewhidden = has_capability('moodle/grade:viewhidden', context_course::instance($params['courseid']));
159         $grades_array['items'] = array();
160         foreach ($grades->items as $grade_item) {
161             // Switch the stdClass instance for a grade item instance so we can call is_hidden() and use the ID.
162             $grade_item_instance = self::get_grade_item($course->id, $grade_item->itemtype, $grade_item->itemmodule, $grade_item->iteminstance, 0);
163             if (!$canviewhidden && $grade_item_instance->is_hidden()) {
164                 continue;
165             }
166             $grade_item_array = (array)$grade_item;
167             $grade_item_array['grades'] = array();
169             if (!empty($grade_item->grades)) {
170                 foreach ($grade_item->grades as $studentid => $studentgrade) {
171                     $grade_grade_instance = grade_grade::fetch(
172                         array(
173                             'userid' => $studentid,
174                             'itemid' => $grade_item_instance->id
175                         )
176                     );
177                     if (!$canviewhidden && $grade_grade_instance->is_hidden()) {
178                         continue;
179                     }
180                     $grade_item_array['grades'][$studentid] = (array)$studentgrade;
181                     // Add the student ID as some WS clients can't access the array key.
182                     $grade_item_array['grades'][$studentid]['userid'] = $studentid;
183                 }
184             }
186             // If they requested grades for multiple activities load the cm object now.
187             $modulecm = $cm;
188             if (empty($modulecm) && !empty($grade_item_instance->itemmodule)) {
189                 $modulecm = $activity_instances[$grade_item_instance->itemmodule][$grade_item_instance->iteminstance];
190             }
191             if ($grade_item_instance->itemtype == 'course') {
192                 $grades_array['items']['course'] = $grade_item_array;
193                 $grades_array['items']['course']['activityid'] = 'course';
194             } else {
195                 $grades_array['items'][$modulecm->id] = $grade_item_array;
196                 // Add the activity ID as some WS clients can't access the array key.
197                 $grades_array['items'][$modulecm->id]['activityid'] = $modulecm->id;
198             }
199         }
201         $grades_array['outcomes'] = array();
202         foreach ($grades->outcomes as $outcome) {
203             $modulecm = $cm;
204             if (empty($modulecm)) {
205                 $modulecm = $activity_instances[$outcome->itemmodule][$outcome->iteminstance];
206             }
207             $grades_array['outcomes'][$modulecm->id] = (array)$outcome;
208             $grades_array['outcomes'][$modulecm->id]['activityid'] = $modulecm->id;
210             $grades_array['outcomes'][$modulecm->id]['grades'] = array();
211             if (!empty($outcome->grades)) {
212                 foreach ($outcome->grades as $studentid => $studentgrade) {
213                     if (!$canviewhidden) {
214                         // Need to load the grade_grade object to check visibility.
215                         $grade_item_instance = self::get_grade_item($course->id, $outcome->itemtype, $outcome->itemmodule, $outcome->iteminstance, $outcome->itemnumber);
216                         $grade_grade_instance = grade_grade::fetch(
217                             array(
218                                 'userid' => $studentid,
219                                 'itemid' => $grade_item_instance->id
220                             )
221                         );
222                         // The grade grade may be legitimately missing if the student has no grade.
223                         if (!empty($grade_grade_instance) && $grade_grade_instance->is_hidden()) {
224                             continue;
225                         }
226                     }
227                     $grades_array['outcomes'][$modulecm->id]['grades'][$studentid] = (array)$studentgrade;
229                     // Add the student ID into the grade structure as some WS clients can't access the key.
230                     $grades_array['outcomes'][$modulecm->id]['grades'][$studentid]['userid'] = $studentid;
231                 }
232             }
233         }
235         return $grades_array;
236     }
238     private static function get_grade_item($courseid, $itemtype, $itemmodule = null, $iteminstance = null, $itemnumber = null) {
239         $grade_item_instance = null;
240         if ($itemtype == 'course') {
241             $grade_item_instance = grade_item::fetch(array('courseid' => $courseid, 'itemtype' => $itemtype));
242         } else {
243             $grade_item_instance = grade_item::fetch(array('courseid' => $courseid, 'itemtype' => $itemtype, 'itemmodule' => $itemmodule, 'iteminstance' => $iteminstance, 'itemnumber' => $itemnumber));
244         }
245         return $grade_item_instance;
246     }
248     /**
249      * Returns description of method result value
250      *
251      * @return external_description
252      * @since Moodle 2.6
253      */
254     public static function get_grades_returns() {
255         return new external_single_structure(
256             array(
257                 'items'  => new external_multiple_structure(
258                     new external_single_structure(
259                         array(
260                             'activityid' => new external_value(PARAM_ALPHANUM, 'The ID of the activity or "course" for the course grade item'),
261                             'itemnumber'  => new external_value(PARAM_INT, 'Will be 0 unless the module has multiple grades'),
262                             'scaleid' => new external_value(PARAM_INT, 'The ID of the custom scale or 0'),
263                             'name' => new external_value(PARAM_RAW, 'The module name'),
264                             'grademin' => new external_value(PARAM_FLOAT, 'Minimum grade'),
265                             'grademax' => new external_value(PARAM_FLOAT, 'Maximum grade'),
266                             'gradepass' => new external_value(PARAM_FLOAT, 'The passing grade threshold'),
267                             'locked' => new external_value(PARAM_BOOL, 'Is the grade item locked?'),
268                             'hidden' => new external_value(PARAM_BOOL, 'Is the grade item hidden?'),
269                             'grades' => new external_multiple_structure(
270                                 new external_single_structure(
271                                     array(
272                                         'userid' => new external_value(PARAM_INT, 'Student ID'),
273                                         'grade' => new external_value(PARAM_FLOAT, 'Student grade'),
274                                         'locked' => new external_value(PARAM_BOOL, 'Is the student\'s grade locked?'),
275                                         'hidden' => new external_value(PARAM_BOOL, 'Is the student\'s grade hidden?'),
276                                         'overridden' => new external_value(PARAM_BOOL, 'Is the student\'s grade overridden?'),
277                                         'feedback' => new external_value(PARAM_RAW, 'Feedback from the grader'),
278                                         'feedbackformat' => new external_value(PARAM_INT, 'The format of the feedback'),
279                                         'usermodified' => new external_value(PARAM_INT, 'The ID of the last user to modify this student grade'),
280                                         'datesubmitted' => new external_value(PARAM_INT, 'A timestamp indicating when the student submitted the activity'), 
281                                         'dategraded' => new external_value(PARAM_INT, 'A timestamp indicating when the assignment was grades'),
282                                         'str_grade' => new external_value(PARAM_RAW, 'A string representation of the grade'),
283                                         'str_long_grade' => new external_value(PARAM_RAW, 'A nicely formatted string representation of the grade'),
284                                         'str_feedback' => new external_value(PARAM_TEXT, 'A string representation of the feedback from the grader'),
285                                     )
286                                 )
287                             ),
288                         )
289                     )
290                 ),
291                 'outcomes'  => new external_multiple_structure(
292                     new external_single_structure(
293                         array(
294                             'activityid' => new external_value(PARAM_ALPHANUM, 'The ID of the activity or "course" for the course grade item'),
295                             'itemnumber'  => new external_value(PARAM_INT, 'Will be 0 unless the module has multiple grades'),
296                             'scaleid' => new external_value(PARAM_INT, 'The ID of the custom scale or 0'),
297                             'name' => new external_value(PARAM_RAW, 'The module name'),
298                             'locked' => new external_value(PARAM_BOOL, 'Is the grade item locked?'),
299                             'hidden' => new external_value(PARAM_BOOL, 'Is the grade item hidden?'),
300                             'grades' => new external_multiple_structure(
301                                 new external_single_structure(
302                                     array(
303                                         'userid' => new external_value(PARAM_INT, 'Student ID'),
304                                         'grade' => new external_value(PARAM_FLOAT, 'Student grade'),
305                                         'locked' => new external_value(PARAM_BOOL, 'Is the student\'s grade locked?'),
306                                         'hidden' => new external_value(PARAM_BOOL, 'Is the student\'s grade hidden?'),
307                                         'feedback' => new external_value(PARAM_RAW, 'Feedback from the grader'),
308                                         'feedbackformat' => new external_value(PARAM_INT, 'The feedback format'),
309                                         'usermodified' => new external_value(PARAM_INT, 'The ID of the last user to modify this student grade'),
310                                         'str_grade' => new external_value(PARAM_RAW, 'A string representation of the grade'),
311                                         'str_feedback' => new external_value(PARAM_TEXT, 'A string representation of the feedback from the grader'),
312                                     )
313                                 )
314                             ),
315                         )
316                     ), 'An array of outcomes associated with the grade items', VALUE_OPTIONAL
317                 )
318             )
319         );
321     }
323     /**
324      * Returns description of method parameters
325      *
326      * @return external_function_parameters
327      * @since Moodle 2.6
328      */
329     public static function update_grades_parameters() {
330         return new external_function_parameters(
331             array(
332                 'source' => new external_value(PARAM_TEXT, 'The source of the grade update'),
333                 'courseid' => new external_value(PARAM_INT, 'id of course'),
334                 'component' => new external_value(PARAM_COMPONENT, 'A component, for example mod_forum or mod_quiz'),
335                 'activityid' => new external_value(PARAM_INT, 'The activity ID'),
336                 'itemnumber' => new external_value(PARAM_INT, 'grade item ID number for modules that have multiple grades. Typically this is 0.'),
337                 'grades' => new external_multiple_structure(
338                     new external_single_structure(
339                         array(
340                             'studentid' => new external_value(PARAM_INT, 'Student ID'),
341                             'grade' => new external_value(PARAM_FLOAT, 'Student grade'),
342                             'str_feedback' => new external_value(PARAM_TEXT, 'A string representation of the feedback from the grader', VALUE_OPTIONAL),
343                         )
344                 ), 'Any student grades to alter', VALUE_OPTIONAL),
345                 'itemdetails' => new external_single_structure(
346                     array(
347                         'itemname' => new external_value(PARAM_ALPHANUMEXT, 'The grade item name', VALUE_OPTIONAL),
348                         'idnumber' => new external_value(PARAM_INT, 'Arbitrary ID provided by the module responsible for the grade item', VALUE_OPTIONAL),
349                         'gradetype' => new external_value(PARAM_INT, 'The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)', VALUE_OPTIONAL),
350                         'grademax' => new external_value(PARAM_FLOAT, 'Maximum grade allowed', VALUE_OPTIONAL),
351                         'grademin' => new external_value(PARAM_FLOAT, 'Minimum grade allowed', VALUE_OPTIONAL),
352                         'scaleid' => new external_value(PARAM_INT, 'The ID of the custom scale being is used', VALUE_OPTIONAL),
353                         'multfactor' => new external_value(PARAM_FLOAT, 'Multiply all grades by this number', VALUE_OPTIONAL),
354                         'plusfactor' => new external_value(PARAM_FLOAT, 'Add this to all grades', VALUE_OPTIONAL),
355                         'deleted' => new external_value(PARAM_BOOL, 'True if the grade item should be deleted', VALUE_OPTIONAL),
356                         'hidden' => new external_value(PARAM_BOOL, 'True if the grade item is hidden', VALUE_OPTIONAL),
357                     ), 'Any grade item settings to alter', VALUE_OPTIONAL
358                 )
359             )
360         );
361     }
363     /**
364      * Update a grade item and, optionally, student grades
365      *
366      * @param array $grade array of grade information
367      * @since Moodle 2.6
368      */
369     public static function update_grades($source, $courseid, $component, $activityid, $itemnumber, $grades = array(), $itemdetails = array()) {
370         global $CFG;
372         require_once("$CFG->libdir/gradelib.php");
374         $params = self::validate_parameters(
375             self::update_grades_parameters(),
376             array(
377                 'source' => $source,
378                 'courseid' => $courseid,
379                 'component' => $component,
380                 'activityid' => $activityid,
381                 'itemnumber' => $itemnumber,
382                 'grades' => $grades,
383                 'itemdetails' => $itemdetails
384             )
385         );
387         list($itemtype, $itemmodule) = normalize_component($params['component']);
389         if (! $cm = get_coursemodule_from_id($itemmodule, $activityid)) {
390             throw new moodle_exception('invalidcoursemodule');
391         }
392         $iteminstance = $cm->instance;
394         $coursecontext = context_course::instance($params['courseid']);
396         try {
397             self::validate_context($coursecontext);
398         } catch (Exception $e) {
399             $exceptionparam = new stdClass();
400             $exceptionparam->message = $e->getMessage();
401             $exceptionparam->courseid = $params['courseid'];
402             throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam);
403         }
404         
405         $hidinggrades = false;
406         $editinggradeitem = false;
407         $editinggrades = false;
409         $gradestructure = array();
410         foreach ($grades as $grade) {
411             $editinggrades = true;
412             $gradestructure[ $grade['studentid'] ] = array('userid' => $grade['studentid'], 'rawgrade' => $grade['grade']);
413         }
414         if (!empty($params['itemdetails'])) {
415             if (isset($params['itemdetails']['hidden'])) {
416                 $hidinggrades = true;
417             } else {
418                 $editinggradeitem = true;
419             }
420         }
422         if ($editinggradeitem && !has_capability('moodle/grade:manage', $coursecontext)) {
423             throw new moodle_exception('nopermissiontoviewgrades', 'error', '', null, 'moodle/grade:manage required to edit grade information');
424         }
425         if ($hidinggrades && !has_capability('moodle/grade:hide', $coursecontext) && !has_capability('moodle/grade:hide', $coursecontext)) {
426             throw new moodle_exception('nopermissiontoviewgrades', 'error', '', null, 'moodle/grade:hide required to hide grade items');
427         }
428         if ($editinggrades && !has_capability('moodle/grade:edit', $coursecontext)) {
429             throw new moodle_exception('nopermissiontoviewgrades', 'error', '', null, 'moodle/grade:edit required to edit grades');
430         }
432         return grade_update($params['source'], $params['courseid'], $itemtype, $itemmodule, $iteminstance, $itemnumber, $gradestructure, $params['itemdetails']);
433     }
435     /**
436      * Returns description of method result value
437      *
438      * @return external_description
439      * @since Moodle 2.6
440      */
441     public static function update_grades_returns() {
442         return new external_single_structure(
443             array (
444                 'result' => new external_value(PARAM_INT, 'A value like ' . GRADE_UPDATE_OK . ' => OK, ' . GRADE_UPDATE_FAILED . ' => FAILED  as defined in lib/grade/constants.php')
445             )
446         );
447     }