From 29932b211287985c8e3235e52fc78782528309a2 Mon Sep 17 00:00:00 2001 From: Paul Charsley Date: Thu, 19 Dec 2013 09:39:22 +1300 Subject: [PATCH] MDL-42425 modify mod_assign_save_grade to process advanced grading data --- mod/assign/db/services.php | 8 + mod/assign/externallib.php | 201 ++++++++++++++++- mod/assign/tests/externallib_test.php | 299 +++++++++++++++++++++++++- mod/assign/upgrade.txt | 5 + mod/assign/version.php | 2 +- 5 files changed, 505 insertions(+), 10 deletions(-) diff --git a/mod/assign/db/services.php b/mod/assign/db/services.php index cc7056feb3f..3061e78812b 100644 --- a/mod/assign/db/services.php +++ b/mod/assign/db/services.php @@ -122,6 +122,14 @@ $functions = array( 'type' => 'write' ), + 'mod_assign_save_grades' => array( + 'classname' => 'mod_assign_external', + 'methodname' => 'save_grades', + 'classpath' => 'mod/assign/externallib.php', + 'description' => 'Save multiple grade updates for an assignment.', + 'type' => 'write' + ), + 'mod_assign_save_user_extensions' => array( 'classname' => 'mod_assign_external', 'methodname' => 'save_user_extensions', diff --git a/mod/assign/externallib.php b/mod/assign/externallib.php index cf582ce86d8..4b795b89997 100644 --- a/mod/assign/externallib.php +++ b/mod/assign/externallib.php @@ -1644,6 +1644,7 @@ class mod_assign_external extends external_api { public static function save_grade_parameters() { global $CFG; require_once("$CFG->dirroot/mod/assign/locallib.php"); + require_once("$CFG->dirroot/grade/grading/lib.php"); $instance = new assign(null, null, null); $pluginfeedbackparams = array(); @@ -1654,20 +1655,41 @@ class mod_assign_external extends external_api { } } + $advancedgradingdata = array(); + $methods = array_keys(grading_manager::available_methods(false)); + foreach ($methods as $method) { + require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php'); + $details = call_user_func('gradingform_'.$method.'_controller::get_external_instance_filling_details'); + if (!empty($details)) { + $items = array(); + foreach ($details as $key => $value) { + $value->required = VALUE_OPTIONAL; + unset($value->content->keys['id']); + $items[$key] = new external_multiple_structure (new external_single_structure( + array( + 'criterionid' => new external_value(PARAM_INT, 'criterion id'), + 'fillings' => $value + ) + )); + } + $advancedgradingdata[$method] = new external_single_structure($items, 'items', VALUE_OPTIONAL); + } + } + return new external_function_parameters( array( 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'), 'userid' => new external_value(PARAM_INT, 'The student id to operate on'), - 'grade' => new external_value(PARAM_FLOAT, 'The new grade for this user'), + 'grade' => new external_value(PARAM_FLOAT, 'The new grade for this user. Ignored if advanced grading used'), 'attemptnumber' => new external_value(PARAM_INT, 'The attempt number (-1 means latest attempt)'), 'addattempt' => new external_value(PARAM_BOOL, 'Allow another attempt if the attempt reopen method is manual'), 'workflowstate' => new external_value(PARAM_ALPHA, 'The next marking workflow state'), 'applytoall' => new external_value(PARAM_BOOL, 'If true, this grade will be applied ' . 'to all members ' . 'of the group (for group assignments).'), - 'plugindata' => new external_single_structure( - $pluginfeedbackparams - ) + 'plugindata' => new external_single_structure($pluginfeedbackparams, 'plugin data', VALUE_DEFAULT, array()), + 'advancedgradingdata' => new external_single_structure($advancedgradingdata, 'advanced grading data', + VALUE_DEFAULT, array()) ) ); } @@ -1683,6 +1705,7 @@ class mod_assign_external extends external_api { * @param string $workflowstate New workflow state * @param bool $applytoall Apply the grade to all members of the group * @param array $plugindata Custom data used by plugins + * @param array $advancedgradingdata Advanced grading data * @return null * @since Moodle 2.6 */ @@ -1693,7 +1716,8 @@ class mod_assign_external extends external_api { $addattempt, $workflowstate, $applytoall, - $plugindata) { + $plugindata = array(), + $advancedgradingdata = array()) { global $CFG, $USER; require_once("$CFG->dirroot/mod/assign/locallib.php"); @@ -1705,7 +1729,8 @@ class mod_assign_external extends external_api { 'workflowstate' => $workflowstate, 'addattempt' => $addattempt, 'applytoall' => $applytoall, - 'plugindata' => $plugindata)); + 'plugindata' => $plugindata, + 'advancedgradingdata' => $advancedgradingdata)); $cm = get_coursemodule_from_instance('assign', $assignmentid, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); @@ -1720,6 +1745,21 @@ class mod_assign_external extends external_api { $gradedata->applytoall = $applytoall; $gradedata->grade = $grade; + if (!empty($advancedgradingdata)) { + $advancedgrading = array(); + $criteria = reset($advancedgradingdata); + foreach ($criteria as $key => $criterion) { + $details = array(); + foreach ($criterion as $value) { + foreach ($value['fillings'] as $filling) { + $details[$value['criterionid']] = $filling; + } + } + $advancedgrading[$key] = $details; + } + $gradedata->advancedgrading = $advancedgrading; + } + $assignment->save_grade($userid, $gradedata); return null; @@ -1735,6 +1775,155 @@ class mod_assign_external extends external_api { return null; } + /** + * Describes the parameters for save_grades + * @return external_external_function_parameters + * @since Moodle 2.7 + */ + public static function save_grades_parameters() { + global $CFG; + require_once("$CFG->dirroot/mod/assign/locallib.php"); + require_once("$CFG->dirroot/grade/grading/lib.php"); + $instance = new assign(null, null, null); + $pluginfeedbackparams = array(); + + foreach ($instance->get_feedback_plugins() as $plugin) { + $pluginparams = $plugin->get_external_parameters(); + if (!empty($pluginparams)) { + $pluginfeedbackparams = array_merge($pluginfeedbackparams, $pluginparams); + } + } + + $advancedgradingdata = array(); + $methods = array_keys(grading_manager::available_methods(false)); + foreach ($methods as $method) { + require_once($CFG->dirroot.'/grade/grading/form/'.$method.'/lib.php'); + $details = call_user_func('gradingform_'.$method.'_controller::get_external_instance_filling_details'); + if (!empty($details)) { + $items = array(); + foreach ($details as $key => $value) { + $value->required = VALUE_OPTIONAL; + unset($value->content->keys['id']); + $items[$key] = new external_multiple_structure (new external_single_structure( + array( + 'criterionid' => new external_value(PARAM_INT, 'criterion id'), + 'fillings' => $value + ) + )); + } + $advancedgradingdata[$method] = new external_single_structure($items, 'items', VALUE_OPTIONAL); + } + } + + return new external_function_parameters( + array( + 'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'), + 'applytoall' => new external_value(PARAM_BOOL, 'If true, this grade will be applied ' . + 'to all members ' . + 'of the group (for group assignments).'), + 'grades' => new external_multiple_structure( + new external_single_structure( + array ( + 'userid' => new external_value(PARAM_INT, 'The student id to operate on'), + 'grade' => new external_value(PARAM_FLOAT, 'The new grade for this user'), + 'attemptnumber' => new external_value(PARAM_INT, 'The attempt number (-1 means latest attempt)'), + 'addattempt' => new external_value(PARAM_BOOL, 'Allow another attempt if manual attempt reopen method'), + 'workflowstate' => new external_value(PARAM_ALPHA, 'The next marking workflow state'), + 'plugindata' => new external_single_structure($pluginfeedbackparams, 'plugin data', + VALUE_DEFAULT, array()), + 'advancedgradingdata' => new external_single_structure($advancedgradingdata, 'advanced grading data', + VALUE_DEFAULT, array()) + ) + ) + ) + ) + ); + } + + /** + * Save multiple student grades for a single assignment. + * + * @param int $assignmentid The id of the assignment + * @param boolean $applytoall If set to true and this is a team assignment, + * apply the grade to all members of the group + * @param array $grades grade data for one or more students that includes + * userid - The id of the student being graded + * grade - The grade + * attemptnumber - The attempt number + * addattempt - Allow another attempt + * workflowstate - New workflow state + * plugindata - optional feedback data used by plugins + * advancedgradingdata - optional Advanced grading data + * @throws invalid_parameter_exception if multiple grades are supplied for + * a team assignment that has $applytoall set to true + * @return null + * @since Moodle 2.7 + */ + public static function save_grades($assignmentid, $applytoall = false, $grades) { + global $CFG, $USER; + require_once("$CFG->dirroot/mod/assign/locallib.php"); + + $params = self::validate_parameters(self::save_grades_parameters(), + array('assignmentid' => $assignmentid, + 'applytoall' => $applytoall, + 'grades' => $grades)); + + $cm = get_coursemodule_from_instance('assign', $assignmentid, 0, false, MUST_EXIST); + $context = context_module::instance($cm->id); + + $assignment = new assign($context, $cm, null); + if ($assignment->get_instance()->teamsubmission && $applytoall) { + // Check that only 1 user per submission group is provided. + $groupids = array(); + foreach ($grades as $gradeinfo) { + $group = $assignment->get_submission_group($gradeinfo['userid']); + if (in_array($group->id, $groupids)) { + throw new invalid_parameter_exception('Multiple grades for the same team have been supplied ' + .' this is not permitted when the applytoall flag is set'); + } else { + $groupids[] = $group->id; + } + } + } + + foreach ($grades as $gradeinfo) { + $gradedata = (object)$gradeinfo['plugindata']; + $gradedata->addattempt = $gradeinfo['addattempt']; + $gradedata->attemptnumber = $gradeinfo['attemptnumber']; + $gradedata->workflowstate = $gradeinfo['workflowstate']; + $gradedata->applytoall = $applytoall; + $gradedata->grade = $gradeinfo['grade']; + + if (!empty($gradeinfo['advancedgradingdata'])) { + $advancedgrading = array(); + $criteria = reset($gradeinfo['advancedgradingdata']); + foreach ($criteria as $key => $criterion) { + $details = array(); + foreach ($criterion as $value) { + foreach ($value['fillings'] as $filling) { + $details[$value['criterionid']] = $filling; + } + } + $advancedgrading[$key] = $details; + } + $gradedata->advancedgrading = $advancedgrading; + } + $assignment->save_grade($gradeinfo['userid'], $gradedata); + } + + return null; + } + + /** + * Describes the return value for save_grades + * + * @return external_single_structure + * @since Moodle 2.7 + */ + public static function save_grades_returns() { + return null; + } + /** * Describes the parameters for copy_previous_attempt * @return external_external_function_parameters diff --git a/mod/assign/tests/externallib_test.php b/mod/assign/tests/externallib_test.php index 5ab2faa7d6c..b27db1898bb 100644 --- a/mod/assign/tests/externallib_test.php +++ b/mod/assign/tests/externallib_test.php @@ -916,7 +916,7 @@ class mod_assign_external_testcase extends externallib_advanced_testcase { $student1 = self::getDataGenerator()->create_user(); $student2 = self::getDataGenerator()->create_user(); - $studentrole = $DB->get_record('role', array('shortname'=>'student')); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); $this->getDataGenerator()->enrol_user($student1->id, $course->id, $studentrole->id); @@ -945,8 +945,8 @@ class mod_assign_external_testcase extends externallib_advanced_testcase { // Now try a grade. $feedbackpluginparams = array(); $feedbackpluginparams['files_filemanager'] = $draftidfile; - $feedbackeditorparams = array('text'=>'Yeeha!', - 'format'=>1); + $feedbackeditorparams = array('text' => 'Yeeha!', + 'format' => 1); $feedbackpluginparams['assignfeedbackcomments_editor'] = $feedbackeditorparams; $result = mod_assign_external::save_grade($instance->id, $student1->id, @@ -965,6 +965,299 @@ class mod_assign_external_testcase extends externallib_advanced_testcase { $this->assertEquals($result['assignments'][0]['grades'][0]['grade'], '50.0'); } + /** + * Test save grades with advanced grading data + */ + public function test_save_grades_with_advanced_grading() { + global $DB, $USER; + + $this->resetAfterTest(true); + // Create a course and assignment and users. + $course = self::getDataGenerator()->create_course(); + + $teacher = self::getDataGenerator()->create_user(); + $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); + $this->getDataGenerator()->enrol_user($teacher->id, + $course->id, + $teacherrole->id); + + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $params['course'] = $course->id; + $params['assignfeedback_file_enabled'] = 0; + $params['assignfeedback_comments_enabled'] = 0; + $instance = $generator->create_instance($params); + $cm = get_coursemodule_from_instance('assign', $instance->id); + $context = context_module::instance($cm->id); + + $assign = new assign($context, $cm, $course); + + $student1 = self::getDataGenerator()->create_user(); + $student2 = self::getDataGenerator()->create_user(); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $this->getDataGenerator()->enrol_user($student1->id, + $course->id, + $studentrole->id); + $this->getDataGenerator()->enrol_user($student2->id, + $course->id, + $studentrole->id); + + $this->setUser($teacher); + + $feedbackpluginparams = array(); + $feedbackpluginparams['files_filemanager'] = 0; + $feedbackeditorparams = array('text' => '', 'format' => 1); + $feedbackpluginparams['assignfeedbackcomments_editor'] = $feedbackeditorparams; + + // Create advanced grading data. + // Create grading area. + $gradingarea = array( + 'contextid' => $context->id, + 'component' => 'mod_assign', + 'areaname' => 'submissions', + 'activemethod' => 'rubric' + ); + $areaid = $DB->insert_record('grading_areas', $gradingarea); + + // Create a rubric grading definition. + $rubricdefinition = array ( + 'areaid' => $areaid, + 'method' => 'rubric', + 'name' => 'test', + 'status' => 20, + 'copiedfromid' => 1, + 'timecreated' => 1, + 'usercreated' => $teacher->id, + 'timemodified' => 1, + 'usermodified' => $teacher->id, + 'timecopied' => 0 + ); + $definitionid = $DB->insert_record('grading_definitions', $rubricdefinition); + + // Create a criterion with a level. + $rubriccriteria = array ( + 'definitionid' => $definitionid, + 'sortorder' => 1, + 'description' => 'Demonstrate an understanding of disease control', + 'descriptionformat' => 0 + ); + $criterionid = $DB->insert_record('gradingform_rubric_criteria', $rubriccriteria); + $rubriclevel1 = array ( + 'criterionid' => $criterionid, + 'score' => 50, + 'definition' => 'pass', + 'definitionformat' => 0 + ); + $rubriclevel2 = array ( + 'criterionid' => $criterionid, + 'score' => 100, + 'definition' => 'excellent', + 'definitionformat' => 0 + ); + $rubriclevel3 = array ( + 'criterionid' => $criterionid, + 'score' => 0, + 'definition' => 'fail', + 'definitionformat' => 0 + ); + $levelid1 = $DB->insert_record('gradingform_rubric_levels', $rubriclevel1); + $levelid2 = $DB->insert_record('gradingform_rubric_levels', $rubriclevel2); + $levelid3 = $DB->insert_record('gradingform_rubric_levels', $rubriclevel3); + + // Create the filling. + $student1filling = array ( + 'criterionid' => $criterionid, + 'levelid' => $levelid1, + 'remark' => 'well done you passed', + 'remarkformat' => 0 + ); + + $student2filling = array ( + 'criterionid' => $criterionid, + 'levelid' => $levelid2, + 'remark' => 'Excellent work', + 'remarkformat' => 0 + ); + + $student1criteria = array(array('criterionid' => $criterionid, 'fillings' => array($student1filling))); + $student1advancedgradingdata = array('rubric' => array('criteria' => $student1criteria)); + + $student2criteria = array(array('criterionid' => $criterionid, 'fillings' => array($student2filling))); + $student2advancedgradingdata = array('rubric' => array('criteria' => $student2criteria)); + + $grades = array(); + $student1gradeinfo = array(); + $student1gradeinfo['userid'] = $student1->id; + $student1gradeinfo['grade'] = 0; // Ignored since advanced grading is being used. + $student1gradeinfo['attemptnumber'] = -1; + $student1gradeinfo['addattempt'] = true; + $student1gradeinfo['workflowstate'] = 'released'; + $student1gradeinfo['plugindata'] = $feedbackpluginparams; + $student1gradeinfo['advancedgradingdata'] = $student1advancedgradingdata; + $grades[] = $student1gradeinfo; + + $student2gradeinfo = array(); + $student2gradeinfo['userid'] = $student2->id; + $student2gradeinfo['grade'] = 0; // Ignored since advanced grading is being used. + $student2gradeinfo['attemptnumber'] = -1; + $student2gradeinfo['addattempt'] = true; + $student2gradeinfo['workflowstate'] = 'released'; + $student2gradeinfo['plugindata'] = $feedbackpluginparams; + $student2gradeinfo['advancedgradingdata'] = $student2advancedgradingdata; + $grades[] = $student2gradeinfo; + + $result = mod_assign_external::save_grades($instance->id, false, $grades); + // No warnings. + $this->assertEquals(0, count($result)); + + $result = mod_assign_external::get_grades(array($instance->id)); + + $this->assertCount(2, $result['assignments'][0]['grades']); + + foreach ($result['assignments'][0]['grades'] as $grade) { + if ($grade['userid'] == $student1->id) { + $this->assertEquals($grade['grade'], '50.0'); + } else if ($grade['userid'] == $student2->id) { + $this->assertEquals($grade['grade'], '100.0'); + } else { + $this->fail("Unexpected student id"); + } + } + } + + /** + * Test save grades for a team submission + */ + public function test_save_grades_with_group_submission() { + global $DB, $USER, $CFG; + require_once($CFG->dirroot . '/group/lib.php'); + + $this->resetAfterTest(true); + // Create a course and assignment and users. + $course = self::getDataGenerator()->create_course(); + + $teacher = self::getDataGenerator()->create_user(); + $teacherrole = $DB->get_record('role', array('shortname' => 'teacher')); + $this->getDataGenerator()->enrol_user($teacher->id, + $course->id, + $teacherrole->id); + + $groupingdata = array(); + $groupingdata['courseid'] = $course->id; + $groupingdata['name'] = 'Group assignment grouping'; + + $grouping = self::getDataGenerator()->create_grouping($groupingdata); + + $group1data = array(); + $group1data['courseid'] = $course->id; + $group1data['name'] = 'Team 1'; + $group2data = array(); + $group2data['courseid'] = $course->id; + $group2data['name'] = 'Team 2'; + + $group1 = self::getDataGenerator()->create_group($group1data); + $group2 = self::getDataGenerator()->create_group($group2data); + + groups_assign_grouping($grouping->id, $group1->id); + groups_assign_grouping($grouping->id, $group2->id); + + $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign'); + $params['course'] = $course->id; + $params['teamsubmission'] = 1; + $params['teamsubmissiongroupingid'] = $grouping->id; + $instance = $generator->create_instance($params); + $cm = get_coursemodule_from_instance('assign', $instance->id); + $context = context_module::instance($cm->id); + + $assign = new assign($context, $cm, $course); + + $student1 = self::getDataGenerator()->create_user(); + $student2 = self::getDataGenerator()->create_user(); + $student3 = self::getDataGenerator()->create_user(); + $student4 = self::getDataGenerator()->create_user(); + $studentrole = $DB->get_record('role', array('shortname' => 'student')); + $this->getDataGenerator()->enrol_user($student1->id, + $course->id, + $studentrole->id); + $this->getDataGenerator()->enrol_user($student2->id, + $course->id, + $studentrole->id); + $this->getDataGenerator()->enrol_user($student3->id, + $course->id, + $studentrole->id); + $this->getDataGenerator()->enrol_user($student4->id, + $course->id, + $studentrole->id); + + groups_add_member($group1->id, $student1->id); + groups_add_member($group1->id, $student2->id); + groups_add_member($group1->id, $student3->id); + groups_add_member($group2->id, $student4->id); + $this->setUser($teacher); + + $feedbackpluginparams = array(); + $feedbackpluginparams['files_filemanager'] = 0; + $feedbackeditorparams = array('text' => '', 'format' => 1); + $feedbackpluginparams['assignfeedbackcomments_editor'] = $feedbackeditorparams; + + $grades1 = array(); + $student1gradeinfo = array(); + $student1gradeinfo['userid'] = $student1->id; + $student1gradeinfo['grade'] = 50; + $student1gradeinfo['attemptnumber'] = -1; + $student1gradeinfo['addattempt'] = true; + $student1gradeinfo['workflowstate'] = 'released'; + $student1gradeinfo['plugindata'] = $feedbackpluginparams; + $grades1[] = $student1gradeinfo; + + $student2gradeinfo = array(); + $student2gradeinfo['userid'] = $student2->id; + $student2gradeinfo['grade'] = 75; + $student2gradeinfo['attemptnumber'] = -1; + $student2gradeinfo['addattempt'] = true; + $student2gradeinfo['workflowstate'] = 'released'; + $student2gradeinfo['plugindata'] = $feedbackpluginparams; + $grades1[] = $student2gradeinfo; + + $this->setExpectedException('invalid_parameter_exception'); + // Expect an exception since 2 grades have been submitted for the same team. + $result = mod_assign_external::save_grades($instance->id, true, $grades1); + + $grades2 = array(); + $student3gradeinfo = array(); + $student3gradeinfo['userid'] = $student3->id; + $student3gradeinfo['grade'] = 50; + $student3gradeinfo['attemptnumber'] = -1; + $student3gradeinfo['addattempt'] = true; + $student3gradeinfo['workflowstate'] = 'released'; + $student3gradeinfo['plugindata'] = $feedbackpluginparams; + $grades2[] = $student3gradeinfo; + + $student4gradeinfo = array(); + $student4gradeinfo['userid'] = $student4->id; + $student4gradeinfo['grade'] = 75; + $student4gradeinfo['attemptnumber'] = -1; + $student4gradeinfo['addattempt'] = true; + $student4gradeinfo['workflowstate'] = 'released'; + $student4gradeinfo['plugindata'] = $feedbackpluginparams; + $grades2[] = $student4gradeinfo; + $result = mod_assign_external::save_grades($instance->id, true, $grades2); + // There should be no warnings. + $this->assertEquals(0, count($result)); + $result = mod_assign_external::get_grades(array($instance->id)); + + $this->assertCount(2, $result['assignments'][0]['grades']); + + foreach ($result['assignments'][0]['grades'] as $grade) { + if ($grade['userid'] == $student3->id) { + $this->assertEquals($grade['grade'], '50.0'); + } else if ($grade['userid'] == $student4->id) { + $this->assertEquals($grade['grade'], '75.0'); + } else { + $this->fail("Unexpected student id"); + } + } + } + /** * Test copy_previous_attempt */ diff --git a/mod/assign/upgrade.txt b/mod/assign/upgrade.txt index 44c2f892be6..90c1205b059 100644 --- a/mod/assign/upgrade.txt +++ b/mod/assign/upgrade.txt @@ -1,5 +1,10 @@ This files describes API changes in the assign code. +=== 2.7 === +* Web service function mod_assign_save_grade has an additional optional parameter $advancedgradingdata which allows + advanced grading data to be used. +* A new web service function mod_assign_save_grades has been added which allows multiple grades to be processed. + === 2.6 === * To see submission/grades of inactive users, user should have moodle/course:viewsuspendedusers capability. * count_* functions will return only active participants. diff --git a/mod/assign/version.php b/mod/assign/version.php index 266f34f16e7..1e8f15b436f 100644 --- a/mod/assign/version.php +++ b/mod/assign/version.php @@ -25,7 +25,7 @@ defined('MOODLE_INTERNAL') || die(); $module->component = 'mod_assign'; // Full name of the plugin (used for diagnostics). -$module->version = 2013110500; // The current module version (Date: YYYYMMDDXX). +$module->version = 2013110501; // The current module version (Date: YYYYMMDDXX). $module->requires = 2013110500; // Requires this Moodle version. $module->cron = 60; -- 2.43.0