Commit fcc88fdd authored by Andrew Nicols's avatar Andrew Nicols Committed by Mathew May
Browse files

MDL-66079 core_grades: Add support for multiple grade items in an activity

Part of MDL-66074
parent bae67469
......@@ -27,6 +27,8 @@
defined('MOODLE_INTERNAL') || die;
use \core_grades\component_gradeitems;
require_once($CFG->dirroot.'/course/lib.php');
/**
......@@ -213,49 +215,63 @@ function edit_module_post_actions($moduleinfo, $course) {
$hasgrades = plugin_supports('mod', $moduleinfo->modulename, FEATURE_GRADE_HAS_GRADE, false);
$hasoutcomes = plugin_supports('mod', $moduleinfo->modulename, FEATURE_GRADE_OUTCOMES, true);
// Sync idnumber with grade_item.
if ($hasgrades && $grade_item = grade_item::fetch(array('itemtype'=>'mod', 'itemmodule'=>$moduleinfo->modulename,
'iteminstance'=>$moduleinfo->instance, 'itemnumber'=>0, 'courseid'=>$course->id))) {
$gradeupdate = false;
if ($grade_item->idnumber != $moduleinfo->cmidnumber) {
$grade_item->idnumber = $moduleinfo->cmidnumber;
$gradeupdate = true;
}
if (isset($moduleinfo->gradepass) && $grade_item->gradepass != $moduleinfo->gradepass) {
$grade_item->gradepass = $moduleinfo->gradepass;
$gradeupdate = true;
}
if ($gradeupdate) {
$grade_item->update();
}
}
if ($hasgrades) {
$items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$moduleinfo->modulename,
'iteminstance'=>$moduleinfo->instance, 'courseid'=>$course->id));
} else {
$items = array();
}
$items = grade_item::fetch_all([
'itemtype' => 'mod',
'itemmodule' => $moduleinfo->modulename,
'iteminstance' => $moduleinfo->instance,
'courseid' => $course->id,
]);
// Create parent category if requested and move to correct parent category.
if ($items and isset($moduleinfo->gradecat)) {
if ($moduleinfo->gradecat == -1) {
$grade_category = new grade_category();
$grade_category->courseid = $course->id;
$grade_category->fullname = $moduleinfo->name;
$grade_category->insert();
if ($grade_item) {
$parent = $grade_item->get_parent_category();
$grade_category->set_parent($parent->id);
$component = "mod_{$moduleinfo->modulename}";
if ($items) {
foreach ($items as $item) {
$update = false;
// Sync idnumber with grade_item.
// Note: This only happens for itemnumber 0 at this time.
if ($item->itemnumber == 0 && ($item->idnumber != $moduleinfo->cmidnumber)) {
$item->idnumber = $moduleinfo->cmidnumber;
$update = true;
}
$moduleinfo->gradecat = $grade_category->id;
}
foreach ($items as $itemid=>$unused) {
$items[$itemid]->set_parent($moduleinfo->gradecat);
if ($itemid == $grade_item->id) {
// Use updated grade_item.
$grade_item = $items[$itemid];
// Determine the grade category.
$gradecatfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $item->itemnumber, 'gradecat');
if (property_exists($moduleinfo, $gradecatfieldname)) {
$gradecat = $moduleinfo->$gradecatfieldname;
if ($gradecat == -1) {
$gradecategory = new grade_category();
$gradecategory->courseid = $course->id;
$gradecategory->fullname = $moduleinfo->name;
$gradecategory->insert();
$parent = $item->get_parent_category();
$gradecategory->set_parent($parent->id);
$gradecat = $gradecategory->id;
}
$oldgradecat = null;
if ($parent = $item->get_parent_category()) {
$oldgradecat = $parent->id;
}
if ($oldgradecat != $gradecat) {
$item->set_parent($gradecat);
$update = true;
}
}
// Determine the gradepass.
$gradepassfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $item->itemnumber, 'gradepass');
if (isset($moduleinfo->{$gradepassfieldname})) {
$gradepass = $moduleinfo->{$gradepassfieldname};
if (null !== $gradepass && $gradepass != $item->gradepass) {
$item->gradepass = $gradepass;
$update = true;
}
}
if ($update) {
$item->update();
}
}
}
......@@ -263,8 +279,6 @@ function edit_module_post_actions($moduleinfo, $course) {
require_once($CFG->libdir.'/grade/grade_outcome.php');
// Add outcomes if requested.
if ($hasoutcomes && $outcomes = grade_outcome::fetch_all_available($course->id)) {
$grade_items = array();
// Outcome grade_item.itemnumber start at 1000, there is nothing above outcomes.
$max_itemnumber = 999;
if ($items) {
......@@ -279,7 +293,7 @@ function edit_module_post_actions($moduleinfo, $course) {
$elname = 'outcome_'.$outcome->id;
if (property_exists($moduleinfo, $elname) and $moduleinfo->$elname) {
// So we have a request for new outcome grade item?
// Check if this is a new outcome grade item.
if ($items) {
$outcomeexists = false;
foreach($items as $item) {
......@@ -295,25 +309,25 @@ function edit_module_post_actions($moduleinfo, $course) {
$max_itemnumber++;
$outcome_item = new grade_item();
$outcome_item->courseid = $course->id;
$outcome_item->itemtype = 'mod';
$outcome_item->itemmodule = $moduleinfo->modulename;
$outcome_item->iteminstance = $moduleinfo->instance;
$outcome_item->itemnumber = $max_itemnumber;
$outcome_item->itemname = $outcome->fullname;
$outcome_item->outcomeid = $outcome->id;
$outcome_item->gradetype = GRADE_TYPE_SCALE;
$outcome_item->scaleid = $outcome->scaleid;
$outcome_item->insert();
// Move the new outcome into correct category and fix sortorder if needed.
if ($grade_item) {
$outcome_item->set_parent($grade_item->categoryid);
$outcome_item->move_after_sortorder($grade_item->sortorder);
$outcomeitem = new grade_item();
$outcomeitem->courseid = $course->id;
$outcomeitem->itemtype = 'mod';
$outcomeitem->itemmodule = $moduleinfo->modulename;
$outcomeitem->iteminstance = $moduleinfo->instance;
$outcomeitem->itemnumber = $max_itemnumber;
$outcomeitem->itemname = $outcome->fullname;
$outcomeitem->outcomeid = $outcome->id;
$outcomeitem->gradetype = GRADE_TYPE_SCALE;
$outcomeitem->scaleid = $outcome->scaleid;
$outcomeitem->insert();
if ($items) {
// Move the new outcome into the same category and immediately after the first grade item.
$item = reset($items);
$outcomeitem->set_parent($item->categoryid);
$outcomeitem->move_after_sortorder($item->sortorder);
} else if (isset($moduleinfo->gradecat)) {
$outcome_item->set_parent($moduleinfo->gradecat);
$outcomeitem->set_parent($moduleinfo->gradecat);
}
}
}
......@@ -354,7 +368,6 @@ function edit_module_post_actions($moduleinfo, $course) {
return $moduleinfo;
}
/**
* Set module info default values for the unset module attributs.
*
......@@ -702,34 +715,43 @@ function get_moduleinfo_data($cm, $course) {
}
}
if ($items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$data->modulename,
'iteminstance'=>$data->instance, 'courseid'=>$course->id))) {
$component = "mod_{$data->modulename}";
$items = grade_item::fetch_all([
'itemtype' => 'mod',
'itemmodule' => $data->modulename,
'iteminstance' => $data->instance,
'courseid' => $course->id,
]);
if ($items) {
// Add existing outcomes.
foreach ($items as $item) {
if (!empty($item->outcomeid)) {
$data->{'outcome_' . $item->outcomeid} = 1;
} else if (isset($item->gradepass)) {
$decimalpoints = $item->get_decimals();
$data->gradepass = format_float($item->gradepass, $decimalpoints);
$gradepassfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $item->itemnumber, 'gradepass');
$data->{$gradepassfieldname} = format_float($item->gradepass, $item->get_decimals());
}
}
// set category if present
$gradecat = false;
$gradecat = [];
foreach ($items as $item) {
if ($gradecat === false) {
$gradecat = $item->categoryid;
continue;
if (!isset($gradecat[$item->itemnumber])) {
$gradecat[$item->itemnumber] = $item->categoryid;
}
if ($gradecat != $item->categoryid) {
//mixed categories
$gradecat = false;
break;
if ($gradecat[$item->itemnumber] != $item->categoryid) {
// Mixed categories.
$gradecat[$item->itemnumber] = false;
}
}
if ($gradecat !== false) {
// do not set if mixed categories present
$data->gradecat = $gradecat;
foreach ($gradecat as $itemnumber => $cat) {
if ($cat !== false) {
$gradecatfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradecat');
// Do not set if mixed categories present.
$data->{$gradecatfieldname} = $cat;
}
}
}
return array($cm, $context, $module, $data, $cw);
......
This diff is collapsed.
......@@ -2065,7 +2065,7 @@ class core_course_externallib_testcase extends externallib_advanced_testcase {
$outcomegradeitem->cmid = 0;
$outcomegradeitem->courseid = $course->id;
$outcomegradeitem->aggregationcoef = 0;
$outcomegradeitem->itemnumber = 1; // The activity's original grade item will be 0.
$outcomegradeitem->itemnumber = 1000; // Outcomes start at 1000.
$outcomegradeitem->gradetype = GRADE_TYPE_SCALE;
$outcomegradeitem->scaleid = $outcome->scaleid;
$outcomegradeitem->insert();
......
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Helper class to fetch information about component grade items.
*
* @package core_grades
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types = 1);
namespace core_grades;
use code_grades\local\gradeitem\itemnumber_mapping;
use code_grades\local\gradeitem\advancedgrading_mapping;
/**
* Helper class to fetch information about component grade items.
*
* @package core_grades
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class component_gradeitems {
/**
* Get the gradeitems classname for the specific component.
*
* @param string $component The component to fetch the classname for
* @return string The composed classname
*/
protected static function get_component_classname(string $component): string {
return "{$component}\\grades\gradeitems";
}
/**
* Get the grade itemnumber mapping for a component.
*
* @param string $component The component that the grade item belongs to
* @return array
*/
public static function get_itemname_mapping_for_component(string $component): array {
$classname = "{$component}\\grades\gradeitems";
if (!class_exists($classname)) {
return [
0 => '',
];
}
if (!is_subclass_of($classname, 'core_grades\local\gradeitem\itemnumber_mapping')) {
throw new \coding_exception("The {$classname} class does not implement " . itemnumber_mapping::class);
}
return $classname::get_itemname_mapping_for_component();
}
/**
* Whether the named grading item exists.
*
* @param string $component
* @param string $itemname
* @return bool
*/
public static function is_valid_itemname(string $component, string $itemname): bool {
$items = self::get_itemname_mapping_for_component($component);
return array_search($itemname, $items) !== false;
}
/**
* Check whether the component class defines the advanced grading items.
*
* @param string $component The component to check
* @return bool
*/
public static function defines_advancedgrading_itemnames_for_component(string $component): bool {
return is_subclass_of(self::get_component_classname($component), 'core_grades\local\gradeitem\advancedgrading_mapping');
}
/**
* Get the list of advanced grading item names for the named component.
*
* @param string $component
* @return array
*/
public static function get_advancedgrading_itemnames_for_component(string $component): array {
$classname = self::get_component_classname($component);
if (!self::defines_advancedgrading_itemnames_for_component($component)) {
throw new \coding_exception("The {$classname} class does not implement " . advancedgrading_mapping::class);
}
return $classname::get_advancedgrading_itemnames();
}
/**
* Whether the named grading item name supports advanced grading.
*
* @param string $component
* @param string $itemname
* @return bool
*/
public static function is_advancedgrading_itemname(string $component, string $itemname): bool {
$gradingareas = self::get_advancedgrading_itemnames_for_component($component);
return array_search($itemname, $gradingareas) !== false;
}
/**
* Get the suffixed field name for an activity field mapped from its itemnumber.
*
* For legacy reasons, the first itemnumber has no suffix on field names.
*
* @param string $component The component that the grade item belongs to
* @param int $itemnumber The grade itemnumber
* @param string $fieldname The name of the field to be rewritten
* @return string The translated field name
*/
public static function get_field_name_for_itemnumber(string $component, int $itemnumber, string $fieldname): string {
$itemname = static::get_itemname_from_itemnumber($component, $itemnumber);
if ($itemname) {
return "{$fieldname}_{$itemname}";
}
return $fieldname;
}
/**
* Get the suffixed field name for an activity field mapped from its itemnumber.
*
* For legacy reasons, the first itemnumber has no suffix on field names.
*
* @param string $component The component that the grade item belongs to
* @param string $itemname The grade itemname
* @param string $fieldname The name of the field to be rewritten
* @return string The translated field name
*/
public static function get_field_name_for_itemname(string $component, string $itemname, string $fieldname): string {
if (empty($itemname)) {
return $fieldname;
}
$itemnumber = static::get_itemnumber_from_itemname($component, $itemname);
if ($itemnumber > 0) {
return "{$fieldname}_{$itemname}";
}
return $fieldname;
}
/**
* Get the itemname for an itemnumber.
*
* For legacy compatability when the itemnumber is 0, the itemname will always be empty.
*
* @param string $component The component that the grade item belongs to
* @param int $itemnumber The grade itemnumber
* @return int The grade itemnumber of the itemname
*/
public static function get_itemname_from_itemnumber(string $component, int $itemnumber): string {
if ($itemnumber === 0) {
return '';
}
$mappings = self::get_itemname_mapping_for_component($component);
if (isset($mappings[$itemnumber])) {
return $mappings[$itemnumber];
}
if ($itemnumber >= 1000) {
// An itemnumber >= 1000 belongs to an outcome.
return '';
}
throw new \coding_exception("Unknown itemnumber mapping for {$itemnumber} in {$component}");
}
/**
* Get the itemnumber for a item name.
*
* For legacy compatability when the itemname is empty, the itemnumber will always be 0.
*
* @param string $component The component that the grade item belongs to
* @param string $itemname The grade itemname
* @return int The grade itemname of the itemnumber
*/
public static function get_itemnumber_from_itemname(string $component, string $itemname): int {
if (empty($itemname)) {
return 0;
}
$mappings = self::get_itemname_mapping_for_component($component);
$flipped = array_flip($mappings);
if (isset($flipped[$itemname])) {
return $flipped[$itemname];
}
throw new \coding_exception("Unknown itemnumber mapping for {$itemname} in {$component}");
}
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Grade item, itemnumber mapping.
*
* @package core_grades
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types = 1);
namespace core_grades\local\gradeitem;
/**
* Grade item, itemnumber mapping.
*
* @package core_grades
* @copyright Andrew Nicols <andrew@nicols.co.uk>
*/
interface advancedgrading_mapping {
/**
* Get the list of advanced grading item names for this component.
*
* @return array
*/
public static function get_advancedgrading_itemnames(): array;
}
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Grade item, itemnumber mapping.
*
* @package core_grades
* @copyright Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
declare(strict_types = 1);
namespace core_grades\local\gradeitem;
/**
* Grade item, itemnumber mapping.
*
* @package core_grades
* @copyright Andrew Nicols <andrew@nicols.co.uk>
*/
interface itemnumber_mapping {
/**
* Get the grade item mapping of item number to item name.
*
* @return array
*/
public static function get_itemname_mapping_for_component(): array;
}
......@@ -24,6 +24,8 @@
defined('MOODLE_INTERNAL') || die();
use core_grades\component_gradeitems;
/**
* Factory method returning an instance of the grading manager
*
......@@ -288,14 +290,29 @@ class grading_manager {
public static function available_areas($component) {
global $CFG;
if (component_gradeitems::defines_advancedgrading_itemnames_for_component($component)) {
$result = [];
foreach (component_gradeitems::get_advancedgrading_itemnames_for_component($component) as $itemnumber => $itemname) {
$result[$itemname] = get_string("gradeitem:{$itemname}", $component);
}
return $result;
}
list($plugintype, $pluginname) = core_component::normalize_component($component);
if ($component === 'core_grading') {
return array();
} else if ($plugintype === 'mod') {
return plugin_callback('mod', $pluginname, 'grading', 'areas_list', null, array());
$callbackfunction = "grading_areas_list";
if (component_callback_exists($component, $callbackfunction)) {
debugging(
"Components supporting advanced grading should be updated to implement the component_gradeitems class",
DEBUG_DEVELOPER
);
return component_callback($component, $callbackfunction, [], []);
}
} else {
throw new coding_exception('Unsupported area location');
}
......
......@@ -217,7 +217,7 @@ Feature: We can set the grade to pass value
And I am on "Course 1" course homepage
And I follow "Test Forum 1"
And I follow "Edit settings"
And the field "Grade to pass" matches value "80"
And the field "Ratings > Grade to pass" matches value "80"
Scenario: Set a valid grade to pass for glossary activity
When I turn editing mode on
......
This diff is collapsed.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by