diff --git a/course/externallib.php b/course/externallib.php index 8b5025695e3..e1052eead10 100644 --- a/course/externallib.php +++ b/course/externallib.php @@ -2385,12 +2385,9 @@ class core_course_external extends external_api { * @throws moodle_exception */ public static function get_course_module($cmid) { + global $CFG, $DB; - $params = self::validate_parameters(self::get_course_module_parameters(), - array( - 'cmid' => $cmid, - )); - + $params = self::validate_parameters(self::get_course_module_parameters(), array('cmid' => $cmid)); $warnings = array(); $cm = get_coursemodule_from_id(null, $params['cmid'], 0, true, MUST_EXIST); @@ -2399,7 +2396,49 @@ class core_course_external extends external_api { // If the user has permissions to manage the activity, return all the information. if (has_capability('moodle/course:manageactivities', $context)) { + require_once($CFG->dirroot . '/course/modlib.php'); + require_once($CFG->libdir . '/gradelib.php'); + $info = $cm; + // Get the extra information: grade, advanced grading and outcomes data. + $course = get_course($cm->course); + list($newcm, $newcontext, $module, $extrainfo, $cw) = get_moduleinfo_data($cm, $course); + // Grades. + $gradeinfo = array('grade', 'gradepass', 'gradecat'); + foreach ($gradeinfo as $gfield) { + if (isset($extrainfo->{$gfield})) { + $info->{$gfield} = $extrainfo->{$gfield}; + } + } + if (isset($extrainfo->grade) and $extrainfo->grade < 0) { + $info->scale = $DB->get_field('scale', 'scale', array('id' => abs($extrainfo->grade))); + } + // Advanced grading. + if (isset($extrainfo->_advancedgradingdata)) { + $info->advancedgrading = array(); + foreach ($extrainfo as $key => $val) { + if (strpos($key, 'advancedgradingmethod_') === 0) { + $info->advancedgrading[] = array( + 'area' => str_replace('advancedgradingmethod_', '', $key), + 'method' => $val + ); + } + } + } + // Outcomes. + foreach ($extrainfo as $key => $val) { + if (strpos($key, 'outcome_') === 0) { + if (!isset($info->outcomes)) { + $info->outcomes = array(); + } + $id = str_replace('outcome_', '', $key); + $outcome = $outcome = grade_outcome::fetch(array('id' => $id)); + $info->outcomes[] = array( + 'id' => $id, + 'name' => external_format_string($outcome->get_name(), $context->id) + ); + } + } } else { // Return information is safe to show to any user. $info = new stdClass(); @@ -2416,7 +2455,6 @@ class core_course_external extends external_api { } // Format name. $info->name = external_format_string($cm->name, $context->id); - $result = array(); $result['cm'] = $info; $result['warnings'] = $warnings; @@ -2456,6 +2494,28 @@ class core_course_external extends external_api { 'completionexpected' => new external_value(PARAM_INT, 'Completion time expected', VALUE_OPTIONAL), 'showdescription' => new external_value(PARAM_INT, 'If the description is showed', VALUE_OPTIONAL), 'availability' => new external_value(PARAM_RAW, 'Availability settings', VALUE_OPTIONAL), + 'grade' => new external_value(PARAM_INT, 'Grade (max value or scale id)', VALUE_OPTIONAL), + 'scale' => new external_value(PARAM_TEXT, 'Scale items (if used)', VALUE_OPTIONAL), + 'gradepass' => new external_value(PARAM_RAW, 'Grade to pass (float)', VALUE_OPTIONAL), + 'gradecat' => new external_value(PARAM_INT, 'Grade category', VALUE_OPTIONAL), + 'advancedgrading' => new external_multiple_structure( + new external_single_structure( + array( + 'area' => new external_value(PARAM_AREA, 'Gradable area name'), + 'method' => new external_value(PARAM_COMPONENT, 'Grading method'), + ) + ), + 'Advanced grading settings', VALUE_OPTIONAL + ), + 'outcomes' => new external_multiple_structure( + new external_single_structure( + array( + 'id' => new external_value(PARAM_ALPHANUMEXT, 'Outcome id'), + 'name' => new external_value(PARAM_TEXT, 'Outcome full name'), + ) + ), + 'Outcomes information', VALUE_OPTIONAL + ), ) ), 'warnings' => new external_warnings() diff --git a/course/modedit.php b/course/modedit.php index 27b982b3930..c386e5940d9 100644 --- a/course/modedit.php +++ b/course/modedit.php @@ -60,48 +60,10 @@ if (!empty($add)) { // will be the closest match we have. navigation_node::override_active_url(course_get_url($course, $section)); - list($module, $context, $cw) = can_add_moduleinfo($course, $add, $section); - - $cm = null; - - $data = new stdClass(); - $data->section = $section; // The section number itself - relative!!! (section column in course_sections) - $data->visible = $cw->visible; - $data->course = $course->id; - $data->module = $module->id; - $data->modulename = $module->name; - $data->groupmode = $course->groupmode; - $data->groupingid = $course->defaultgroupingid; - $data->id = ''; - $data->instance = ''; - $data->coursemodule = ''; - $data->add = $add; - $data->return = 0; //must be false if this is an add, go back to course view on cancel - $data->sr = $sectionreturn; - - if (plugin_supports('mod', $data->modulename, FEATURE_MOD_INTRO, true)) { - $draftid_editor = file_get_submitted_draft_itemid('introeditor'); - file_prepare_draft_area($draftid_editor, null, null, null, null, array('subdirs'=>true)); - $data->introeditor = array('text'=>'', 'format'=>FORMAT_HTML, 'itemid'=>$draftid_editor); // TODO: add better default - } - - if (plugin_supports('mod', $data->modulename, FEATURE_ADVANCED_GRADING, false) - and has_capability('moodle/grade:managegradingforms', $context)) { - require_once($CFG->dirroot.'/grade/grading/lib.php'); - - $data->_advancedgradingdata['methods'] = grading_manager::available_methods(); - $areas = grading_manager::available_areas('mod_'.$module->name); - - foreach ($areas as $areaname => $areatitle) { - $data->_advancedgradingdata['areas'][$areaname] = array( - 'title' => $areatitle, - 'method' => '', - ); - $formfield = 'advancedgradingmethod_'.$areaname; - $data->{$formfield} = ''; - } - } - + list($module, $context, $cw, $cm, $data) = prepare_new_moduleinfo_data($course, $add, $section); + $data->return = 0; + $data->sr = $sectionreturn; + $data->add = $add; if (!empty($type)) { //TODO: hopefully will be removed in 2.0 $data->type = $type; } @@ -136,86 +98,10 @@ if (!empty($add)) { // require_login require_login($course, false, $cm); // needed to setup proper $COURSE - list($cm, $context, $module, $data, $cw) = can_update_moduleinfo($cm); - - $data->coursemodule = $cm->id; - $data->section = $cw->section; // The section number itself - relative!!! (section column in course_sections) - $data->visible = $cm->visible; //?? $cw->visible ? $cm->visible : 0; // section hiding overrides - $data->cmidnumber = $cm->idnumber; // The cm IDnumber - $data->groupmode = groups_get_activity_groupmode($cm); // locked later if forced - $data->groupingid = $cm->groupingid; - $data->course = $course->id; - $data->module = $module->id; - $data->modulename = $module->name; - $data->instance = $cm->instance; - $data->return = $return; - $data->sr = $sectionreturn; - $data->update = $update; - $data->completion = $cm->completion; - $data->completionview = $cm->completionview; - $data->completionexpected = $cm->completionexpected; - $data->completionusegrade = is_null($cm->completiongradeitemnumber) ? 0 : 1; - $data->showdescription = $cm->showdescription; - $data->tags = core_tag_tag::get_item_tags_array('core', 'course_modules', $cm->id); - if (!empty($CFG->enableavailability)) { - $data->availabilityconditionsjson = $cm->availability; - } - - if (plugin_supports('mod', $data->modulename, FEATURE_MOD_INTRO, true)) { - $draftid_editor = file_get_submitted_draft_itemid('introeditor'); - $currentintro = file_prepare_draft_area($draftid_editor, $context->id, 'mod_'.$data->modulename, 'intro', 0, array('subdirs'=>true), $data->intro); - $data->introeditor = array('text'=>$currentintro, 'format'=>$data->introformat, 'itemid'=>$draftid_editor); - } - - if (plugin_supports('mod', $data->modulename, FEATURE_ADVANCED_GRADING, false) - and has_capability('moodle/grade:managegradingforms', $context)) { - require_once($CFG->dirroot.'/grade/grading/lib.php'); - $gradingman = get_grading_manager($context, 'mod_'.$data->modulename); - $data->_advancedgradingdata['methods'] = $gradingman->get_available_methods(); - $areas = $gradingman->get_available_areas(); - - foreach ($areas as $areaname => $areatitle) { - $gradingman->set_area($areaname); - $method = $gradingman->get_active_method(); - $data->_advancedgradingdata['areas'][$areaname] = array( - 'title' => $areatitle, - 'method' => $method, - ); - $formfield = 'advancedgradingmethod_'.$areaname; - $data->{$formfield} = $method; - } - } - - if ($items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$data->modulename, - 'iteminstance'=>$data->instance, 'courseid'=>$course->id))) { - // Add existing outcomes. - foreach ($items as $item) { - if (!empty($item->outcomeid)) { - $data->{'outcome_' . $item->outcomeid} = 1; - } else if (!empty($item->gradepass)) { - $decimalpoints = $item->get_decimals(); - $data->gradepass = format_float($item->gradepass, $decimalpoints); - } - } - - // set category if present - $gradecat = false; - foreach ($items as $item) { - if ($gradecat === false) { - $gradecat = $item->categoryid; - continue; - } - if ($gradecat != $item->categoryid) { - //mixed categories - $gradecat = false; - break; - } - } - if ($gradecat !== false) { - // do not set if mixed categories present - $data->gradecat = $gradecat; - } - } + list($cm, $context, $module, $data, $cw) = get_moduleinfo_data($cm, $course); + $data->return = $return; + $data->sr = $sectionreturn; + $data->update = $update; $sectionname = get_section_name($course, $cw); $fullmodulename = get_string('modulename', $module->name); diff --git a/course/modlib.php b/course/modlib.php index ab0c7a6c091..db96183101c 100644 --- a/course/modlib.php +++ b/course/modlib.php @@ -637,3 +637,147 @@ function include_modulelib($modulename) { } } +/** + * Get module information data required for updating the module. + * + * @param stdClass $cm course module object + * @param stdClass $course course object + * @return array required data for updating a module + * @since Moodle 3.2 + */ +function get_moduleinfo_data($cm, $course) { + global $CFG; + + list($cm, $context, $module, $data, $cw) = can_update_moduleinfo($cm); + + $data->coursemodule = $cm->id; + $data->section = $cw->section; // The section number itself - relative!!! (section column in course_sections) + $data->visible = $cm->visible; //?? $cw->visible ? $cm->visible : 0; // section hiding overrides + $data->cmidnumber = $cm->idnumber; // The cm IDnumber + $data->groupmode = groups_get_activity_groupmode($cm); // locked later if forced + $data->groupingid = $cm->groupingid; + $data->course = $course->id; + $data->module = $module->id; + $data->modulename = $module->name; + $data->instance = $cm->instance; + $data->completion = $cm->completion; + $data->completionview = $cm->completionview; + $data->completionexpected = $cm->completionexpected; + $data->completionusegrade = is_null($cm->completiongradeitemnumber) ? 0 : 1; + $data->showdescription = $cm->showdescription; + $data->tags = core_tag_tag::get_item_tags_array('core', 'course_modules', $cm->id); + if (!empty($CFG->enableavailability)) { + $data->availabilityconditionsjson = $cm->availability; + } + + if (plugin_supports('mod', $data->modulename, FEATURE_MOD_INTRO, true)) { + $draftid_editor = file_get_submitted_draft_itemid('introeditor'); + $currentintro = file_prepare_draft_area($draftid_editor, $context->id, 'mod_'.$data->modulename, 'intro', 0, array('subdirs'=>true), $data->intro); + $data->introeditor = array('text'=>$currentintro, 'format'=>$data->introformat, 'itemid'=>$draftid_editor); + } + + if (plugin_supports('mod', $data->modulename, FEATURE_ADVANCED_GRADING, false) + and has_capability('moodle/grade:managegradingforms', $context)) { + require_once($CFG->dirroot.'/grade/grading/lib.php'); + $gradingman = get_grading_manager($context, 'mod_'.$data->modulename); + $data->_advancedgradingdata['methods'] = $gradingman->get_available_methods(); + $areas = $gradingman->get_available_areas(); + + foreach ($areas as $areaname => $areatitle) { + $gradingman->set_area($areaname); + $method = $gradingman->get_active_method(); + $data->_advancedgradingdata['areas'][$areaname] = array( + 'title' => $areatitle, + 'method' => $method, + ); + $formfield = 'advancedgradingmethod_'.$areaname; + $data->{$formfield} = $method; + } + } + + if ($items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$data->modulename, + 'iteminstance'=>$data->instance, 'courseid'=>$course->id))) { + // Add existing outcomes. + foreach ($items as $item) { + if (!empty($item->outcomeid)) { + $data->{'outcome_' . $item->outcomeid} = 1; + } else if (!empty($item->gradepass)) { + $decimalpoints = $item->get_decimals(); + $data->gradepass = format_float($item->gradepass, $decimalpoints); + } + } + + // set category if present + $gradecat = false; + foreach ($items as $item) { + if ($gradecat === false) { + $gradecat = $item->categoryid; + continue; + } + if ($gradecat != $item->categoryid) { + //mixed categories + $gradecat = false; + break; + } + } + if ($gradecat !== false) { + // do not set if mixed categories present + $data->gradecat = $gradecat; + } + } + return array($cm, $context, $module, $data, $cw); +} + +/** + * Prepare the standard module information for a new module instance. + * + * @param stdClass $course course object + * @param string $modulename module name + * @param int $section section number + * @return array module information about other required data + * @since Moodle 3.2 + */ +function prepare_new_moduleinfo_data($course, $modulename, $section) { + global $CFG; + + list($module, $context, $cw) = can_add_moduleinfo($course, $modulename, $section); + + $cm = null; + + $data = new stdClass(); + $data->section = $section; // The section number itself - relative!!! (section column in course_sections) + $data->visible = $cw->visible; + $data->course = $course->id; + $data->module = $module->id; + $data->modulename = $module->name; + $data->groupmode = $course->groupmode; + $data->groupingid = $course->defaultgroupingid; + $data->id = ''; + $data->instance = ''; + $data->coursemodule = ''; + + if (plugin_supports('mod', $data->modulename, FEATURE_MOD_INTRO, true)) { + $draftid_editor = file_get_submitted_draft_itemid('introeditor'); + file_prepare_draft_area($draftid_editor, null, null, null, null, array('subdirs'=>true)); + $data->introeditor = array('text'=>'', 'format'=>FORMAT_HTML, 'itemid'=>$draftid_editor); // TODO: add better default + } + + if (plugin_supports('mod', $data->modulename, FEATURE_ADVANCED_GRADING, false) + and has_capability('moodle/grade:managegradingforms', $context)) { + require_once($CFG->dirroot.'/grade/grading/lib.php'); + + $data->_advancedgradingdata['methods'] = grading_manager::available_methods(); + $areas = grading_manager::available_areas('mod_'.$module->name); + + foreach ($areas as $areaname => $areatitle) { + $data->_advancedgradingdata['areas'][$areaname] = array( + 'title' => $areatitle, + 'method' => '', + ); + $formfield = 'advancedgradingmethod_'.$areaname; + $data->{$formfield} = ''; + } + } + + return array($module, $context, $cw, $cm, $data); +} diff --git a/course/tests/externallib_test.php b/course/tests/externallib_test.php index 2a49a475785..c531f8dc75e 100644 --- a/course/tests/externallib_test.php +++ b/course/tests/externallib_test.php @@ -1593,24 +1593,29 @@ class core_course_externallib_testcase extends externallib_advanced_testcase { $course = self::getDataGenerator()->create_course(); $record = array( 'course' => $course->id, - 'name' => 'First Chat' + 'name' => 'First Assignment' ); $options = array( 'idnumber' => 'ABC', 'visible' => 0 ); // Hidden activity. - $chat = self::getDataGenerator()->create_module('chat', $record, $options); + $assign = self::getDataGenerator()->create_module('assign', $record, $options); // Test admin user can see the complete hidden activity. - $result = core_course_external::get_course_module($chat->cmid); + $result = core_course_external::get_course_module($assign->cmid); $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result); $this->assertCount(0, $result['warnings']); // Test we retrieve all the fields. - $this->assertCount(22, $result['cm']); + $this->assertCount(26, $result['cm']); $this->assertEquals($record['name'], $result['cm']['name']); $this->assertEquals($options['idnumber'], $result['cm']['idnumber']); + $this->assertEquals(100, $result['cm']['grade']); + $this->assertEquals(0.0, $result['cm']['gradepass']); + $this->assertEquals('submissions', $result['cm']['advancedgrading'][0]['area']); + $this->assertEmpty($result['cm']['advancedgrading'][0]['method']); + $this->assertArrayNotHasKey('outcomes', $result['cm']); $student = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); @@ -1620,26 +1625,26 @@ class core_course_externallib_testcase extends externallib_advanced_testcase { // The user shouldn't be able to see the activity. try { - core_course_external::get_course_module($chat->cmid); + core_course_external::get_course_module($assign->cmid); $this->fail('Exception expected due to invalid permissions.'); } catch (moodle_exception $e) { $this->assertEquals('requireloginerror', $e->errorcode); } // Make module visible. - set_coursemodule_visible($chat->cmid, 1); + set_coursemodule_visible($assign->cmid, 1); // Test student user. - $result = core_course_external::get_course_module($chat->cmid); + $result = core_course_external::get_course_module($assign->cmid); $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result); $this->assertCount(0, $result['warnings']); // Test we retrieve only the few files we can see. $this->assertCount(11, $result['cm']); - $this->assertEquals($chat->cmid, $result['cm']['id']); + $this->assertEquals($assign->cmid, $result['cm']['id']); $this->assertEquals($course->id, $result['cm']['course']); - $this->assertEquals('chat', $result['cm']['modname']); - $this->assertEquals($chat->id, $result['cm']['instance']); + $this->assertEquals('assign', $result['cm']['modname']); + $this->assertEquals($assign->id, $result['cm']['instance']); } diff --git a/course/tests/modlib_test.php b/course/tests/modlib_test.php new file mode 100644 index 00000000000..c1a6b45db11 --- /dev/null +++ b/course/tests/modlib_test.php @@ -0,0 +1,153 @@ +. + +/** + * Module lib related unit tests + * + * @package core + * @category phpunit + * @copyright 2016 Juan Leyva + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +global $CFG; +require_once($CFG->dirroot . '/course/lib.php'); +require_once($CFG->dirroot . '/course/modlib.php'); + +class core_course_modlib_testcase extends advanced_testcase { + + /** + * Test prepare_new_moduleinfo_data + */ + public function test_prepare_new_moduleinfo_data() { + global $DB; + $this->resetAfterTest(true); + + $this->setAdminUser(); + $course = self::getDataGenerator()->create_course(); + $coursecontext = context_course::instance($course->id); + // Test with a complex module, like assign. + $assignmodule = $DB->get_record('modules', array('name' => 'assign'), '*', MUST_EXIST); + $sectionnumber = 1; + + list($module, $context, $cw, $cm, $data) = prepare_new_moduleinfo_data($course, $assignmodule->name, $sectionnumber); + $this->assertEquals($assignmodule, $module); + $this->assertEquals($coursecontext, $context); + $this->assertNull($cm); // Not cm yet. + + $expecteddata = new stdClass(); + $expecteddata->section = $sectionnumber; + $expecteddata->visible = 1; + $expecteddata->course = $course->id; + $expecteddata->module = $module->id; + $expecteddata->modulename = $module->name; + $expecteddata->groupmode = $course->groupmode; + $expecteddata->groupingid = $course->defaultgroupingid; + $expecteddata->id = ''; + $expecteddata->instance = ''; + $expecteddata->coursemodule = ''; + $expecteddata->advancedgradingmethod_submissions = ''; // Not grading methods enabled by default. + // Unset untestable. + unset($data->introeditor); + unset($data->_advancedgradingdata); + + $this->assertEquals($expecteddata, $data); + + // Create a viewer user. Not able to edit. + $viewer = self::getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($viewer->id, $course->id); + $this->setUser($viewer); + $this->expectException('required_capability_exception'); + prepare_new_moduleinfo_data($course, $assignmodule->name, $sectionnumber); + } + + /** + * Test get_moduleinfo_data + */ + public function test_get_moduleinfo_data() { + global $DB; + $this->resetAfterTest(true); + $this->setAdminUser(); + $course = self::getDataGenerator()->create_course(); + $assignmodule = $DB->get_record('modules', array('name' => 'assign'), '*', MUST_EXIST); + $assign = self::getDataGenerator()->create_module('assign', array('course' => $course->id)); + $assigncm = get_coursemodule_from_id('assign', $assign->cmid); + $assigncontext = context_module::instance($assign->cmid); + + list($cm, $context, $module, $data, $cw) = get_moduleinfo_data($assigncm, $course); + $this->assertEquals($assigncm, $cm); + $this->assertEquals($assigncontext, $context); + $this->assertEquals($assignmodule, $module); + + // Prepare expected data. + $expecteddata = clone $assign; + $expecteddata->coursemodule = $assigncm->id; + $expecteddata->section = $cw->section; + $expecteddata->visible = $assigncm->visible; + $expecteddata->cmidnumber = $assigncm->idnumber; + $expecteddata->groupmode = groups_get_activity_groupmode($cm); + $expecteddata->groupingid = $assigncm->groupingid; + $expecteddata->course = $course->id; + $expecteddata->module = $module->id; + $expecteddata->modulename = $module->name; + $expecteddata->instance = $assigncm->instance; + $expecteddata->completion = $assigncm->completion; + $expecteddata->completionview = $assigncm->completionview; + $expecteddata->completionexpected = $assigncm->completionexpected; + $expecteddata->completionusegrade = is_null($assigncm->completiongradeitemnumber) ? 0 : 1; + $expecteddata->showdescription = $assigncm->showdescription; + $expecteddata->tags = core_tag_tag::get_item_tags_array('core', 'course_modules', $assigncm->id); + $expecteddata->availabilityconditionsjson = null; + $expecteddata->advancedgradingmethod_submissions = null; + if ($items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => 'assign', + 'iteminstance' => $assign->id, 'courseid' => $course->id))) { + // set category if present + $gradecat = false; + foreach ($items as $item) { + if ($gradecat === false) { + $gradecat = $item->categoryid; + continue; + } + if ($gradecat != $item->categoryid) { + //mixed categories + $gradecat = false; + break; + } + } + if ($gradecat !== false) { + // do not set if mixed categories present + $expecteddata->gradecat = $gradecat; + } + } + $expecteddata->gradepass = '0.00'; + + // Unset untestable. + unset($expecteddata->cmid); + unset($data->introeditor); + unset($data->_advancedgradingdata); + + $this->assertEquals($expecteddata, $data); + + // Create a viewer user. Not able to edit. + $viewer = self::getDataGenerator()->create_user(); + $this->getDataGenerator()->enrol_user($viewer->id, $course->id); + $this->setUser($viewer); + $this->expectException('required_capability_exception'); + get_moduleinfo_data($assigncm, $course); + } +}