diff --git a/course/externallib.php b/course/externallib.php index f8b2fa4a703..6c10da1a5f9 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -140,6 +140,7 @@ class core_course_external extends external_api { //common info (for people being able to see the module or availability dates) $module['id'] = $cm->id; $module['name'] = format_string($cm->name, true); + $module['instance'] = $cm->instance; $module['modname'] = $cm->modname; $module['modplural'] = $cm->modplural; $module['modicon'] = $cm->get_icon_url()->out(false); diff --git a/lib/classes/grades_external.php b/lib/classes/grades_external.php new file mode 100644 index 00000000000..5fcee4094cd --- /dev/null +++ b/lib/classes/grades_external.php @@ -0,0 +1,511 @@ +. + +/** + * Core grades external functions + * + * @package core_grades + * @copyright 2012 Andrew Davis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.7 + */ + +defined('MOODLE_INTERNAL') || die; + +require_once("$CFG->libdir/externallib.php"); +require_once("$CFG->libdir/gradelib.php"); + +/** + * core grades functions + */ +class core_grades_external extends external_api { + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.7 + */ + public static function get_grades_parameters() { + return new external_function_parameters( + array( + 'courseid' => new external_value(PARAM_INT, 'id of course'), + 'component' => new external_value( + PARAM_COMPONENT, 'A component, for example mod_forum or mod_quiz', VALUE_DEFAULT, ''), + 'activityid' => new external_value(PARAM_INT, 'The activity ID', VALUE_DEFAULT, null), + 'userids' => new external_multiple_structure( + new external_value(PARAM_INT, 'user ID'), + 'An array of user IDs, leave empty to just retrieve grade item information', VALUE_DEFAULT, array() + ) + ) + ); + } + + /** + * Retrieve grade items and, optionally, student grades + * + * @param int $courseid Course id + * @param string $component Component name + * @param int $activityid Activity id + * @param array $userids Array of user ids + * @return array Array of grades + * @since Moodle 2.7 + */ + public static function get_grades($courseid, $component = null, $activityid = null, $userids = array()) { + global $CFG, $USER, $DB; + + $params = self::validate_parameters(self::get_grades_parameters(), + array('courseid' => $courseid, 'component' => $component, 'activityid' => $activityid, 'userids' => $userids)); + + $coursecontext = context_course::instance($params['courseid']); + + try { + self::validate_context($coursecontext); + } catch (Exception $e) { + $exceptionparam = new stdClass(); + $exceptionparam->message = $e->getMessage(); + $exceptionparam->courseid = $params['courseid']; + throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); + } + + $course = $DB->get_record('course', array('id' => $params['courseid']), '*', MUST_EXIST); + + $access = false; + if (has_capability('moodle/grade:viewall', $coursecontext)) { + // Can view all user's grades in this course. + $access = true; + + } else if ($course->showgrades && count($params['userids']) == 1) { + // Course showgrades == students/parents can access grades. + + if ($params['userids'][0] == $USER->id and has_capability('moodle/grade:view', $coursecontext)) { + // Student can view their own grades in this course. + $access = true; + + } else if (has_capability('moodle/grade:viewall', context_user::instance($params['userids'][0]))) { + // User can view the grades of this user. Parent most probably. + $access = true; + } + } + + if (!$access) { + throw new moodle_exception('nopermissiontoviewgrades', 'error'); + } + + $itemtype = null; + $itemmodule = null; + if (!empty($params['component'])) { + list($itemtype, $itemmodule) = normalize_component($params['component']); + } + + $cm = null; + if (!empty($itemmodule) && !empty($activityid)) { + if (! $cm = get_coursemodule_from_id($itemmodule, $activityid)) { + throw new moodle_exception('invalidcoursemodule'); + } + } + + $cminstanceid = null; + if (!empty($cm)) { + $cminstanceid = $cm->instance; + } + $grades = grade_get_grades($params['courseid'], $itemtype, $itemmodule, $cminstanceid, $params['userids']); + + $acitivityinstances = null; + if (empty($cm)) { + // If we're dealing with multiple activites load all the module info. + $modinfo = get_fast_modinfo($params['courseid']); + $acitivityinstances = $modinfo->get_instances(); + } + + foreach ($grades->items as $gradeitem) { + if (!empty($cm)) { + // If they only requested one activity we will already have the cm. + $modulecm = $cm; + } else if (!empty($gradeitem->itemmodule)) { + $modulecm = $acitivityinstances[$gradeitem->itemmodule][$gradeitem->iteminstance]; + } else { + // Course grade item. + continue; + } + + // Make student feedback ready for output. + foreach ($gradeitem->grades as $studentgrade) { + if (!empty($studentgrade->feedback)) { + list($studentgrade->feedback, $categoryinfo->feedbackformat) = + external_format_text($studentgrade->feedback, $studentgrade->feedbackformat, + $modulecm->id, $params['component'], 'feedback', null); + } + } + } + + // Convert from objects to arrays so all web service clients are supported. + // While we're doing that we also remove grades the current user can't see due to hiding. + $gradesarray = array(); + $canviewhidden = has_capability('moodle/grade:viewhidden', context_course::instance($params['courseid'])); + + $gradesarray['items'] = array(); + foreach ($grades->items as $gradeitem) { + // Switch the stdClass instance for a grade item instance so we can call is_hidden() and use the ID. + $gradeiteminstance = self::get_grade_item( + $course->id, $gradeitem->itemtype, $gradeitem->itemmodule, $gradeitem->iteminstance, 0); + if (!$canviewhidden && $gradeiteminstance->is_hidden()) { + continue; + } + $gradeitemarray = (array)$gradeitem; + $gradeitemarray['grades'] = array(); + + if (!empty($gradeitem->grades)) { + foreach ($gradeitem->grades as $studentid => $studentgrade) { + $gradegradeinstance = grade_grade::fetch( + array( + 'userid' => $studentid, + 'itemid' => $gradeiteminstance->id + ) + ); + if (!$canviewhidden && $gradegradeinstance->is_hidden()) { + continue; + } + $gradeitemarray['grades'][$studentid] = (array)$studentgrade; + // Add the student ID as some WS clients can't access the array key. + $gradeitemarray['grades'][$studentid]['userid'] = $studentid; + } + } + + // If they requested grades for multiple activities load the cm object now. + $modulecm = $cm; + if (empty($modulecm) && !empty($gradeiteminstance->itemmodule)) { + $modulecm = $acitivityinstances[$gradeiteminstance->itemmodule][$gradeiteminstance->iteminstance]; + } + if ($gradeiteminstance->itemtype == 'course') { + $gradesarray['items']['course'] = $gradeitemarray; + $gradesarray['items']['course']['activityid'] = 'course'; + } else { + $gradesarray['items'][$modulecm->id] = $gradeitemarray; + // Add the activity ID as some WS clients can't access the array key. + $gradesarray['items'][$modulecm->id]['activityid'] = $modulecm->id; + } + } + + $gradesarray['outcomes'] = array(); + foreach ($grades->outcomes as $outcome) { + $modulecm = $cm; + if (empty($modulecm)) { + $modulecm = $acitivityinstances[$outcome->itemmodule][$outcome->iteminstance]; + } + $gradesarray['outcomes'][$modulecm->id] = (array)$outcome; + $gradesarray['outcomes'][$modulecm->id]['activityid'] = $modulecm->id; + + $gradesarray['outcomes'][$modulecm->id]['grades'] = array(); + if (!empty($outcome->grades)) { + foreach ($outcome->grades as $studentid => $studentgrade) { + if (!$canviewhidden) { + // Need to load the grade_grade object to check visibility. + $gradeiteminstance = self::get_grade_item( + $course->id, $outcome->itemtype, $outcome->itemmodule, $outcome->iteminstance, $outcome->itemnumber); + $gradegradeinstance = grade_grade::fetch( + array( + 'userid' => $studentid, + 'itemid' => $gradeiteminstance->id + ) + ); + // The grade grade may be legitimately missing if the student has no grade. + if (!empty($gradegradeinstance ) && $gradegradeinstance->is_hidden()) { + continue; + } + } + $gradesarray['outcomes'][$modulecm->id]['grades'][$studentid] = (array)$studentgrade; + + // Add the student ID into the grade structure as some WS clients can't access the key. + $gradesarray['outcomes'][$modulecm->id]['grades'][$studentid]['userid'] = $studentid; + } + } + } + + return $gradesarray; + } + + /** + * Get a grade item + * @param int $courseid Course id + * @param string $itemtype Item type + * @param string $itemmodule Item module + * @param int $iteminstance Item instance + * @param int $itemnumber Item number + * @return grade_item A grade_item instance + */ + private static function get_grade_item($courseid, $itemtype, $itemmodule = null, $iteminstance = null, $itemnumber = null) { + $gradeiteminstance = null; + if ($itemtype == 'course') { + $gradeiteminstance = grade_item::fetch(array('courseid' => $courseid, 'itemtype' => $itemtype)); + } else { + $gradeiteminstance = grade_item::fetch( + array('courseid' => $courseid, 'itemtype' => $itemtype, + 'itemmodule' => $itemmodule, 'iteminstance' => $iteminstance, 'itemnumber' => $itemnumber)); + } + return $gradeiteminstance; + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.7 + */ + public static function get_grades_returns() { + return new external_single_structure( + array( + 'items' => new external_multiple_structure( + new external_single_structure( + array( + 'activityid' => new external_value( + PARAM_ALPHANUM, 'The ID of the activity or "course" for the course grade item'), + 'itemnumber' => new external_value(PARAM_INT, 'Will be 0 unless the module has multiple grades'), + 'scaleid' => new external_value(PARAM_INT, 'The ID of the custom scale or 0'), + 'name' => new external_value(PARAM_RAW, 'The module name'), + 'grademin' => new external_value(PARAM_FLOAT, 'Minimum grade'), + 'grademax' => new external_value(PARAM_FLOAT, 'Maximum grade'), + 'gradepass' => new external_value(PARAM_FLOAT, 'The passing grade threshold'), + 'locked' => new external_value(PARAM_BOOL, 'Is the grade item locked?'), + 'hidden' => new external_value(PARAM_BOOL, 'Is the grade item hidden?'), + 'grades' => new external_multiple_structure( + new external_single_structure( + array( + 'userid' => new external_value( + PARAM_INT, 'Student ID'), + 'grade' => new external_value( + PARAM_FLOAT, 'Student grade'), + 'locked' => new external_value( + PARAM_BOOL, 'Is the student\'s grade locked?'), + 'hidden' => new external_value( + PARAM_BOOL, 'Is the student\'s grade hidden?'), + 'overridden' => new external_value( + PARAM_BOOL, 'Is the student\'s grade overridden?'), + 'feedback' => new external_value( + PARAM_RAW, 'Feedback from the grader'), + 'feedbackformat' => new external_value( + PARAM_INT, 'The format of the feedback'), + 'usermodified' => new external_value( + PARAM_INT, 'The ID of the last user to modify this student grade'), + 'datesubmitted' => new external_value( + PARAM_INT, 'A timestamp indicating when the student submitted the activity'), + 'dategraded' => new external_value( + PARAM_INT, 'A timestamp indicating when the assignment was grades'), + 'str_grade' => new external_value( + PARAM_RAW, 'A string representation of the grade'), + 'str_long_grade' => new external_value( + PARAM_RAW, 'A nicely formatted string representation of the grade'), + 'str_feedback' => new external_value( + PARAM_TEXT, 'A string representation of the feedback from the grader'), + ) + ) + ), + ) + ) + ), + 'outcomes' => new external_multiple_structure( + new external_single_structure( + array( + 'activityid' => new external_value( + PARAM_ALPHANUM, 'The ID of the activity or "course" for the course grade item'), + 'itemnumber' => new external_value(PARAM_INT, 'Will be 0 unless the module has multiple grades'), + 'scaleid' => new external_value(PARAM_INT, 'The ID of the custom scale or 0'), + 'name' => new external_value(PARAM_RAW, 'The module name'), + 'locked' => new external_value(PARAM_BOOL, 'Is the grade item locked?'), + 'hidden' => new external_value(PARAM_BOOL, 'Is the grade item hidden?'), + 'grades' => new external_multiple_structure( + new external_single_structure( + array( + 'userid' => new external_value( + PARAM_INT, 'Student ID'), + 'grade' => new external_value( + PARAM_FLOAT, 'Student grade'), + 'locked' => new external_value( + PARAM_BOOL, 'Is the student\'s grade locked?'), + 'hidden' => new external_value( + PARAM_BOOL, 'Is the student\'s grade hidden?'), + 'feedback' => new external_value( + PARAM_RAW, 'Feedback from the grader'), + 'feedbackformat' => new external_value( + PARAM_INT, 'The feedback format'), + 'usermodified' => new external_value( + PARAM_INT, 'The ID of the last user to modify this student grade'), + 'str_grade' => new external_value( + PARAM_RAW, 'A string representation of the grade'), + 'str_feedback' => new external_value( + PARAM_TEXT, 'A string representation of the feedback from the grader'), + ) + ) + ), + ) + ), 'An array of outcomes associated with the grade items', VALUE_OPTIONAL + ) + ) + ); + + } + + /** + * Returns description of method parameters + * + * @return external_function_parameters + * @since Moodle 2.7 + */ + public static function update_grades_parameters() { + return new external_function_parameters( + array( + 'source' => new external_value(PARAM_TEXT, 'The source of the grade update'), + 'courseid' => new external_value(PARAM_INT, 'id of course'), + 'component' => new external_value(PARAM_COMPONENT, 'A component, for example mod_forum or mod_quiz'), + 'activityid' => new external_value(PARAM_INT, 'The activity ID'), + 'itemnumber' => new external_value( + PARAM_INT, 'grade item ID number for modules that have multiple grades. Typically this is 0.'), + 'grades' => new external_multiple_structure( + new external_single_structure( + array( + 'studentid' => new external_value(PARAM_INT, 'Student ID'), + 'grade' => new external_value(PARAM_FLOAT, 'Student grade'), + 'str_feedback' => new external_value( + PARAM_TEXT, 'A string representation of the feedback from the grader', VALUE_OPTIONAL), + ) + ), 'Any student grades to alter', VALUE_OPTIONAL), + 'itemdetails' => new external_single_structure( + array( + 'itemname' => new external_value( + PARAM_ALPHANUMEXT, 'The grade item name', VALUE_OPTIONAL), + 'idnumber' => new external_value( + PARAM_INT, 'Arbitrary ID provided by the module responsible for the grade item', VALUE_OPTIONAL), + 'gradetype' => new external_value( + PARAM_INT, 'The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)', VALUE_OPTIONAL), + 'grademax' => new external_value( + PARAM_FLOAT, 'Maximum grade allowed', VALUE_OPTIONAL), + 'grademin' => new external_value( + PARAM_FLOAT, 'Minimum grade allowed', VALUE_OPTIONAL), + 'scaleid' => new external_value( + PARAM_INT, 'The ID of the custom scale being is used', VALUE_OPTIONAL), + 'multfactor' => new external_value( + PARAM_FLOAT, 'Multiply all grades by this number', VALUE_OPTIONAL), + 'plusfactor' => new external_value( + PARAM_FLOAT, 'Add this to all grades', VALUE_OPTIONAL), + 'deleted' => new external_value( + PARAM_BOOL, 'True if the grade item should be deleted', VALUE_OPTIONAL), + 'hidden' => new external_value( + PARAM_BOOL, 'True if the grade item is hidden', VALUE_OPTIONAL), + ), 'Any grade item settings to alter', VALUE_OPTIONAL + ) + ) + ); + } + + /** + * Update a grade item and, optionally, student grades + * + * @param string $source The source of the grade update + * @param int $courseid The course id + * @param string $component Component name + * @param int $activityid The activity id + * @param int $itemnumber The item number + * @param array $grades Array of grades + * @param array $itemdetails Array of item details + * @return int A status flag + * @since Moodle 2.7 + */ + public static function update_grades($source, $courseid, $component, $activityid, + $itemnumber, $grades = array(), $itemdetails = array()) { + global $CFG; + + $params = self::validate_parameters( + self::update_grades_parameters(), + array( + 'source' => $source, + 'courseid' => $courseid, + 'component' => $component, + 'activityid' => $activityid, + 'itemnumber' => $itemnumber, + 'grades' => $grades, + 'itemdetails' => $itemdetails + ) + ); + + list($itemtype, $itemmodule) = normalize_component($params['component']); + + if (! $cm = get_coursemodule_from_id($itemmodule, $activityid)) { + throw new moodle_exception('invalidcoursemodule'); + } + $iteminstance = $cm->instance; + + $coursecontext = context_course::instance($params['courseid']); + + try { + self::validate_context($coursecontext); + } catch (Exception $e) { + $exceptionparam = new stdClass(); + $exceptionparam->message = $e->getMessage(); + $exceptionparam->courseid = $params['courseid']; + throw new moodle_exception('errorcoursecontextnotvalid' , 'webservice', '', $exceptionparam); + } + + $hidinggrades = false; + $editinggradeitem = false; + $editinggrades = false; + + $gradestructure = array(); + foreach ($grades as $grade) { + $editinggrades = true; + $gradestructure[ $grade['studentid'] ] = array('userid' => $grade['studentid'], 'rawgrade' => $grade['grade']); + } + if (!empty($params['itemdetails'])) { + if (isset($params['itemdetails']['hidden'])) { + $hidinggrades = true; + } else { + $editinggradeitem = true; + } + } + + if ($editinggradeitem && !has_capability('moodle/grade:manage', $coursecontext)) { + throw new moodle_exception('nopermissiontoviewgrades', 'error', '', null, + 'moodle/grade:manage required to edit grade information'); + } + if ($hidinggrades && !has_capability('moodle/grade:hide', $coursecontext) && + !has_capability('moodle/grade:hide', $coursecontext)) { + throw new moodle_exception('nopermissiontoviewgrades', 'error', '', null, + 'moodle/grade:hide required to hide grade items'); + } + if ($editinggrades && !has_capability('moodle/grade:edit', $coursecontext)) { + throw new moodle_exception('nopermissiontoviewgrades', 'error', '', null, + 'moodle/grade:edit required to edit grades'); + } + + return grade_update($params['source'], $params['courseid'], $itemtype, + $itemmodule, $iteminstance, $itemnumber, $gradestructure, $params['itemdetails']); + } + + /** + * Returns description of method result value + * + * @return external_description + * @since Moodle 2.7 + */ + public static function update_grades_returns() { + return new external_single_structure( + array ( + 'result' => new external_value( + PARAM_INT, + 'A value like ' . GRADE_UPDATE_OK . ' => OK, ' . GRADE_UPDATE_FAILED . ' => FAILED + as defined in lib/grade/constants.php') + ) + ); + } +} diff --git a/lib/db/services.php b/lib/db/services.php index 7d9f557ac1b..d93c067d552 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -99,6 +99,23 @@ $functions = array( 'type' => 'read', 'capabilities'=> 'moodle/cohort:view', ), + // Grade related functions. + + 'core_grades_get_grades' => array( + 'classname' => 'core_grades_external', + 'methodname' => 'get_grades', + 'description' => 'Returns grade item details and optionally student grades.', + 'type' => 'read', + 'capabilities' => 'moodle/grade:view, moodle/grade:viewall', + ), + + 'core_grades_update_grades' => array( + 'classname' => 'core_grades_external', + 'methodname' => 'update_grades', + 'description' => 'Update a grade item and associated student grades.', + 'type' => 'write', + 'capabilities' => '', + ), // === group related functions === diff --git a/lib/gradelib.php b/lib/gradelib.php index 28bb4a70a1e..3ad3a6a7daa 100644 --- a/lib/gradelib.php +++ b/lib/gradelib.php @@ -326,7 +326,7 @@ function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $item } /** - * Returns grading information for given activity, optionally with user grades + * Returns grading information for one or more activities, optionally with user grades * Manual, course or category items can not be queried. * * @category grade @@ -337,7 +337,7 @@ function grade_update_outcomes($source, $courseid, $itemtype, $itemmodule, $item * @param mixed $userid_or_ids Either a single user ID, an array of user IDs or null. If user ID or IDs are not supplied returns information about grade_item * @return array Array of grade information objects (scaleid, name, grade and locked status, etc.) indexed with itemnumbers */ -function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $userid_or_ids=null) { +function grade_get_grades($courseid, $itemtype = null, $itemmodule = null, $iteminstance = null, $userid_or_ids=null) { global $CFG; $return = new stdClass(); @@ -353,14 +353,28 @@ function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $use } } - if ($grade_items = grade_item::fetch_all(array('itemtype'=>$itemtype, 'itemmodule'=>$itemmodule, 'iteminstance'=>$iteminstance, 'courseid'=>$courseid))) { + $params = array('courseid' => $courseid); + if (!empty($itemtype)) { + $params['itemtype'] = $itemtype; + } + if (!empty($itemmodule)) { + $params['itemmodule'] = $itemmodule; + } + if (!empty($iteminstance)) { + $params['iteminstance'] = $iteminstance; + } + if ($grade_items = grade_item::fetch_all($params)) { foreach ($grade_items as $grade_item) { $decimalpoints = null; if (empty($grade_item->outcomeid)) { // prepare information about grade item $item = new stdClass(); + $item->id = $grade_item->id; $item->itemnumber = $grade_item->itemnumber; + $item->itemtype = $grade_item->itemtype; + $item->itemmodule = $grade_item->itemmodule; + $item->iteminstance = $grade_item->iteminstance; $item->scaleid = $grade_item->scaleid; $item->name = $grade_item->get_name(); $item->grademin = $grade_item->grademin; @@ -459,7 +473,11 @@ function grade_get_grades($courseid, $itemtype, $itemmodule, $iteminstance, $use // outcome info $outcome = new stdClass(); + $outcome->id = $grade_item->id; $outcome->itemnumber = $grade_item->itemnumber; + $outcome->itemtype = $grade_item->itemtype; + $outcome->itemmodule = $grade_item->itemmodule; + $outcome->iteminstance = $grade_item->iteminstance; $outcome->scaleid = $grade_outcome->scaleid; $outcome->name = $grade_outcome->get_name(); $outcome->locked = $grade_item->is_locked(); diff --git a/lib/tests/grades_externallib_test.php b/lib/tests/grades_externallib_test.php new file mode 100644 index 00000000000..866ac32b354 --- /dev/null +++ b/lib/tests/grades_externallib_test.php @@ -0,0 +1,524 @@ +. + +/** + * Unit tests for the grade API at /lib/classes/grades_external.php + * + * @package core_grades + * @category external + * @copyright 2012 Andrew Davis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 2.7 + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; + +require_once($CFG->dirroot . '/webservice/tests/helpers.php'); + +/** + * Grades functions unit tests + * + * @package core_grades + * @category external + * @copyright 2012 Andrew Davis + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class core_grades_external_testcase extends externallib_advanced_testcase { + + /** + * Load initial test information + * + * @param string $assignmentname Assignment name + * @param int $student1rawgrade Student 1 grade + * @param int $student2rawgrade Student 2 grade + * @return array Array of vars with test information + */ + protected function load_test_data($assignmentname, $student1rawgrade, $student2rawgrade) { + global $DB; + + // Adds a course, a teacher, 2 students, an assignment and grades for the students. + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + + $student1 = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id); + + $student2 = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($student2->id, $course->id, $studentrole->id); + + $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); + $teacher = $this->getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id); + + $parent = $this->getDataGenerator()->create_user(); + $this->setUser($parent); + $student1context = context_user::instance($student1->id); + // Creates a new role, gives it the capability and gives $USER that role. + $parentroleid = $this->assignUserCapability('moodle/grade:viewall', $student1context->id); + // Enrol the user in the course using the new role. + $this->getDataGenerator()->enrol_user($parent->id, $course->id, $parentroleid); + + $assignment = $this->getDataGenerator()->create_module('assign', array('name' => $assignmentname, 'course' => $course->id)); + $modcontext = get_coursemodule_from_instance('assign', $assignment->id, $course->id); + $assignment->cmidnumber = $modcontext->id; + + $student1grade = array('userid' => $student1->id, 'rawgrade' => $student1rawgrade); + $student2grade = array('userid' => $student2->id, 'rawgrade' => $student2rawgrade); + $studentgrades = array($student1->id => $student1grade, $student2->id => $student2grade); + assign_grade_item_update($assignment, $studentgrades); + + // Insert a custom grade scale to be used by an outcome. + $gradescale = new grade_scale(); + $gradescale->name = 'unittestscale3'; + $gradescale->courseid = $course->id; + $gradescale->userid = 0; + $gradescale->scale = 'Distinction, Very Good, Good, Pass, Fail'; + $gradescale->description = 'This scale is used to mark standard assignments.'; + $gradescale->insert(); + + // Insert an outcome. + $data = new stdClass(); + $data->courseid = $course->id; + $data->fullname = 'Team work'; + $data->shortname = 'Team work'; + $data->scaleid = $gradescale->id; + $outcome = new grade_outcome($data, false); + $outcome->insert(); + + $outcomegradeitem = new grade_item(); + $outcomegradeitem->itemname = $outcome->shortname; + $outcomegradeitem->itemtype = 'mod'; + $outcomegradeitem->itemmodule = 'assign'; + $outcomegradeitem->iteminstance = $assignment->id; + $outcomegradeitem->outcomeid = $outcome->id; + $outcomegradeitem->cmid = 0; + $outcomegradeitem->courseid = $course->id; + $outcomegradeitem->aggregationcoef = 0; + $outcomegradeitem->itemnumber = 1; // The activity's original grade item will be 0. + $outcomegradeitem->gradetype = GRADE_TYPE_SCALE; + $outcomegradeitem->scaleid = $outcome->scaleid; + $outcomegradeitem->insert(); + + $assignmentgradeitem = grade_item::fetch( + array( + 'itemtype' => 'mod', + 'itemmodule' => 'assign', + 'iteminstance' => $assignment->id, + 'itemnumber' => 0, + 'courseid' => $course->id + ) + ); + $outcomegradeitem->set_parent($assignmentgradeitem->categoryid); + $outcomegradeitem->move_after_sortorder($assignmentgradeitem->sortorder); + + return array($course, $assignment, $student1, $student2, $teacher, $parent); + } + + /** + * Test get_grades() + */ + public function test_get_grades() { + global $CFG; + + $this->resetAfterTest(true); + $CFG->enableoutcomes = 1; + + $assignmentname = 'The assignment'; + $student1rawgrade = 10; + $student2rawgrade = 20; + list($course, $assignment, $student1, $student2, $teacher, $parent) = + $this->load_test_data($assignmentname, $student1rawgrade, $student2rawgrade); + $assigmentcm = get_coursemodule_from_id('assign', $assignment->id, 0, false, MUST_EXIST); + + // Student requesting their own grade for the assignment. + $this->setUser($student1); + $grades = core_grades_external::get_grades( + $course->id, + 'mod_assign', + $assigmentcm->id, + array($student1->id) + ); + $grades = external_api::clean_returnvalue(core_grades_external::get_grades_returns(), $grades); + $this->assertEquals($student1rawgrade, $this->get_activity_student_grade($grades, $assigmentcm->id, $student1->id)); + + // Student requesting all of their grades in a course. + $grades = core_grades_external::get_grades( + $course->id, + null, + null, + array($student1->id) + ); + $grades = external_api::clean_returnvalue(core_grades_external::get_grades_returns(), $grades); + $this->assertTrue(count($grades['items']) == 2); + $this->assertEquals($student1rawgrade, $this->get_activity_student_grade($grades, $assigmentcm->id, $student1->id)); + $this->assertEquals($student1rawgrade, $this->get_activity_student_grade($grades, 'course', $student1->id)); + + $outcome = $this->get_outcome($grades, $assigmentcm->id); + $this->assertEquals($outcome['name'], 'Team work'); + $this->assertEquals(0, $this->get_outcome_student_grade($grades, $assigmentcm->id, $student1->id)); + + // Student requesting another student's grade for the assignment (should fail). + try { + $grades = core_grades_external::get_grades( + $course->id, + 'mod_assign', + $assigmentcm->id, + array($student2->id) + ); + $this->fail('moodle_exception expected'); + } catch (moodle_exception $ex) { + $this->assertTrue(true); + } + + // Parent requesting their child's grade for the assignment. + $this->setUser($parent); + $grades = core_grades_external::get_grades( + $course->id, + 'mod_assign', + $assigmentcm->id, + array($student1->id) + ); + $grades = external_api::clean_returnvalue(core_grades_external::get_grades_returns(), $grades); + $this->assertEquals($student1rawgrade, $this->get_activity_student_grade($grades, $assigmentcm->id, $student1->id)); + + // Parent requesting another student's grade for the assignment(should fail). + try { + $grades = core_grades_external::get_grades( + $course->id, + 'mod_assign', + $assigmentcm->id, + array($student2->id) + ); + $this->fail('moodle_exception expected'); + } catch (moodle_exception $ex) { + $this->assertTrue(true); + } + + // Student requesting all other student grades for the assignment (should fail). + try { + $grades = core_grades_external::get_grades( + $course->id, + 'mod_assign', + $assigmentcm->id, + array($student1->id, $student2->id) + ); + $this->fail('moodle_exception expected'); + } catch (moodle_exception $ex) { + $this->assertTrue(true); + } + + // Student requesting only grade item information (should fail). + try { + $grades = core_grades_external::get_grades( + $course->id, + 'mod_assign', + $assigmentcm->id, + array() + ); + $this->fail('moodle_exception expected'); + } catch (moodle_exception $ex) { + $this->assertTrue(true); + } + + // Teacher requesting student grades for a course. + $this->setUser($teacher); + $grades = core_grades_external::get_grades( + $course->id, + 'mod_assign', + $assigmentcm->id, + array($student1->id, $student2->id) + ); + $grades = external_api::clean_returnvalue(core_grades_external::get_grades_returns(), $grades); + $this->assertEquals($student1rawgrade, $this->get_activity_student_grade($grades, $assigmentcm->id, $student1->id)); + $this->assertEquals($student2rawgrade, $this->get_activity_student_grade($grades, $assigmentcm->id, $student2->id)); + + // Teacher requesting grade item information. + $grades = core_grades_external::get_grades( + $course->id, + 'mod_assign', + $assigmentcm->id + ); + $grades = external_api::clean_returnvalue(core_grades_external::get_grades_returns(), $grades); + $activity = $this->get_activity($grades, $assigmentcm->id); + $this->assertEquals($activity['name'], $assignmentname); + $this->assertEquals(count($activity['grades']), 0); + + // Teacher requesting all grade items in a course. + $grades = core_grades_external::get_grades( + $course->id + ); + $grades = external_api::clean_returnvalue(core_grades_external::get_grades_returns(), $grades); + $this->assertTrue(count($grades['items']) == 2); + + $activity = $this->get_activity($grades, $assigmentcm->id); + $this->assertEquals($activity['name'], $assignmentname); + $this->assertEquals(count($activity['grades']), 0); + + $outcome = $this->get_outcome($grades, $assigmentcm->id); + $this->assertEquals($outcome['name'], 'Team work'); + + // Hide a grade item then have student request it. + $result = core_grades_external::update_grades( + 'test', + $course->id, + 'mod_assign', + $assigmentcm->id, + 0, + array(), + array('hidden' => 1) + ); + $this->assertTrue($result == GRADE_UPDATE_OK); + + // Check it's definitely hidden. + $grades = grade_get_grades($course->id, 'mod', 'assign', $assignment->id); + $this->assertEquals($grades->items[0]->hidden, 1); + + // Student should now not be able to see it. + $this->setUser($student1); + $grades = core_grades_external::get_grades( + $course->id, + 'mod_assign', + $assigmentcm->id, + array($student1->id) + ); + $grades = external_api::clean_returnvalue(core_grades_external::get_grades_returns(), $grades); + $this->assertEquals(null, $this->get_activity($grades, $assigmentcm->id)); + + // Teacher should still be able to see the hidden grades. + $this->setUser($teacher); + $grades = core_grades_external::get_grades( + $course->id, + 'mod_assign', + $assigmentcm->id, + array($student1->id) + ); + $grades = external_api::clean_returnvalue(core_grades_external::get_grades_returns(), $grades); + $this->assertEquals($student1rawgrade, $this->get_activity_student_grade($grades, $assigmentcm->id, $student1->id)); + } + + /** + * Get an activity + * + * @param array $grades Array of grades + * @param int $cmid Activity course module id + * @return strdClass Activity object + */ + private function get_activity($grades, $cmid) { + foreach ($grades['items'] as $item) { + if ($item['activityid'] == $cmid) { + return $item; + } + } + return null; + } + + /** + * Get a grade for an activity + * + * @param array $grades Array of grades + * @param int $cmid Activity course module id + * @param int $studentid Student it + * @return stdClass Activity Object + */ + private function get_activity_student_grade($grades, $cmid, $studentid) { + $item = $this->get_activity($grades, $cmid); + foreach ($item['grades'] as $grade) { + if ($grade['userid'] == $studentid) { + return $grade['grade']; + } + } + return null; + } + + /** + * Get an ouctome + * + * @param array $grades Array of grades + * @param int $cmid Activity course module id + * @return stdClass Outcome object + */ + private function get_outcome($grades, $cmid) { + foreach ($grades['outcomes'] as $outcome) { + if ($outcome['activityid'] == $cmid) { + return $outcome; + } + } + return null; + } + + /** + * Get a grade from an outcome + * + * @param array $grades Array of grades + * @param int $cmid Activity course module id + * @param int $studentid Student id + * @return stdClass Outcome object + */ + private function get_outcome_student_grade($grades, $cmid, $studentid) { + $outcome = $this->get_outcome($grades, $cmid); + foreach ($outcome['grades'] as $grade) { + if ($grade['userid'] == $studentid) { + return $grade['grade']; + } + } + return null; + } + + /** + * Test update_grades() + */ + public function test_update_grades() { + global $DB; + + $this->resetAfterTest(true); + + $assignmentname = 'The assignment'; + $student1rawgrade = 10; + $student2rawgrade = 20; + list($course, $assignment, $student1, $student2, $teacher, $parent) = + $this->load_test_data($assignmentname, $student1rawgrade, $student2rawgrade); + $assigmentcm = get_coursemodule_from_id('assign', $assignment->id, 0, false, MUST_EXIST); + + $this->setUser($teacher); + + // Teacher updating grade item information. + $changedmax = 93; + $result = core_grades_external::update_grades( + 'test', + $course->id, + 'mod_assign', + $assigmentcm->id, + 0, + array(), + array('grademax' => $changedmax) + ); + $this->assertTrue($result == GRADE_UPDATE_OK); + $grades = grade_get_grades($course->id, 'mod', 'assign', $assignment->id); + $this->assertTrue($grades->items[0]->grademax == $changedmax); + + // Teacher updating 1 student grade. + $student1grade = 23; + $result = core_grades_external::update_grades( + 'test', + $course->id, + 'mod_assign', + $assigmentcm->id, + 0, + array(array('studentid' => $student1->id, 'grade' => $student1grade)) + ); + $this->assertTrue($result == GRADE_UPDATE_OK); + $grades = grade_get_grades($course->id, 'mod', 'assign', $assignment->id, array($student1->id)); + $this->assertTrue($grades->items[0]->grades[$student1->id]->grade == $student1grade); + + // Teacher updating multiple student grades. + $student1grade = 11; + $student2grade = 13; + $result = core_grades_external::update_grades( + 'test', + $course->id, + 'mod_assign', + $assigmentcm->id, + 0, + array( + array('studentid' => $student1->id, 'grade' => $student1grade), + array('studentid' => $student2->id, 'grade' => $student2grade) + ) + ); + $this->assertTrue($result == GRADE_UPDATE_OK); + $grades = grade_get_grades($course->id, 'mod', 'assign', $assignment->id, array($student1->id, $student2->id)); + $this->assertTrue($grades->items[0]->grades[$student1->id]->grade == $student1grade); + $this->assertTrue($grades->items[0]->grades[$student2->id]->grade == $student2grade); + + // Student attempting to update their own grade (should fail). + $this->setUser($student1); + try { + $student1grade = 17; + $result = core_grades_external::update_grades( + 'test', + $course->id, + 'mod_assign', + $assigmentcm->id, + 0, + array( array('studentid' => $student1->id, 'grade' => $student1grade)) + ); + $this->fail('moodle_exception expected'); + } catch (moodle_exception $ex) { + $this->assertTrue(true); + } + + // Parent attempting to update their child's grade (should fail). + $this->setUser($parent); + try { + $student1grade = 13; + $result = core_grades_external::update_grades( + 'test', + $course->id, + 'mod_assign', + $assigmentcm->id, + 0, + array( array('studentid' => $student1->id, 'grade' => $student1grade)) + ); + $this->fail('moodle_exception expected'); + } catch (moodle_exception $ex) { + $this->assertTrue(true); + } + + // Student trying to hide a grade item (should fail). + $this->setUser($student1); + try { + $result = core_grades_external::update_grades( + 'test', + $course->id, + 'mod_assign', + $assigmentcm->id, + 0, + array(), + array('hidden' => 1) + ); + $this->fail('moodle_exception expected'); + } catch (moodle_exception $ex) { + $this->assertTrue(true); + } + + // Give the student role 'moodle/grade:hide' and they should now be able to hide the grade item. + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $coursecontext = context_course::instance($course->id); + assign_capability('moodle/grade:hide', CAP_ALLOW, $studentrole->id, $coursecontext->id); + accesslib_clear_all_caches_for_unit_testing(); + + // Check the activity isn't already hidden. + $grades = grade_get_grades($course->id, 'mod', 'assign', $assignment->id); + $this->assertTrue($grades->items[0]->hidden == 0); + + $result = core_grades_external::update_grades( + 'test', + $course->id, + 'mod_assign', + $assigmentcm->id, + 0, + array(), + array('hidden' => 1) + ); + $this->assertTrue($result == GRADE_UPDATE_OK); + $grades = grade_get_grades($course->id, 'mod', 'assign', $assignment->id); + $this->assertTrue($grades->items[0]->hidden == 1); + } + +}