$predictionsprocessor = false;
}
+ if (!isset($data->contexts)) {
+ $data->contexts = null;
+ }
+
$model->update($data->enabled, $indicators, $timesplitting, $predictionsprocessor, $data->contexts);
redirect($returnurl);
}
And I reload the page
And I should see "Download ready" in the "Export all of my personal data" "table_row"
And I open the action menu in "Victim User 1" "table_row"
- And following "Download" should download between "1" and "140000" bytes
+ And following "Download" should download between "1" and "142000" bytes
And the following config values are set as admin:
| privacyrequestexpiry | 1 | tool_dataprivacy |
$predictionsprocessor = $this->model->predictionsprocessor;
}
- if ($contextids !== false) {
+ if ($contextids === false) {
+ $contextsstr = $this->model->contextids;
+ } else if (!$contextids) {
+ $contextsstr = null;
+ } else {
$contextsstr = json_encode($contextids);
// Reset the internal cache.
$this->contexts = null;
- } else {
- $contextsstr = $this->model->contextids;
}
if ($this->model->timesplitting !== $timesplittingid ||
{{> core_analytics/notification_styles}}
{{#body}}
- <div>
- {{{.}}}
- </div>
+ {{{.}}}
{{/body}}
<br/>
}
}}
-<style>
+{{! The styles defined here will be included in the Moodle web UI and in emails. Emails do not include Moodle
+stylesheets so we want these styles to be applied to emails. However, they will also be included in the Moodle web UI.
+We use the not(.dir-ltr):not(.dir-rtl) so that this style is not applied to the Moodle UI.
+Note that gmail strips out HTML styles which selector includes the caracters (), so the font-family rule
+is not applied in gmail.}}
+<head><style>
body:not(.dir-ltr):not(.dir-rtl) {
font-family: 'Open Sans', sans-serif;
}
-body:not(.dir-ltr):not(.dir-rtl) .btn-insight {
+.btn-insight {
color: #007bff;
background-color: transparent;
display: inline-block;
user-select: none;
border: 1px solid #007bff;
padding: .375rem .75rem;
- font-size: .9375rem;
line-height: 1.5;
border-radius: 0;
text-decoration: none;
cursor: pointer;
}
-</style>
+</style></head>
}
$content = $this->process_filephp_links($content); // Replace all calls to file.php by $@FILEPHP@$ in a normalised way
+
+ // Replace all calls to h5p/embed.php by $@H5PEMBED@$.
+ $content = $this->process_h5pembedphp_links($content);
+
$content = $this->encode_absolute_links($content); // Pass the content against all the found encoders
return $content;
return $content;
}
+ /**
+ * Replace all calls to /h5p/embed.php by $@H5PEMBED@$
+ * to allow restore the /h5p/embed.php url in
+ * other domains.
+ *
+ * @param string $content
+ * @return string
+ */
+ private function process_h5pembedphp_links($content) {
+ global $CFG;
+
+ // No /h5p/embed.php, nothing to convert.
+ if (strpos($content, '/h5p/embed.php') === false) {
+ return $content;
+ }
+
+ return str_replace($CFG->wwwroot.'/h5p/embed.php', '$@H5PEMBED@$', $content);
+ }
+
private function encode_absolute_links($content) {
foreach ($this->absolute_links_encoders as $classname => $methodname) {
$content = call_user_func(array($classname, $methodname), $content);
array('http://test.test/file.php?file=%2F2', 'http://test.test/file.php?file=%2F2'),
array('http://test.test/file.php?file=%2F1%2F1.jpg', '$@FILEPHP@$$@SLASH@$1.jpg'),
array('http://test.test/file.php?file=%2F1%2F%2F1.jpg', '$@FILEPHP@$$@SLASH@$$@SLASH@$1.jpg'),
+ array('http://test.test/h5p/embed.php?url=testurl', '$@H5PEMBED@$?url=testurl'),
);
}
return $cdata;
} else if (strlen($cdata) < 32) { // Impossible to have one link in 32cc
return $cdata; // (http://10.0.0.1/file.php/1/1.jpg, http://10.0.0.1/mod/url/view.php?id=)
- } else if (strpos($cdata, '$@FILEPHP@$') === false) { // No $@FILEPHP@$, nothing to convert
- return $cdata;
}
- if ($CFG->slasharguments) {
- $slash = '/';
- $forcedownload = '?forcedownload=1';
- } else {
- $slash = '%2F';
- $forcedownload = '&forcedownload=1';
- }
+ if (strpos($cdata, '$@FILEPHP@$') !== false) {
+ // We need to convert $@FILEPHP@$.
+ if ($CFG->slasharguments) {
+ $slash = '/';
+ $forcedownload = '?forcedownload=1';
+ } else {
+ $slash = '%2F';
+ $forcedownload = '&forcedownload=1';
+ }
+
+ // We have to remove trailing slashes, otherwise file URLs will be restored with an extra slash.
+ $basefileurl = rtrim(moodle_url::make_legacyfile_url($this->courseid, null)->out(true), $slash);
+ // Decode file.php calls.
+ $search = array ("$@FILEPHP@$");
+ $replace = array($basefileurl);
+ $result = str_replace($search, $replace, $cdata);
- // We have to remove trailing slashes, otherwise file URLs will be restored with an extra slash.
- $basefileurl = rtrim(moodle_url::make_legacyfile_url($this->courseid, null)->out(true), $slash);
- // Decode file.php calls
- $search = array ("$@FILEPHP@$");
- $replace = array($basefileurl);
- $result = str_replace($search, $replace, $cdata);
+ // Now $@SLASH@$ and $@FORCEDOWNLOAD@$ MDL-18799.
+ $search = array('$@SLASH@$', '$@FORCEDOWNLOAD@$');
+ $replace = array($slash, $forcedownload);
- // Now $@SLASH@$ and $@FORCEDOWNLOAD@$ MDL-18799
- $search = array('$@SLASH@$', '$@FORCEDOWNLOAD@$');
- $replace = array($slash, $forcedownload);
+ $cdata = str_replace($search, $replace, $result);
+ }
+
+ if (strpos($cdata, '$@H5PEMBED@$') !== false) {
+ // We need to convert $@H5PEMBED@$.
+ // Decode embed.php calls.
+ $cdata = str_replace('$@H5PEMBED@$', $CFG->wwwroot.'/h5p/embed.php', $cdata);
+ }
- return str_replace($search, $replace, $result);
+ return $cdata;
}
/**
"<a href='http://test.test/file.php?file=%2F1%2F1.jpg&forcedownload=1'>Image</a>",
false
),
+ array(
+ "<iframe src='$@H5PEMBED@$?url=testurl'></iframe>",
+ "<iframe src='http://test.test/h5p/embed.php?url=testurl'></iframe>",
+ true
+ ),
);
}
return Ajax.call([request])[0];
};
+ /**
+ * Get the list of users enrolled in this cmid.
+ *
+ * @param {Number} cmid Course Module from which the users will be obtained
+ * @returns {Promise} Promise containing a list of users
+ */
+ var getEnrolledUsersFromCourseModuleID = function(cmid) {
+ var request = {
+ methodname: 'core_course_get_enrolled_users_by_cmid',
+ args: {
+ cmid: cmid,
+ },
+ };
+
+ return Ajax.call([request])[0];
+ };
+
return {
getEnrolledCoursesByTimelineClassification: getEnrolledCoursesByTimelineClassification,
- getLastAccessedCourses: getLastAccessedCourses
+ getLastAccessedCourses: getLastAccessedCourses,
+ getUsersFromCourseModuleID: getEnrolledUsersFromCourseModuleID,
};
});
public static function get_recent_courses_returns() {
return new external_multiple_structure(course_summary_exporter::get_read_structure(), 'Courses');
}
+
+ /**
+ * Returns description of method parameters
+ *
+ * @return external_function_parameters
+ */
+ public static function get_enrolled_users_by_cmid_parameters() {
+ return new external_function_parameters([
+ 'cmid' => new external_value(PARAM_INT, 'id of the course module', VALUE_REQUIRED),
+ ]);
+ }
+
+ /**
+ * Get all users in a course for a given cmid.
+ *
+ * @param int $cmid Course Module id from which the users will be obtained
+ * @return array List of users
+ * @throws invalid_parameter_exception
+ */
+ public static function get_enrolled_users_by_cmid(int $cmid) {
+ $warnings = [];
+
+ [
+ 'cmid' => $cmid,
+ ] = self::validate_parameters(self::get_enrolled_users_by_cmid_parameters(), [
+ 'cmid' => $cmid,
+ ]);
+
+ list($course, $cm) = get_course_and_cm_from_cmid($cmid);
+ $coursecontext = context_course::instance($course->id);
+ self::validate_context($coursecontext);
+
+ $enrolledusers = get_enrolled_users($coursecontext);
+
+ $users = array_map(function ($user) {
+ $user->fullname = fullname($user);
+ return $user;
+ }, $enrolledusers);
+ sort($users);
+
+ return [
+ 'users' => $users,
+ 'warnings' => $warnings,
+ ];
+ }
+
+ /**
+ * Returns description of method result value
+ *
+ * @return external_description
+ */
+ public static function get_enrolled_users_by_cmid_returns() {
+ return new external_single_structure([
+ 'users' => new external_multiple_structure(self::user_description()),
+ 'warnings' => new external_warnings(),
+ ]);
+ }
+
+ /**
+ * Create user return value description.
+ *
+ * @return external_description
+ */
+ public static function user_description() {
+ $userfields = array(
+ 'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'),
+ 'fullname' => new external_value(PARAM_TEXT, 'The full name of the user', VALUE_OPTIONAL),
+ 'firstname' => new external_value(
+ core_user::get_property_type('firstname'),
+ 'The first name(s) of the user',
+ VALUE_OPTIONAL),
+ 'lastname' => new external_value(
+ core_user::get_property_type('lastname'),
+ 'The family name of the user',
+ VALUE_OPTIONAL),
+ );
+ return new external_single_structure($userfields);
+ }
}
defined('MOODLE_INTERNAL') || die;
+use \core_grades\component_gradeitems;
+
require_once($CFG->dirroot.'/course/lib.php');
/**
$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();
}
}
}
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) {
$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) {
$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);
}
}
}
return $moduleinfo;
}
-
/**
* Set module info default values for the unset module attributs.
*
}
}
- 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);
<?php
-require_once ($CFG->libdir.'/formslib.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/>.
+
+/**
+ * Moodleform.
+ *
+ * @package core_course
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once($CFG->libdir.'/formslib.php');
require_once($CFG->libdir.'/completionlib.php');
require_once($CFG->libdir.'/gradelib.php');
require_once($CFG->libdir.'/plagiarismlib.php');
+use core_grades\component_gradeitems;
+
/**
- * This class adds extra methods to form wrapper specific to be used for module
- * add / update forms mod/{modname}/mod_form.php replaced deprecated mod/{modname}/mod.html
+ * This class adds extra methods to form wrapper specific to be used for module add / update forms
+ * mod/{modname}/mod_form.php replaced deprecated mod/{modname}/mod.html Moodleform.
+ *
+ * @package core_course
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
*/
abstract class moodleform_mod extends moodleform {
/** Current data */
/** @var object The course format of the current course. */
protected $courseformat;
+ /** @var string Whether this is graded or rated. */
+ private $gradedorrated = null;
+
public function __construct($current, $section, $cm, $course) {
global $CFG;
if ($id = $mform->getElementValue('update')) {
$modulename = $mform->getElementValue('modulename');
$instance = $mform->getElementValue('instance');
+ $component = "mod_{$modulename}";
if ($this->_features->gradecat) {
- $gradecat = false;
- if (!empty($CFG->enableoutcomes) and $this->_features->outcomes) {
- $outcomes = grade_outcome::fetch_all_available($COURSE->id);
- if (!empty($outcomes)) {
- $gradecat = true;
- }
- }
-
$hasgradeitems = false;
- $items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename,'iteminstance'=>$instance, 'courseid'=>$COURSE->id));
+ $items = grade_item::fetch_all([
+ 'itemtype' => 'mod',
+ 'itemmodule' => $modulename,
+ 'iteminstance' => $instance,
+ 'courseid' => $COURSE->id,
+ ]);
+
+ $gradecategories = [];
+ $removecategories = [];
//will be no items if, for example, this activity supports ratings but rating aggregate type == no ratings
if (!empty($items)) {
foreach ($items as $item) {
}
foreach ($items as $item) {
- if (is_bool($gradecat)) {
- $gradecat = $item->categoryid;
- continue;
- }
- if ($gradecat != $item->categoryid) {
- //mixed categories
- $gradecat = false;
- break;
+ $gradecatfieldname = component_gradeitems::get_field_name_for_itemnumber(
+ $component,
+ $item->itemnumber,
+ 'gradecat'
+ );
+
+ if (!isset($gradecategories[$gradecatfieldname])) {
+ $gradecategories[$gradecatfieldname] = $item->categoryid;
+ } else if ($gradecategories[$gradecatfieldname] != $item->categoryid) {
+ $removecategories[$gradecatfieldname] = true;
}
}
}
- if (!$hasgradeitems && $mform->elementExists('gradepass')) {
- // Remove form element 'Grade to pass' since there are no grade items (when rating not selected).
- $mform->removeElement('gradepass');
- }
-
- if ($gradecat === false) {
- // items and outcomes in different categories - remove the option
- // TODO: add a "Mixed categories" text instead of removing elements with no explanation
- if ($mform->elementExists('gradecat')) {
- $mform->removeElement('gradecat');
- if ($this->_features->rating && !$mform->elementExists('gradepass')) {
- //if supports ratings then the max grade dropdown wasnt added so the grade box can be removed entirely
- $mform->removeElement('modstandardgrade');
- }
+ foreach ($removecategories as $toremove) {
+ if ($mform->elementExists($toremove)) {
+ $mform->removeElement($toremove);
}
}
}
if ($COURSE->groupmodeforce) {
if ($mform->elementExists('groupmode')) {
- $mform->hardFreeze('groupmode'); // groupmode can not be changed if forced from course settings
+ // The groupmode can not be changed if forced from course settings.
+ $mform->hardFreeze('groupmode');
}
}
- // Don't disable/remove groupingid if it is currently set to something,
- // otherwise you cannot turn it off at same time as turning off other
- // option (MDL-30764)
+ // Don't disable/remove groupingid if it is currently set to something, otherwise you cannot turn it off at same
+ // time as turning off other option (MDL-30764).
if (empty($this->_cm) || !$this->_cm->groupingid) {
if ($mform->elementExists('groupmode') && empty($COURSE->groupmodeforce)) {
$mform->hideIf('groupingid', 'groupmode', 'eq', NOGROUPS);
}
}
- // Ratings: Don't let them select an aggregate type without selecting a scale.
- // If the user has selected to use ratings but has not chosen a scale or set max points then the form is
- // invalid. If ratings have been selected then the user must select either a scale or max points.
- // This matches (horrible) logic in data_preprocessing.
- if (isset($data['assessed']) && $data['assessed'] > 0 && empty($data['scale'])) {
- $errors['assessed'] = get_string('scaleselectionrequired', 'rating');
- }
-
- // Check that the grade pass is a valid number.
- $gradepassvalid = false;
- if (isset($data['gradepass'])) {
- if (unformat_float($data['gradepass'], true) === false) {
- $errors['gradepass'] = get_string('err_numeric', 'form');
- } else {
- $gradepassvalid = true;
+ $component = "mod_{$this->_modname}";
+ $itemnames = component_gradeitems::get_itemname_mapping_for_component($component);
+ foreach ($itemnames as $itemnumber => $itemname) {
+ $gradefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'grade');
+ $gradepassfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradepass');
+ $assessedfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'assessed');
+ $scalefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'scale');
+
+ // Ratings: Don't let them select an aggregate type without selecting a scale.
+ // If the user has selected to use ratings but has not chosen a scale or set max points then the form is
+ // invalid. If ratings have been selected then the user must select either a scale or max points.
+ // This matches (horrible) logic in data_preprocessing.
+ if (isset($data[$assessedfieldname]) && $data[$assessedfieldname] > 0 && empty($data[$scalefieldname])) {
+ $errors[$assessedfieldname] = get_string('scaleselectionrequired', 'rating');
}
- }
- // Grade to pass: ensure that the grade to pass is valid for points and scales.
- // If we are working with a scale, convert into a positive number for validation.
- if ($gradepassvalid && isset($data['gradepass']) && (!empty($data['grade']) || !empty($data['scale']))) {
- $scale = !empty($data['grade']) ? $data['grade'] : $data['scale'];
- if ($scale < 0) {
- $scalevalues = $DB->get_record('scale', array('id' => -$scale));
- $grade = count(explode(',', $scalevalues->scale));
- } else {
- $grade = $scale;
+ // Check that the grade pass is a valid number.
+ $gradepassvalid = false;
+ if (isset($data[$gradepassfieldname])) {
+ if (unformat_float($data[$gradepassfieldname], true) === false) {
+ $errors[$gradepassfieldname] = get_string('err_numeric', 'form');
+ } else {
+ $gradepassvalid = true;
+ }
}
- if (unformat_float($data['gradepass']) > $grade) {
- $errors['gradepass'] = get_string('gradepassgreaterthangrade', 'grades', $grade);
+
+ // Grade to pass: ensure that the grade to pass is valid for points and scales.
+ // If we are working with a scale, convert into a positive number for validation.
+ if ($gradepassvalid && isset($data[$gradepassfieldname]) && (!empty($data[$gradefieldname]) || !empty($data[$scalefieldname]))) {
+ $scale = !empty($data[$gradefieldname]) ? $data[$gradefieldname] : $data[$scalefieldname];
+ if ($scale < 0) {
+ $scalevalues = $DB->get_record('scale', array('id' => -$scale));
+ $grade = count(explode(',', $scalevalues->scale));
+ } else {
+ $grade = $scale;
+ }
+ if (unformat_float($data[$gradepassfieldname]) > $grade) {
+ $errors[$gradepassfieldname] = get_string('gradepassgreaterthangrade', 'grades', $grade);
+ }
}
}
/**
* Adds all the standard elements to a form to edit the settings for an activity module.
*/
- function standard_coursemodule_elements(){
+ protected function standard_coursemodule_elements() {
global $COURSE, $CFG, $DB;
$mform =& $this->_form;
}
}
-
if ($this->_features->rating) {
- require_once($CFG->dirroot.'/rating/lib.php');
- $rm = new rating_manager();
-
- $mform->addElement('header', 'modstandardratings', get_string('ratings', 'rating'));
-
- $permission=CAP_ALLOW;
- $rolenamestring = null;
- $isupdate = false;
- if (!empty($this->_cm)) {
- $isupdate = true;
- $context = context_module::instance($this->_cm->id);
-
- $rolenames = get_role_names_with_caps_in_context($context, array('moodle/rating:rate', 'mod/'.$this->_cm->modname.':rate'));
- $rolenamestring = implode(', ', $rolenames);
- } else {
- $rolenamestring = get_string('capabilitychecknotavailable','rating');
- }
- $mform->addElement('static', 'rolewarning', get_string('rolewarning','rating'), $rolenamestring);
- $mform->addHelpButton('rolewarning', 'rolewarning', 'rating');
-
- $mform->addElement('select', 'assessed', get_string('aggregatetype', 'rating') , $rm->get_aggregate_types());
- $mform->setDefault('assessed', 0);
- $mform->addHelpButton('assessed', 'aggregatetype', 'rating');
-
- $gradeoptions = array('isupdate' => $isupdate,
- 'currentgrade' => false,
- 'hasgrades' => false,
- 'canrescale' => $this->_features->canrescale,
- 'useratings' => $this->_features->rating);
- if ($isupdate) {
- $gradeitem = grade_item::fetch(array('itemtype' => 'mod',
- 'itemmodule' => $this->_cm->modname,
- 'iteminstance' => $this->_cm->instance,
- 'itemnumber' => 0,
- 'courseid' => $COURSE->id));
- if ($gradeitem) {
- $gradeoptions['currentgrade'] = $gradeitem->grademax;
- $gradeoptions['currentgradetype'] = $gradeitem->gradetype;
- $gradeoptions['currentscaleid'] = $gradeitem->scaleid;
- $gradeoptions['hasgrades'] = $gradeitem->has_grades();
- }
- }
- $mform->addElement('modgrade', 'scale', get_string('scale'), $gradeoptions);
- $mform->hideIf('scale', 'assessed', 'eq', 0);
- $mform->addHelpButton('scale', 'modgrade', 'grades');
- $mform->setDefault('scale', $CFG->gradepointdefault);
-
- $mform->addElement('checkbox', 'ratingtime', get_string('ratingtime', 'rating'));
- $mform->hideIf('ratingtime', 'assessed', 'eq', 0);
-
- $mform->addElement('date_time_selector', 'assesstimestart', get_string('from'));
- $mform->hideIf('assesstimestart', 'assessed', 'eq', 0);
- $mform->disabledIf('assesstimestart', 'ratingtime');
-
- $mform->addElement('date_time_selector', 'assesstimefinish', get_string('to'));
- $mform->hideIf('assesstimefinish', 'assessed', 'eq', 0);
- $mform->disabledIf('assesstimefinish', 'ratingtime');
+ $this->add_rating_settings($mform, 0);
}
- //doing this here means splitting up the grade related settings on the lesson settings page
- //$this->standard_grading_coursemodule_elements();
-
$mform->addElement('header', 'modstandardelshdr', get_string('modstandardels', 'form'));
$section = get_fast_modinfo($COURSE)->get_section_info($this->_section);
$this->plugin_extend_coursemodule_standard_elements();
}
+ /**
+ * Add rating settings.
+ *
+ * @param moodleform_mod $mform
+ * @param int $itemnumber
+ */
+ protected function add_rating_settings($mform, int $itemnumber) {
+ global $CFG, $COURSE;
+
+ if ($this->gradedorrated && $this->gradedorrated !== 'rated') {
+ return;
+ }
+ $this->gradedorrated = 'rated';
+
+ require_once("{$CFG->dirroot}/rating/lib.php");
+ $rm = new rating_manager();
+
+ $component = "mod_{$this->_modname}";
+ $gradecatfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradecat');
+ $gradepassfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradepass');
+ $assessedfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'assessed');
+ $scalefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'scale');
+
+ $mform->addElement('header', 'modstandardratings', get_string('ratings', 'rating'));
+
+ $isupdate = !empty($this->_cm);
+
+ $rolenamestring = null;
+ if ($isupdate) {
+ $context = context_module::instance($this->_cm->id);
+ $capabilities = ['moodle/rating:rate', "mod/{$this->_cm->modname}:rate"];
+ $rolenames = get_role_names_with_caps_in_context($context, $capabilities);
+ $rolenamestring = implode(', ', $rolenames);
+ } else {
+ $rolenamestring = get_string('capabilitychecknotavailable', 'rating');
+ }
+
+ $mform->addElement('static', 'rolewarning', get_string('rolewarning', 'rating'), $rolenamestring);
+ $mform->addHelpButton('rolewarning', 'rolewarning', 'rating');
+
+ $mform->addElement('select', $assessedfieldname, get_string('aggregatetype', 'rating') , $rm->get_aggregate_types());
+ $mform->setDefault($assessedfieldname, 0);
+ $mform->addHelpButton($assessedfieldname, 'aggregatetype', 'rating');
+
+ $gradeoptions = [
+ 'isupdate' => $isupdate,
+ 'currentgrade' => false,
+ 'hasgrades' => false,
+ 'canrescale' => false,
+ 'useratings' => true,
+ ];
+ if ($isupdate) {
+ $gradeitem = grade_item::fetch([
+ 'itemtype' => 'mod',
+ 'itemmodule' => $this->_cm->modname,
+ 'iteminstance' => $this->_cm->instance,
+ 'itemnumber' => $itemnumber,
+ 'courseid' => $COURSE->id,
+ ]);
+ if ($gradeitem) {
+ $gradeoptions['currentgrade'] = $gradeitem->grademax;
+ $gradeoptions['currentgradetype'] = $gradeitem->gradetype;
+ $gradeoptions['currentscaleid'] = $gradeitem->scaleid;
+ $gradeoptions['hasgrades'] = $gradeitem->has_grades();
+ }
+ }
+
+ $mform->addElement('modgrade', $scalefieldname, get_string('scale'), $gradeoptions);
+ $mform->hideIf($scalefieldname, $assessedfieldname, 'eq', 0);
+ $mform->addHelpButton($scalefieldname, 'modgrade', 'grades');
+ $mform->setDefault($scalefieldname, $CFG->gradepointdefault);
+
+ $mform->addElement('checkbox', 'ratingtime', get_string('ratingtime', 'rating'));
+ $mform->hideIf('ratingtime', $assessedfieldname, 'eq', 0);
+
+ $mform->addElement('date_time_selector', 'assesstimestart', get_string('from'));
+ $mform->hideIf('assesstimestart', $assessedfieldname, 'eq', 0);
+ $mform->hideIf('assesstimestart', 'ratingtime');
+
+ $mform->addElement('date_time_selector', 'assesstimefinish', get_string('to'));
+ $mform->hideIf('assesstimefinish', $assessedfieldname, 'eq', 0);
+ $mform->hideIf('assesstimefinish', 'ratingtime');
+
+ if ($this->_features->gradecat) {
+ $mform->addElement(
+ 'select',
+ $gradecatfieldname,
+ get_string('gradecategoryonmodform', 'grades'),
+ grade_get_categories_menu($COURSE->id, $this->_outcomesused)
+ );
+ $mform->addHelpButton($gradecatfieldname, 'gradecategoryonmodform', 'grades');
+ $mform->hideIf($gradecatfieldname, $assessedfieldname, 'eq', 0);
+ $mform->hideIf($gradecatfieldname, "{$scalefieldname}[modgrade_type]", 'eq', 'none');
+ }
+
+ // Grade to pass.
+ $mform->addElement('text', $gradepassfieldname, get_string('gradepass', 'grades'));
+ $mform->addHelpButton($gradepassfieldname, 'gradepass', 'grades');
+ $mform->setDefault($gradepassfieldname, '');
+ $mform->setType($gradepassfieldname, PARAM_RAW);
+ $mform->hideIf($gradepassfieldname, $assessedfieldname, 'eq', '0');
+ $mform->hideIf($gradepassfieldname, "{$scalefieldname}[modgrade_type]", 'eq', 'none');
+ }
+
/**
* Plugins can extend the coursemodule settings form.
*/
public function standard_grading_coursemodule_elements() {
global $COURSE, $CFG;
+
+ if ($this->gradedorrated && $this->gradedorrated !== 'graded') {
+ return;
+ }
+ if ($this->_features->rating) {
+ return;
+ }
+ $this->gradedorrated = 'graded';
+
+ $itemnumber = 0;
+ $component = "mod_{$this->_modname}";
+ $gradefieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'grade');
+ $gradecatfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradecat');
+ $gradepassfieldname = component_gradeitems::get_field_name_for_itemnumber($component, $itemnumber, 'gradepass');
+
$mform =& $this->_form;
$isupdate = !empty($this->_cm);
$gradeoptions = array('isupdate' => $isupdate,
'useratings' => $this->_features->rating);
if ($this->_features->hasgrades) {
-
- if (!$this->_features->rating || $this->_features->gradecat) {
+ if ($this->_features->gradecat) {
$mform->addElement('header', 'modstandardgrade', get_string('grade'));
}
//if supports grades and grades arent being handled via ratings
- if (!$this->_features->rating) {
-
- if ($isupdate) {
- $gradeitem = grade_item::fetch(array('itemtype' => 'mod',
- 'itemmodule' => $this->_cm->modname,
- 'iteminstance' => $this->_cm->instance,
- 'itemnumber' => 0,
- 'courseid' => $COURSE->id));
- if ($gradeitem) {
- $gradeoptions['currentgrade'] = $gradeitem->grademax;
- $gradeoptions['currentgradetype'] = $gradeitem->gradetype;
- $gradeoptions['currentscaleid'] = $gradeitem->scaleid;
- $gradeoptions['hasgrades'] = $gradeitem->has_grades();
- }
+ if ($isupdate) {
+ $gradeitem = grade_item::fetch(array('itemtype' => 'mod',
+ 'itemmodule' => $this->_cm->modname,
+ 'iteminstance' => $this->_cm->instance,
+ 'itemnumber' => 0,
+ 'courseid' => $COURSE->id));
+ if ($gradeitem) {
+ $gradeoptions['currentgrade'] = $gradeitem->grademax;
+ $gradeoptions['currentgradetype'] = $gradeitem->gradetype;
+ $gradeoptions['currentscaleid'] = $gradeitem->scaleid;
+ $gradeoptions['hasgrades'] = $gradeitem->has_grades();
}
- $mform->addElement('modgrade', 'grade', get_string('grade'), $gradeoptions);
- $mform->addHelpButton('grade', 'modgrade', 'grades');
- $mform->setDefault('grade', $CFG->gradepointdefault);
}
+ $mform->addElement('modgrade', $gradefieldname, get_string('grade'), $gradeoptions);
+ $mform->addHelpButton($gradefieldname, 'modgrade', 'grades');
+ $mform->setDefault($gradefieldname, $CFG->gradepointdefault);
if ($this->_features->advancedgrading
and !empty($this->current->_advancedgradingdata['methods'])
$mform->addElement('select', 'advancedgradingmethod_'.$areaname,
get_string('gradingmethod', 'core_grading'), $this->current->_advancedgradingdata['methods']);
$mform->addHelpButton('advancedgradingmethod_'.$areaname, 'gradingmethod', 'core_grading');
- if (!$this->_features->rating) {
- $mform->hideIf('advancedgradingmethod_'.$areaname, 'grade[modgrade_type]', 'eq', 'none');
- }
+ $mform->hideIf('advancedgradingmethod_'.$areaname, "{$gradefieldname}[modgrade_type]", 'eq', 'none');
} else {
// the module defines multiple gradable areas, display a selector
}
if ($this->_features->gradecat) {
- $mform->addElement('select', 'gradecat',
+ $mform->addElement('select', $gradecatfieldname,
get_string('gradecategoryonmodform', 'grades'),
grade_get_categories_menu($COURSE->id, $this->_outcomesused));
- $mform->addHelpButton('gradecat', 'gradecategoryonmodform', 'grades');
- if (!$this->_features->rating) {
- $mform->hideIf('gradecat', 'grade[modgrade_type]', 'eq', 'none');
- }
+ $mform->addHelpButton($gradecatfieldname, 'gradecategoryonmodform', 'grades');
+ $mform->hideIf($gradecatfieldname, "{$gradefieldname}[modgrade_type]", 'eq', 'none');
}
// Grade to pass.
- $mform->addElement('text', 'gradepass', get_string('gradepass', 'grades'));
- $mform->addHelpButton('gradepass', 'gradepass', 'grades');
- $mform->setDefault('gradepass', '');
- $mform->setType('gradepass', PARAM_RAW);
- if (!$this->_features->rating) {
- $mform->hideIf('gradepass', 'grade[modgrade_type]', 'eq', 'none');
- } else {
- $mform->hideIf('gradepass', 'assessed', 'eq', '0');
- }
+ $mform->addElement('text', $gradepassfieldname, get_string($gradepassfieldname, 'grades'));
+ $mform->addHelpButton($gradepassfieldname, $gradepassfieldname, 'grades');
+ $mform->setDefault($gradepassfieldname, '');
+ $mform->setType($gradepassfieldname, PARAM_RAW);
+ $mform->hideIf($gradepassfieldname, "{$gradefieldname}[modgrade_type]", 'eq', 'none');
}
}
return $data;
}
}
-
-
$moduleinfo->blockperiod = 60*60*24;
$moduleinfo->blockafter = 10;
$moduleinfo->warnafter = 5;
+
+ // Grading of whole forum settings.
+ $moduleinfo->grade_forum = 0;
}
/**
$moduleinfo->blockperiod = 60*60*24;
$moduleinfo->blockafter = 10;
$moduleinfo->warnafter = 5;
+
+ // Grading of whole forum settings.
+ $moduleinfo->grade_forum = 0;
}
/**
$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();
$this->assertCount(2, $result['courses']);
// Check default filters.
- $this->assertCount(3, $result['courses'][0]['filters']);
- $this->assertCount(3, $result['courses'][1]['filters']);
+ $this->assertCount(4, $result['courses'][0]['filters']);
+ $this->assertCount(4, $result['courses'][1]['filters']);
$result = core_course_external::get_courses_by_field('category', $category1->id);
$result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result);
// Check default filters.
$filters = $result['courses'][0]['filters'];
- $this->assertCount(3, $filters);
+ $this->assertCount(4, $filters);
$found = false;
foreach ($filters as $filter) {
if ($filter['filter'] == 'mediaplugin' and $filter['localstate'] == TEXTFILTER_OFF) {
$this->assertCount(1, $result);
$this->assertEquals($courses[0]->id, array_shift($result)->id);
}
+
+ /**
+ * Test get enrolled users by cmid function.
+ */
+ public function test_get_enrolled_users_by_cmid() {
+ $this->resetAfterTest(true);
+
+ $user1 = self::getDataGenerator()->create_user();
+ $user2 = self::getDataGenerator()->create_user();
+
+ // Set the first created user to the test user.
+ self::setUser($user1);
+
+ // Create course to add the module.
+ $course1 = self::getDataGenerator()->create_course();
+
+ // Forum with tracking off.
+ $record = new stdClass();
+ $record->course = $course1->id;
+ $forum1 = self::getDataGenerator()->create_module('forum', $record);
+
+ // Following lines enrol and assign default role id to the users.
+ $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
+ $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
+
+ // Create what we expect to be returned when querying the course module.
+ $expectedusers = array(
+ 'users' => array(),
+ 'warnings' => array(),
+ );
+
+ $expectedusers['users'][0] = [
+ 'id' => $user1->id,
+ 'fullname' => fullname($user1),
+ 'firstname' => $user1->firstname,
+ 'lastname' => $user1->lastname,
+ ];
+ $expectedusers['users'][1] = [
+ 'id' => $user2->id,
+ 'fullname' => fullname($user2),
+ 'firstname' => $user2->firstname,
+ 'lastname' => $user2->lastname,
+ ];
+
+ // Test getting the users in a given context.
+ $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid);
+ $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users);
+
+ $this->assertEquals(2, count($users['users']));
+ $this->assertEquals($expectedusers, $users);
+ }
}
--- /dev/null
+<?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/>.
+
+/**
+ * Display H5P active by default
+ *
+ * @package filter_displayh5p
+ * @copyright 2019 Amaia Anabitarte <amaia@moodle.com>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Enable displayh5p filter by default to render H5P contents.
+ * @throws coding_exception
+ */
+function xmldb_filter_displayh5p_install() {
+ global $CFG;
+
+ require_once("$CFG->libdir/filterlib.php");
+
+ // Display H5P filter should be enabled by default because we need this filter for H5P atto button to work.
+ filter_set_global_state('displayh5p', TEXTFILTER_ON, -1);
+}
* @return string
*/
public function filter($text, array $options = array()) {
+ global $CFG;
if (!is_string($text) or empty($text)) {
// Non string data can not be filtered anyway.
return $text;
}
- if (stripos($text, 'http') === false) {
+ // We are trying to minimize performance impact checking there's some H5P related URL.
+ $h5purl = '(http[^ &<]*h5p)';
+ if (!preg_match($h5purl, $text)) {
return $text;
}
$allowedsources = get_config('filter_displayh5p', 'allowedsources');
$allowedsources = array_filter(array_map('trim', explode("\n", $allowedsources)));
- if (empty($allowedsources)) {
- return $text;
- }
+
+ $localsource = '('.preg_quote($CFG->wwwroot).'/[^ &<]*\.h5p([?][^ <]*)?[^ &<]*)';
+ $allowedsources[] = $localsource;
$params = array(
- 'tagbegin' => "<iframe src=",
- 'tagend' => "</iframe>"
+ 'tagbegin' => '<iframe src="',
+ 'tagend' => '</iframe>'
);
+ $specialchars = ['*', '?', '&', '[^<]'];
+ $escapedspecialchars = ['[^.]+', '\?', '&', '[^<]*'];
+ $h5pcontents = array();
+
+ // Check all allowed sources.
foreach ($allowedsources as $source) {
// It is needed to add "/embed" at the end of URLs like https:://*.h5p.com/content/12345 (H5P.com).
$params['urlmodifier'] = '';
- if (!(stripos($source, 'embed'))) {
- $params['urlmodifier'] = '/embed';
+
+ if (($source == $localsource)) {
+ $params['tagbegin'] = '<iframe src="'.$CFG->wwwroot.'/h5p/embed.php?url=';
+ $ultimatepattern = '#'.$source.'#';
+ } else {
+ if (!stripos($source, 'embed')) {
+ $params['urlmodifier'] = '/embed';
+ }
+ // Convert special chars.
+ $sourceid = str_replace('[id]', '[0-9]+', $source);
+ $escapechars = str_replace($specialchars, $escapedspecialchars, $sourceid);
+ $ultimatepattern = '#(' . $escapechars . ')#';
}
- // Convert special chars.
- $specialchars = ['*', '?', '&'];
- $escapedspecialchars = ['[^.]+', '\?', '&'];
- $sourceid = str_replace('[id]', '[0-9]+', $source);
- $escapechars = str_replace($specialchars, $escapedspecialchars, $sourceid);
- $ultimatepattern = '#(' . $escapechars . ')#';
+ // Improve performance creating filterobjects only when needed.
+ if (!preg_match($ultimatepattern, $text)) {
+ continue;
+ }
$h5pcontenturl = new filterobject($source, null, null, false,
- false, null, [$this, 'filterobject_prepare_replacement_callback'], $params);
+ false, null, [$this, 'filterobject_prepare_replacement_callback'], $params);
$h5pcontenturl->workregexp = $ultimatepattern;
$h5pcontents[] = $h5pcontenturl;
}
- return filter_phrases($text, $h5pcontents, null, null, false, true);
+ if (empty($h5pcontents)) {
+ // No matches to deal with.
+ return $text;
+ }
+
+ $result = filter_phrases($text, $h5pcontents, null, null, false, true);
+
+ // Encoding H5P file URLs.
+ // embed.php page is requesting a PARAM_LOCALURL url parameter, so for files/directories use non-alphanumeric
+ // characters, we need to encode the parameter. Fetch url parameter added to embed.php and encode the whole url.
+ $localurl = '#\?url=([^" <]*[\/]+[^" <]*\.h5p)([?][^"]*)?#';
+ $result = preg_replace_callback($localurl,
+ function ($matches) {
+ $baseurl = rawurlencode($matches[1]);
+ // Deal with possible parameters in the url link.
+ if (!empty($matches[2])) {
+ $match = explode('?', $matches[2]);
+ if (!empty($match[1])) {
+ $baseurl = $baseurl."&".$match[1];
+ }
+ }
+ return "?url=".$baseurl;
+ }, $result);
+
+ return $result;
}
/**
* @return array [$hreftagbegin, $hreftagend, $replacementphrase] for filterobject.
*/
public function filterobject_prepare_replacement_callback($tagbegin, $tagend, $urlmodifier) {
-
$sourceurl = "$1";
if ($urlmodifier !== "") {
$sourceurl .= $urlmodifier;
}
- $h5piframesrc = "\"".$sourceurl."\" width=\"100%\" height=\"637\" allowfullscreen=\"allowfullscreen\" style=\"border: 0;\">";
+ $h5piframesrc = $sourceurl.
+ '" class="h5p-iframe" style="height:230px; width: 100%; border: 0;" allowfullscreen="allowfullscreen">';
// We want to request the resizing script only once.
if (self::$loadresizerjs) {
"https://h5p.org/h5p/embed/[id]\nhttps://*.h5p.com/content/[id]/embed\nhttps://*.h5p.com/content/[id]
\nhttps://generic.wordpress.soton.ac.uk/altc/wp-admin/admin-ajax.php?action=h5p_embed&id=[id]",
'filter_displayh5p');
- // Enable display h5p filter at top level.
- filter_set_global_state('displayh5p', TEXTFILTER_ON);
}
/**
* @return array
*/
public function texts_provider() {
+ global $CFG;
+
return [
["http:://example.com", "#http:://example.com#"],
["http://google.es/h5p/embed/3425234", "#http://google.es/h5p/embed/3425234#"],
["https://generic.wordpress.soton.ac.uk/altc/wp-admin/admin-ajax.php?action=h5p_embed&id=13",
"#<iframe src=\"https://generic.wordpress.soton.ac.uk/altc/wp-admin/admin-ajax.php\?action=h5p_embed\&\;id=13\"[^>]+?>#"],
["https://h5p.org/h5p/embed/547225 another content in the same page https://moodle.h5p.com/content/1290729733828858779/embed",
- "#<iframe src=\"https://h5p.org/h5p/embed/547225\"[^>]+?>((?!<iframe).)*<iframe src=\"https://moodle.h5p.com/content/1290729733828858779/embed\"[^>]+?>#"]
+ "#<iframe src=\"https://h5p.org/h5p/embed/547225\"[^>]+?>((?!<iframe).)*<iframe src=\"https://moodle.h5p.com/content/1290729733828858779/embed\"[^>]+?>#"],
+ [$CFG->wwwroot."/pluginfile.php/5/user/private/interactive-video.h5p?export=1&embed=1",
+ "#<iframe src=\"{$CFG->wwwroot}/h5p/embed.php\?url=".rawurlencode("{$CFG->wwwroot}/pluginfile.php/5/user/private/interactive-video.h5p").
+ "&export=1&embed=1\"[^>]*?></iframe>#"],
+ [$CFG->wwwroot."/pluginfile.php/5/user/private/accordion-6-7138%20%281%29.h5p.h5p",
+ "#<iframe src=\"{$CFG->wwwroot}/h5p/embed.php\?url=".rawurlencode("{$CFG->wwwroot}/pluginfile.php/5/user/private/accordion-6-7138%20%281%29.h5p.h5p").
+ "\"[^>]*?></iframe>#"]
];
}
}
\ No newline at end of file
--- /dev/null
+// 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/>.
+
+/**
+ * Repository for simple direct grading panel.
+ *
+ * @module core_grades/grades/grader/gradingpanel/repository
+ * @package core_grades
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Normalise a resultset for consumption by the grader.
+ *
+ * @param {Object} result The result returned from a grading web service
+ * @return {Object}
+ */
+export const normaliseResult = result => {
+ return {
+ result,
+ failed: !!result.warnings.length,
+ success: !result.warnings.length,
+ error: null,
+ };
+};
+
+/**
+ * Return the resultset used to describe an invalid result.
+ *
+ * @return {Object}
+ */
+export const invalidResult = () => {
+ return {
+ success: false,
+ failed: false,
+ result: {},
+ error: null,
+ };
+};
+
+/**
+ * Return the resultset used to describe a failed update.
+ *
+ * @param {Object} error
+ * @return {Object}
+ */
+export const failedUpdate = error => {
+ return {
+ success: false,
+ failed: true,
+ result: {},
+ error,
+ };
+};
--- /dev/null
+// 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/>.
+
+/**
+ * Grading panel for simple direct grading.
+ *
+ * @module core_grades/grades/grader/gradingpanel/point
+ * @package core_grades
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {saveGrade, fetchGrade} from './repository';
+// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()
+import jQuery from 'jquery';
+import {invalidResult} from './normalise';
+
+/**
+ * Fetch the current grade for a user.
+ *
+ * @param {String} component
+ * @param {Number} context
+ * @param {String} itemname
+ * @param {Number} userId
+ * @param {Element} rootNode
+ * @return {Object}
+ */
+export const fetchCurrentGrade = (...args) => fetchGrade('point')(...args);
+
+/**
+ * Store a new grade for a user.
+ *
+ * @param {String} component
+ * @param {Number} context
+ * @param {String} itemname
+ * @param {Number} userId
+ * @param {Element} rootNode
+ * @return {Object}
+ */
+export const storeCurrentGrade = async(component, context, itemname, userId, rootNode) => {
+ const form = rootNode.querySelector('form');
+ const grade = form.querySelector('input[name="grade"]');
+
+ if (!grade.checkValidity() || !grade.value.trim()) {
+ return invalidResult;
+ }
+
+ return await saveGrade('point')(component, context, itemname, userId, jQuery(form).serialize());
+};
--- /dev/null
+// 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/>.
+
+/**
+ * Repository for simple direct grading panel.
+ *
+ * @module core_grades/grades/grader/gradingpanel/repository
+ * @package core_grades
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+import {call as fetchMany} from 'core/ajax';
+import {normaliseResult} from './normalise';
+
+export const fetchGrade = type => (component, contextid, itemname, gradeduserid) => {
+ return fetchMany([{
+ methodname: `core_grades_grader_gradingpanel_${type}_fetch`,
+ args: {
+ component,
+ contextid,
+ itemname,
+ gradeduserid,
+ },
+ }])[0];
+};
+
+export const saveGrade = type => async(component, contextid, itemname, gradeduserid, formdata) => {
+ return normaliseResult(await fetchMany([{
+ methodname: `core_grades_grader_gradingpanel_${type}_store`,
+ args: {
+ component,
+ contextid,
+ itemname,
+ gradeduserid,
+ formdata,
+ },
+ }])[0]);
+};
--- /dev/null
+// 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/>.
+
+/**
+ * Grading panel for simple direct grading.
+ *
+ * @module core_grades/grades/grader/gradingpanel/scale
+ * @package core_grades
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {saveGrade, fetchGrade} from './repository';
+// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()
+import jQuery from 'jquery';
+import {invalidResult} from './normalise';
+
+export const fetchCurrentGrade = (...args) => fetchGrade('scale')(...args);
+
+export const storeCurrentGrade = (component, context, itemname, userId, rootNode) => {
+ const form = rootNode.querySelector('form');
+ const grade = form.querySelector('select[name="grade"]');
+
+ if (!grade.checkValidity() || !grade.value.trim()) {
+ return invalidResult;
+ }
+
+ return saveGrade('scale')(component, context, itemname, userId, jQuery(form).serialize());
+};
--- /dev/null
+<?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/>.
+
+/**
+ * Compontent definition of a gradeitem.
+ *
+ * @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 context;
+use gradingform_controller;
+use gradingform_instance;
+use moodle_exception;
+use stdClass;
+use grade_item as core_gradeitem;
+use grading_manager;
+
+/**
+ * Compontent definition of a gradeitem.
+ *
+ * @package core_grades
+ * @copyright Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class component_gradeitem {
+
+ /** @var array The scale data for the current grade item */
+ protected $scale;
+
+ /** @var string The component */
+ protected $component;
+
+ /** @var context The context for this activity */
+ protected $context;
+
+ /** @var string The item name */
+ protected $itemname;
+
+ /** @var int The grade itemnumber */
+ protected $itemnumber;
+
+ /**
+ * component_gradeitem constructor.
+ *
+ * @param string $component
+ * @param context $context
+ * @param string $itemname
+ * @throws \coding_exception
+ */
+ final protected function __construct(string $component, context $context, string $itemname) {
+ $this->component = $component;
+ $this->context = $context;
+ $this->itemname = $itemname;
+ $this->itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname);
+ }
+
+ /**
+ * Fetch an instance of a specific component_gradeitem.
+ *
+ * @param string $component
+ * @param context $context
+ * @param string $itemname
+ * @return self
+ */
+ public static function instance(string $component, context $context, string $itemname): self {
+ $itemnumber = component_gradeitems::get_itemnumber_from_itemname($component, $itemname);
+
+ $classname = "{$component}\\grades\\{$itemname}_gradeitem";
+ if (!class_exists($classname)) {
+ throw new coding_exception("Unknown gradeitem {$itemname} for component {$classname}");
+ }
+
+ return $classname::load_from_context($context);
+ }
+
+ /**
+ * Load an instance of the current component_gradeitem based on context.
+ *
+ * @param context $context
+ * @return self
+ */
+ abstract public static function load_from_context(context $context): self;
+
+ /**
+ * The table name used for grading.
+ *
+ * @return string
+ */
+ abstract protected function get_table_name(): string;
+
+ /**
+ * Get the itemid for the current gradeitem.
+ *
+ * @return int
+ */
+ public function get_grade_itemid(): int {
+ return component_gradeitems::get_itemnumber_from_itemname($this->component, $this->itemname);
+ }
+
+ /**
+ * Whether grading is enabled for this item.
+ *
+ * @return bool
+ */
+ abstract public function is_grading_enabled(): bool;
+
+ /**
+ * Get the grade value for this instance.
+ * The itemname is translated to the relevant grade field for the activity.
+ *
+ * @return int
+ */
+ abstract protected function get_gradeitem_value(): ?int;
+
+ /**
+ * Whether the grader can grade the gradee.
+ *
+ * @param stdClass $gradeduser The user being graded
+ * @param stdClass $grader The user who is grading
+ * @return bool
+ */
+ abstract public function user_can_grade(stdClass $gradeduser, stdClass $grader): bool;
+
+ /**
+ * Require that the user can grade, throwing an exception if not.
+ *
+ * @param stdClass $gradeduser The user being graded
+ * @param stdClass $grader The user who is grading
+ * @throws required_capability_exception
+ */
+ abstract public function require_user_can_grade(stdClass $gradeduser, stdClass $grader): void;
+
+ /**
+ * Get the scale if a scale is being used.
+ *
+ * @return stdClass
+ */
+ protected function get_scale(): ?stdClass {
+ global $DB;
+
+ $gradetype = $this->get_gradeitem_value();
+ if ($gradetype > 0) {
+ return null;
+ }
+
+ // This is a scale.
+ if (null === $this->scale) {
+ $this->scale = $DB->get_record('scale', ['id' => -1 * $gradetype]);
+ }
+
+ return $this->scale;
+ }
+
+ /**
+ * Check whether a scale is being used for this grade item.
+ *
+ * @return bool
+ */
+ public function is_using_scale(): bool {
+ $gradetype = $this->get_gradeitem_value();
+
+ return $gradetype < 0;
+ }
+
+ /**
+ * Whether this grade item is configured to use direct grading.
+ *
+ * @return bool
+ */
+ public function is_using_direct_grading(): bool {
+ if ($this->is_using_scale()) {
+ return false;
+ }
+
+ if ($this->get_advanced_grading_controller()) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Whether this grade item is configured to use advanced grading.
+ *
+ * @return bool
+ */
+ public function is_using_advanced_grading(): bool {
+ if ($this->is_using_scale()) {
+ return false;
+ }
+
+ if ($this->get_advanced_grading_controller()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the name of the advanced grading method.
+ *
+ * @return string
+ */
+ public function get_advanced_grading_method(): ?string {
+ $gradingmanager = $this->get_grading_manager();
+
+ if (empty($gradingmanager)) {
+ return null;
+ }
+
+ return $gradingmanager->get_active_method();
+ }
+
+ /**
+ * Get the name of the component responsible for grading this gradeitem.
+ *
+ * @return string
+ */
+ public function get_grading_component_name(): ?string {
+ if (!$this->is_grading_enabled()) {
+ return null;
+ }
+
+ if ($method = $this->get_advanced_grading_method()) {
+ return "gradingform_{$method}";
+ }
+
+ return 'core_grades';
+ }
+
+ /**
+ * Get the name of the component subtype responsible for grading this gradeitem.
+ *
+ * @return string
+ */
+ public function get_grading_component_subtype(): ?string {
+ if (!$this->is_grading_enabled()) {
+ return null;
+ }
+
+ if ($method = $this->get_advanced_grading_method()) {
+ return null;
+ }
+
+ if ($this->is_using_scale()) {
+ return 'scale';
+ }
+
+ return 'point';
+ }
+
+ /**
+ * Whether decimals are allowed.
+ *
+ * @return bool
+ */
+ protected function allow_decimals(): bool {
+ return $this->get_gradeitem_value() > 0;
+ }
+
+ /**
+ * Get the grading manager for this advanced grading definition.
+ *
+ * @return grading_manager
+ */
+ protected function get_grading_manager(): ?grading_manager {
+ require_once(__DIR__ . '/../grading/lib.php');
+ return get_grading_manager($this->context, $this->component, $this->itemname);
+
+ }
+
+ /**
+ * Get the advanced grading controller if advanced grading is enabled.
+ *
+ * @return gradingform_controller
+ */
+ protected function get_advanced_grading_controller(): ?gradingform_controller {
+ $gradingmanager = $this->get_grading_manager();
+
+ if (empty($gradingmanager)) {
+ return null;
+ }
+
+ if ($gradingmethod = $gradingmanager->get_active_method()) {
+ return $gradingmanager->get_controller($gradingmethod);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the list of available grade items.
+ *
+ * @return array
+ */
+ public function get_grade_menu(): array {
+ return make_grades_menu($this->get_gradeitem_value());
+ }
+
+ /**
+ * Check whether the supplied grade is valid and throw an exception if not.
+ *
+ * @param float $grade The value being checked
+ * @throws moodle_exception
+ * @return bool
+ */
+ public function check_grade_validity(?float $grade): bool {
+ $grade = grade_floatval(unformat_float($grade));
+ if ($grade) {
+ if ($this->is_using_scale()) {
+ // Fetch all options for this scale.
+ $scaleoptions = make_menu_from_list($this->get_scale()->scale);
+
+ if ($grade != -1 && !array_key_exists((int) $grade, $scaleoptions)) {
+ // The selected option did not exist.
+ throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
+ 'maxgrade' => count($scaleoptions),
+ 'grade' => $grade,
+ ]);
+ }
+ } else if ($grade) {
+ $maxgrade = $this->get_gradeitem_value();
+ if ($grade > $maxgrade) {
+ // The grade is greater than the maximum possible value.
+ throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
+ 'maxgrade' => $maxgrade,
+ 'grade' => $grade,
+ ]);
+ } else if ($grade < 0) {
+ // Negative grades are not supported.
+ throw new moodle_exception('error:notinrange', 'core_grading', '', (object) [
+ 'maxgrade' => $maxgrade,
+ 'grade' => $grade,
+ ]);
+ }
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Create an empty row in the grade for the specified user and grader.
+ *
+ * @param stdClass $gradeduser The user being graded
+ * @param stdClass $grader The user who is grading
+ * @return stdClass The newly created grade record
+ */
+ abstract public function create_empty_grade(stdClass $gradeduser, stdClass $grader): stdClass;
+
+ /**
+ * Get the grade record for the specified grade id.
+ *
+ * @param int $gradeid
+ * @return stdClass
+ * @throws \dml_exception
+ */
+ public function get_grade(int $gradeid): stdClass {
+ global $DB;
+
+ $grade = $DB->get_record($this->get_table_name(), ['id' => $gradeid]);
+
+ return $grade ?: null;
+ }
+
+ /**
+ * Get the grade for the specified user.
+ *
+ * @param stdClass $gradeduser The user being graded
+ * @param stdClass $grader The user who is grading
+ * @return stdClass The grade value
+ */
+ abstract public function get_grade_for_user(stdClass $gradeduser, stdClass $grader): ?stdClass;
+
+ /**
+ * Get grades for all users for the specified gradeitem.
+ *
+ * @return stdClass[] The grades
+ */
+ abstract public function get_all_grades(): array;
+
+ /**
+ * Create or update the grade.
+ *
+ * @param stdClass $grade
+ * @return bool Success
+ */
+ abstract protected function store_grade(stdClass $grade): bool;
+
+ /**
+ * Create or update the grade.
+ *
+ * @param stdClass $gradeduser The user being graded
+ * @param stdClass $grader The user who is grading
+ * @param stdClass $formdata The data submitted
+ * @return bool Success
+ */
+ public function store_grade_from_formdata(stdClass $gradeduser, stdClass $grader, stdClass $formdata): bool {
+ // Require gradelib for grade_floatval.
+ require_once(__DIR__ . '/../../lib/gradelib.php');
+ $grade = $this->get_grade_for_user($gradeduser, $grader);
+
+ if ($this->is_using_advanced_grading()) {
+ $instanceid = $formdata->instanceid;
+ $gradinginstance = $this->get_advanced_grading_instance($grader, $grade, (int) $instanceid);
+ $grade->grade = $gradinginstance->submit_and_get_grade($formdata->advancedgrading, $grade->id);
+
+ if ($grade->grade == -1) {
+ // In advanced grading, a value of -1 means no data.
+ return false;
+ }
+ } else {
+ // Handle the case when grade is set to No Grade.
+ if (isset($formdata->grade)) {
+ $grade->grade = grade_floatval(unformat_float($formdata->grade));
+ }
+ }
+
+ return $this->store_grade($grade);
+ }
+
+ /**
+ * Get the advanced grading instance for the specified grade entry.
+ *
+ * @param stdClass $grader The user who is grading
+ * @param stdClass $grade The row from the grade table.
+ * @param int $instanceid The instanceid of the advanced grading form
+ * @return gradingform_instance
+ */
+ public function get_advanced_grading_instance(stdClass $grader, stdClass $grade, int $instanceid = null): ?gradingform_instance {
+ $controller = $this->get_advanced_grading_controller($this->itemname);
+
+ if (empty($controller)) {
+ // Advanced grading not enabeld for this item.
+ return null;
+ }
+
+ if (!$controller->is_form_available()) {
+ // The form is not available for this item.
+ return null;
+ }
+
+ // Fetch the instance for the specified graderid/itemid.
+ $gradinginstance = $controller->fetch_instance(
+ (int) $grader->id,
+ (int) $grade->id,
+ $instanceid
+ );
+
+ // Set the allowed grade range.
+ $gradinginstance->get_controller()->set_grade_range(
+ $this->get_grade_menu(),
+ $this->allow_decimals()
+ );
+
+ return $gradinginstance;
+ }
+}
--- /dev/null
+<?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}");
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Web service functions relating to point grades and grading.
+ *
+ * @package core_grades
+ * @copyright 2019 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\grades\grader\gradingpanel\point\external;
+
+use coding_exception;
+use context;
+use core_user;
+use core_grades\component_gradeitem as gradeitem;
+use core_grades\component_gradeitems;
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use moodle_exception;
+use required_capability_exception;
+use stdClass;
+
+/**
+ * External grading panel point API
+ *
+ * @package core_grades
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fetch extends external_api {
+
+ /**
+ * Describes the parameters for fetching the grading panel for a simple grade.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.8
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters ([
+ 'component' => new external_value(
+ PARAM_ALPHANUMEXT,
+ 'The name of the component',
+ VALUE_REQUIRED
+ ),
+ 'contextid' => new external_value(
+ PARAM_INT,
+ 'The ID of the context being graded',
+ VALUE_REQUIRED
+ ),
+ 'itemname' => new external_value(
+ PARAM_ALPHANUM,
+ 'The grade item itemname being graded',
+ VALUE_REQUIRED
+ ),
+ 'gradeduserid' => new external_value(
+ PARAM_INT,
+ 'The ID of the user show',
+ VALUE_REQUIRED
+ ),
+ ]);
+ }
+
+ /**
+ * Fetch the data required to build a grading panel for a simple grade.
+ *
+ * @param string $component
+ * @param int $contextid
+ * @param string $itemname
+ * @param int $gradeduserid
+ * @return array
+ * @throws \dml_exception
+ * @throws \invalid_parameter_exception
+ * @throws \restricted_context_exception
+ * @throws coding_exception
+ * @throws moodle_exception
+ * @since Moodle 3.8
+ */
+ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid): array {
+ global $USER;
+
+ [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ ]);
+
+ // Validate the context.
+ $context = context::instance_by_id($contextid);
+ self::validate_context($context);
+
+ // Validate that the supplied itemname is a gradable item.
+ if (!component_gradeitems::is_valid_itemname($component, $itemname)) {
+ throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component");
+ }
+
+ // Fetch the gradeitem instance.
+ $gradeitem = gradeitem::instance($component, $context, $itemname);
+
+ if (!$gradeitem->is_using_direct_grading()) {
+ throw new moodle_exception("The {$itemname} item in {$component}/{$contextid} is not configured for direct grading");
+ }
+
+ // Fetch the actual data.
+ $gradeduser = \core_user::get_user($gradeduserid);
+ $grade = $gradeitem->get_grade_for_user($gradeduser, $USER);
+
+ return self::get_fetch_data($grade);
+ }
+
+ /**
+ * Get the data to be fetched.
+ *
+ * @param stdClass $grade
+ * @return array
+ */
+ public static function get_fetch_data(stdClass $grade): array {
+ return [
+ 'templatename' => 'core_grades/grades/grader/gradingpanel/point',
+ 'grade' => [
+ 'grade' => $grade->grade,
+ 'timecreated' => $grade->timecreated,
+ 'timemodified' => $grade->timemodified,
+ ],
+ 'warnings' => [],
+ ];
+ }
+
+ /**
+ * Describes the data returned from the external function.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.8
+ */
+ public static function execute_returns(): external_single_structure {
+ return new external_single_structure([
+ 'templatename' => new external_value(PARAM_SAFEPATH, 'The template to use when rendering this data'),
+ 'grade' => new external_single_structure([
+ 'grade' => new external_value(PARAM_FLOAT, 'The numeric grade'),
+ 'timecreated' => new external_value(PARAM_INT, 'The time that the grade was created'),
+ 'timemodified' => new external_value(PARAM_INT, 'The time that the grade was last updated'),
+ ]),
+ 'warnings' => new external_warnings(),
+ ]);
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Web service functions relating to point grades and grading.
+ *
+ * @package core_grades
+ * @copyright 2019 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\grades\grader\gradingpanel\point\external;
+
+use coding_exception;
+use context;
+use core_user;
+use core_grades\component_gradeitem as gradeitem;
+use core_grades\component_gradeitems;
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use moodle_exception;
+use required_capability_exception;
+
+/**
+ * External grading panel point API
+ *
+ * @package core_grades
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class store extends external_api {
+
+ /**
+ * Describes the parameters for fetching the grading panel for a simple grade.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.8
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters ([
+ 'component' => new external_value(
+ PARAM_ALPHANUMEXT,
+ 'The name of the component',
+ VALUE_REQUIRED
+ ),
+ 'contextid' => new external_value(
+ PARAM_INT,
+ 'The ID of the context being graded',
+ VALUE_REQUIRED
+ ),
+ 'itemname' => new external_value(
+ PARAM_ALPHANUM,
+ 'The grade item itemname being graded',
+ VALUE_REQUIRED
+ ),
+ 'gradeduserid' => new external_value(
+ PARAM_INT,
+ 'The ID of the user show',
+ VALUE_REQUIRED
+ ),
+ 'formdata' => new external_value(
+ PARAM_RAW,
+ 'The serialised form data representing the grade',
+ VALUE_REQUIRED
+ ),
+ ]);
+ }
+
+ /**
+ * Fetch the data required to build a grading panel for a simple grade.
+ *
+ * @param string $component
+ * @param int $contextid
+ * @param string $itemname
+ * @param int $gradeduserid
+ * @param string $formdata
+ * @return array
+ * @throws \dml_exception
+ * @throws \invalid_parameter_exception
+ * @throws \restricted_context_exception
+ * @throws coding_exception
+ * @throws moodle_exception
+ * @since Moodle 3.8
+ */
+ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid, string $formdata): array {
+ global $USER;
+
+ [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ 'formdata' => $formdata,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ 'formdata' => $formdata,
+ ]);
+
+ // Validate the context.
+ $context = context::instance_by_id($contextid);
+ self::validate_context($context);
+
+ // Validate that the supplied itemname is a gradable item.
+ if (!component_gradeitems::is_valid_itemname($component, $itemname)) {
+ throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component");
+ }
+
+ // Fetch the gradeitem instance.
+ $gradeitem = gradeitem::instance($component, $context, $itemname);
+
+ // Validate that this gradeitem is actually enabled.
+ if (!$gradeitem->is_grading_enabled()) {
+ throw new moodle_exception("Grading is not enabled for {$itemname} in this context");
+ }
+
+ // Fetch the record for the graded user.
+ $gradeduser = \core_user::get_user($gradeduserid);
+
+ // Require that this user can save grades.
+ $gradeitem->require_user_can_grade($gradeduser, $USER);
+
+ if (!$gradeitem->is_using_direct_grading()) {
+ throw new moodle_exception("The {$itemname} item in {$component}/{$contextid} is not configured for direct grading");
+ }
+
+ // Parse the serialised string into an object.
+ $data = [];
+ parse_str($formdata, $data);
+
+ // Grade.
+ $gradeitem->store_grade_from_formdata($gradeduser, $USER, (object) $data);
+
+ // Fetch the updated grade back out.
+ $grade = $gradeitem->get_grade_for_user($gradeduser, $USER);
+
+ return fetch::get_fetch_data($grade);
+ }
+
+ /**
+ * Describes the data returned from the external function.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.8
+ */
+ public static function execute_returns(): external_single_structure {
+ return fetch::execute_returns();
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Web service functions relating to scale grades and grading.
+ *
+ * @package core_grades
+ * @copyright 2019 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\grades\grader\gradingpanel\scale\external;
+
+use coding_exception;
+use context;
+use core_grades\component_gradeitem as gradeitem;
+use core_grades\component_gradeitems;
+use core_user;
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use moodle_exception;
+use required_capability_exception;
+use stdClass;
+
+/**
+ * External grading panel scale API
+ *
+ * @package core_grades
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fetch extends external_api {
+
+ /**
+ * Describes the parameters for fetching the grading panel for a simple grade.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.8
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters ([
+ 'component' => new external_value(
+ PARAM_ALPHANUMEXT,
+ 'The name of the component',
+ VALUE_REQUIRED
+ ),
+ 'contextid' => new external_value(
+ PARAM_INT,
+ 'The ID of the context being graded',
+ VALUE_REQUIRED
+ ),
+ 'itemname' => new external_value(
+ PARAM_ALPHANUM,
+ 'The grade item itemname being graded',
+ VALUE_REQUIRED
+ ),
+ 'gradeduserid' => new external_value(
+ PARAM_INT,
+ 'The ID of the user show',
+ VALUE_REQUIRED
+ ),
+ ]);
+ }
+
+ /**
+ * Fetch the data required to build a grading panel for a simple grade.
+ *
+ * @param string $component
+ * @param int $contextid
+ * @param string $itemname
+ * @param int $gradeduserid
+ * @return array
+ * @throws \dml_exception
+ * @throws \invalid_parameter_exception
+ * @throws \restricted_context_exception
+ * @throws coding_exception
+ * @throws moodle_exception
+ * @since Moodle 3.8
+ */
+ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid): array {
+ global $USER;
+
+ [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ ]);
+
+ // Validate the context.
+ $context = context::instance_by_id($contextid);
+ self::validate_context($context);
+
+ // Validate that the supplied itemname is a gradable item.
+ if (!component_gradeitems::is_valid_itemname($component, $itemname)) {
+ throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component");
+ }
+
+ // Fetch the gradeitem instance.
+ $gradeitem = gradeitem::instance($component, $context, $itemname);
+
+ if (!$gradeitem->is_using_scale()) {
+ throw new moodle_exception("The {$itemname} item in {$component}/{$contextid} is not configured for grading with scales");
+ }
+
+ $gradeduser = \core_user::get_user($gradeduserid);
+
+ return self::get_fetch_data($gradeitem, $gradeduser);
+ }
+
+ /**
+ * Get the data to be fetched.
+ *
+ * @param gradeitem $gradeitem
+ * @param stdClass $gradeduser
+ * @return array
+ */
+ public static function get_fetch_data(gradeitem $gradeitem, stdClass $gradeduser): array {
+ global $USER;
+
+ $grade = $gradeitem->get_grade_for_user($gradeduser, $USER);
+ $currentgrade = (int) unformat_float($grade->grade);
+
+ $menu = $gradeitem->get_grade_menu();
+ $values = array_map(function($description, $value) use ($currentgrade) {
+ return [
+ 'value' => $value,
+ 'title' => $description,
+ 'selected' => ($value == $currentgrade),
+ ];
+ }, $menu, array_keys($menu));
+
+ return [
+ 'templatename' => 'core_grades/grades/grader/gradingpanel/scale',
+ 'grade' => [
+ 'options' => $values,
+ 'timecreated' => $grade->timecreated,
+ 'timemodified' => $grade->timemodified,
+ ],
+ 'warnings' => [],
+ ];
+ }
+
+ /**
+ * Describes the data returned from the external function.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.8
+ */
+ public static function execute_returns(): external_single_structure {
+ return new external_single_structure([
+ 'templatename' => new external_value(PARAM_SAFEPATH, 'The template to use when rendering this data'),
+ 'grade' => new external_single_structure([
+ 'options' => new external_multiple_structure(
+ new external_single_structure([
+ 'value' => new external_value(PARAM_FLOAT, 'The grade value'),
+ 'title' => new external_value(PARAM_RAW, 'The description fo the option'),
+ 'selected' => new external_value(PARAM_BOOL, 'Whether this item is currently selected'),
+ ]),
+ 'The description of the grade option'
+ ),
+ 'timecreated' => new external_value(PARAM_INT, 'The time that the grade was created'),
+ 'timemodified' => new external_value(PARAM_INT, 'The time that the grade was last updated'),
+ ]),
+ 'warnings' => new external_warnings(),
+ ]);
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Web service functions relating to scale grades and grading.
+ *
+ * @package core_grades
+ * @copyright 2019 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\grades\grader\gradingpanel\scale\external;
+
+use coding_exception;
+use context;
+use core_user;
+use core_grades\component_gradeitem as gradeitem;
+use core_grades\component_gradeitems;
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use moodle_exception;
+use required_capability_exception;
+
+/**
+ * External grading panel scale API
+ *
+ * @package core_grades
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class store extends external_api {
+
+ /**
+ * Describes the parameters for fetching the grading panel for a simple grade.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.8
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters ([
+ 'component' => new external_value(
+ PARAM_ALPHANUMEXT,
+ 'The name of the component',
+ VALUE_REQUIRED
+ ),
+ 'contextid' => new external_value(
+ PARAM_INT,
+ 'The ID of the context being graded',
+ VALUE_REQUIRED
+ ),
+ 'itemname' => new external_value(
+ PARAM_ALPHANUM,
+ 'The grade item itemname being graded',
+ VALUE_REQUIRED
+ ),
+ 'gradeduserid' => new external_value(
+ PARAM_INT,
+ 'The ID of the user show',
+ VALUE_REQUIRED
+ ),
+ 'formdata' => new external_value(
+ PARAM_RAW,
+ 'The serialised form data representing the grade',
+ VALUE_REQUIRED
+ ),
+ ]);
+ }
+
+ /**
+ * Fetch the data required to build a grading panel for a simple grade.
+ *
+ * @param string $component
+ * @param int $contextid
+ * @param string $itemname
+ * @param int $gradeduserid
+ * @param string $formdata
+ * @return array
+ * @throws \dml_exception
+ * @throws \invalid_parameter_exception
+ * @throws \restricted_context_exception
+ * @throws coding_exception
+ * @throws moodle_exception
+ * @since Moodle 3.8
+ */
+ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid, string $formdata): array {
+ global $USER;
+
+ [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ 'formdata' => $formdata,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ 'formdata' => $formdata,
+ ]);
+
+ // Validate the context.
+ $context = context::instance_by_id($contextid);
+ self::validate_context($context);
+
+ // Validate that the supplied itemname is a gradable item.
+ if (!component_gradeitems::is_valid_itemname($component, $itemname)) {
+ throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component");
+ }
+
+ // Fetch the gradeitem instance.
+ $gradeitem = gradeitem::instance($component, $context, $itemname);
+
+ // Validate that this gradeitem is actually enabled.
+ if (!$gradeitem->is_grading_enabled()) {
+ throw new moodle_exception("Grading is not enabled for {$itemname} in this context");
+ }
+
+ // Fetch the record for the graded user.
+ $gradeduser = \core_user::get_user($gradeduserid);
+
+ // Require that this user can save grades.
+ $gradeitem->require_user_can_grade($gradeduser, $USER);
+
+ if (!$gradeitem->is_using_scale()) {
+ throw new moodle_exception("The {$itemname} item in {$component}/{$contextid} is not configured for grading with scales");
+ }
+
+ // Parse the serialised string into an object.
+ $data = [];
+ parse_str($formdata, $data);
+
+ // Grade.
+ $gradeitem->store_grade_from_formdata($gradeduser, $USER, (object) $data);
+
+ return fetch::get_fetch_data($gradeitem, $gradeduser);
+ }
+
+ /**
+ * Describes the data returned from the external function.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.8
+ */
+ public static function execute_returns(): external_single_structure {
+ return fetch::execute_returns();
+ }
+}
--- /dev/null
+<?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;
+}
--- /dev/null
+<?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;
+}
--- /dev/null
+// 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/>.
+
+/**
+ * Grading panel for gradingform_guide.
+ *
+ * @module gradingform_guide/grades/grader/gradingpanel
+ * @package gradingform_guide
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {call as fetchMany} from 'core/ajax';
+import {normaliseResult} from 'core_grades/grades/grader/gradingpanel/normalise';
+
+// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()
+import jQuery from 'jquery';
+
+/**
+ * For a given component, contextid, itemname & gradeduserid we can fetch the currently assigned grade.
+ *
+ * @param {String} component
+ * @param {Number} contextid
+ * @param {String} itemname
+ * @param {Number} gradeduserid
+ *
+ * @returns {Promise}
+ */
+export const fetchCurrentGrade = (component, contextid, itemname, gradeduserid) => {
+ return fetchMany([{
+ methodname: `gradingform_guide_grader_gradingpanel_fetch`,
+ args: {
+ component,
+ contextid,
+ itemname,
+ gradeduserid,
+ },
+ }])[0];
+};
+
+/**
+ * For a given component, contextid, itemname & gradeduserid we can store the currently assigned grade in a given form.
+ *
+ * @param {String} component
+ * @param {Number} contextid
+ * @param {String} itemname
+ * @param {Number} gradeduserid
+ * @param {HTMLElement} rootNode
+ *
+ * @returns {Promise}
+ */
+export const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, rootNode) => {
+ const form = rootNode.querySelector('form');
+
+ return normaliseResult(await fetchMany([{
+ methodname: `gradingform_guide_grader_gradingpanel_store`,
+ args: {
+ component,
+ contextid,
+ itemname,
+ gradeduserid,
+ formdata: jQuery(form).serialize(),
+ },
+ }])[0]);
+};
--- /dev/null
+// 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/>.
+
+/**
+ * Grading panel frequently used comments selector.
+ *
+ * @module gradingform_guide/grades/grader/gradingpanel/comments
+ * @package gradingform_guide
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import Selectors from './comments/selectors';
+
+/**
+ * Manage the frequently used comments in the Marking Guide form.
+ *
+ * @param {String} rootId
+ */
+export const init = (rootId) => {
+ const rootNode = document.querySelector(`#${rootId}`);
+
+ rootNode.addEventListener('click', (e) => {
+ if (!e.target.matches(Selectors.frequentComment)) {
+ return;
+ }
+
+ e.preventDefault();
+
+ const clicked = e.target.closest(Selectors.frequentComment);
+ const criterion = clicked.closest(Selectors.criterion);
+ const remark = criterion.querySelector(Selectors.remark);
+
+ if (!remark) {
+ return;
+ }
+
+ // Either append the comment to an existing comment or set it as the comment.
+ if (remark.value.trim()) {
+ remark.value += `\n${clicked.innerHTML}`;
+ } else {
+ remark.value += clicked.innerHTML;
+ }
+ });
+};
--- /dev/null
+// 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/>.
+
+/**
+ * Define all of the selectors we will be using on the Marking Guide interface.
+ *
+ * @module gradingform_guide/grades/grader/gradingpanel/comments/selectors
+ * @package gradingform_guide
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+export default {
+ frequentComment: '[data-gradingform_guide-role="frequent-comment"]',
+ criterion: '[data-gradingform-guide-role="criterion"]',
+ remark: '[data-gradingform-guide-role="remark"]',
+};
--- /dev/null
+<?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/>.
+
+/**
+ * Web services relating to fetching of a marking guide for the grading panel.
+ *
+ * @package gradingform_guide
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_guide\grades\grader\gradingpanel\external;
+
+global $CFG;
+
+use coding_exception;
+use context;
+use core_user;
+use core_grades\component_gradeitem as gradeitem;
+use core_grades\component_gradeitems;
+use external_api;
+use external_format_value;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use moodle_exception;
+use stdClass;
+require_once($CFG->dirroot.'/grade/grading/form/guide/lib.php');
+
+/**
+ * Web services relating to fetching of a marking guide for the grading panel.
+ *
+ * @package gradingform_guide
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fetch extends external_api {
+
+ /**
+ * Describes the parameters for fetching the grading panel for a simple grade.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.8
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters ([
+ 'component' => new external_value(
+ PARAM_ALPHANUMEXT,
+ 'The name of the component',
+ VALUE_REQUIRED
+ ),
+ 'contextid' => new external_value(
+ PARAM_INT,
+ 'The ID of the context being graded',
+ VALUE_REQUIRED
+ ),
+ 'itemname' => new external_value(
+ PARAM_ALPHANUM,
+ 'The grade item itemname being graded',
+ VALUE_REQUIRED
+ ),
+ 'gradeduserid' => new external_value(
+ PARAM_INT,
+ 'The ID of the user show',
+ VALUE_REQUIRED
+ ),
+ ]);
+ }
+
+ /**
+ * Fetch the data required to build a grading panel for a simple grade.
+ *
+ * @param string $component
+ * @param int $contextid
+ * @param string $itemname
+ * @param int $gradeduserid
+ * @return array
+ * @throws \dml_exception
+ * @throws \invalid_parameter_exception
+ * @throws \restricted_context_exception
+ * @throws coding_exception
+ * @throws moodle_exception
+ * @since Moodle 3.8
+ */
+ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid): array {
+ global $USER;
+
+ [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ ]);
+
+ // Validate the context.
+ $context = context::instance_by_id($contextid);
+ self::validate_context($context);
+
+ // Validate that the supplied itemname is a gradable item.
+ if (!component_gradeitems::is_valid_itemname($component, $itemname)) {
+ throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component");
+ }
+
+ // Fetch the gradeitem instance.
+ $gradeitem = gradeitem::instance($component, $context, $itemname);
+
+ if (MARKING_GUIDE !== $gradeitem->get_advanced_grading_method()) {
+ throw new moodle_exception(
+ "The {$itemname} item in {$component}/{$contextid} is not configured for advanced grading with a marking guide"
+ );
+ }
+
+ // Fetch the actual data.
+ $gradeduser = core_user::get_user($gradeduserid);
+
+ return self::get_fetch_data($gradeitem, $gradeduser);
+ }
+
+ /**
+ * Get the data to be fetched.
+ *
+ * @param gradeitem $gradeitem
+ * @param stdClass $gradeduser
+ * @return array
+ */
+ public static function get_fetch_data(gradeitem $gradeitem, stdClass $gradeduser): array {
+ global $USER;
+
+ $grade = $gradeitem->get_grade_for_user($gradeduser, $USER);
+ $instance = $gradeitem->get_advanced_grading_instance($USER, $grade);
+ $controller = $instance->get_controller();
+ $definition = $controller->get_definition();
+ $fillings = $instance->get_guide_filling();
+ $context = $controller->get_context();
+ $definitionid = (int) $definition->id;
+
+ $criterion = [];
+ if ($definition->guide_criteria) {
+ $criterion = array_map(function($criterion) use ($definitionid, $fillings, $context) {
+ $result = [
+ 'id' => $criterion['id'],
+ 'name' => $criterion['shortname'],
+ 'maxscore' => $criterion['maxscore'],
+ 'description' => self::get_formatted_text(
+ $context,
+ $definitionid,
+ 'description',
+ $criterion['description'],
+ (int) $criterion['descriptionformat']
+ ),
+ 'descriptionmarkers' => self::get_formatted_text(
+ $context,
+ $definitionid,
+ 'descriptionmarkers',
+ $criterion['descriptionmarkers'],
+ (int) $criterion['descriptionmarkersformat']
+ ),
+ 'score' => null,
+ 'remark' => null,
+ ];
+
+ if (array_key_exists($criterion['id'], $fillings['criteria'])) {
+ $filling = $fillings['criteria'][$criterion['id']];
+
+ $result['score'] = $filling['score'];
+ $result['remark'] = self::get_formatted_text(
+ $context,
+ $definitionid,
+ 'remark',
+ $filling['remark'],
+ (int) $filling['remarkformat']
+ );
+ }
+
+ return $result;
+ }, $definition->guide_criteria);
+ }
+
+ $comments = [];
+ if ($definition->guide_comments) {
+ $comments = array_map(function($comment) use ($definitionid, $context) {
+ return [
+ 'id' => $comment['id'],
+ 'sortorder' => $comment['sortorder'],
+ 'description' => self::get_formatted_text(
+ $context,
+ $definitionid,
+ 'description',
+ $comment['description'],
+ (int) $comment['descriptionformat']
+ ),
+ ];
+ }, $definition->guide_comments);
+ }
+
+ return [
+ 'templatename' => 'gradingform_guide/grades/grader/gradingpanel',
+ 'grade' => [
+ 'instanceid' => $instance->get_id(),
+ 'criterion' => $criterion,
+ 'hascomments' => !empty($comments),
+ 'comments' => $comments,
+ 'timecreated' => $grade->timecreated,
+ 'timemodified' => $grade->timemodified,
+ ],
+ 'warnings' => [],
+ ];
+ }
+
+ /**
+ * Describes the data returned from the external function.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.8
+ */
+ public static function execute_returns(): external_single_structure {
+ return new external_single_structure([
+ 'templatename' => new external_value(PARAM_SAFEPATH, 'The template to use when rendering this data'),
+ 'grade' => new external_single_structure([
+ 'instanceid' => new external_value(PARAM_INT, 'The id of the current grading instance'),
+ 'criterion' => new external_multiple_structure(
+ new external_single_structure([
+ 'id' => new external_value(PARAM_INT, 'The id of the criterion'),
+ 'name' => new external_value(PARAM_RAW, 'The name of the criterion'),
+ 'maxscore' => new external_value(PARAM_FLOAT, 'The maximum score for this criterion'),
+ 'description' => new external_value(PARAM_RAW, 'The description of the criterion'),
+ 'descriptionmarkers' => new external_value(PARAM_RAW, 'The description of the criterion for markers'),
+ 'score' => new external_value(PARAM_FLOAT, 'The current score for user being assessed', VALUE_OPTIONAL),
+ 'remark' => new external_value(PARAM_RAW, 'Any remarks for this criterion for the user being assessed', VALUE_OPTIONAL),
+ ]),
+ 'The criterion by which this item will be graded'
+ ),
+ 'hascomments' => new external_value(PARAM_BOOL, 'Whether there are any frequently-used comments'),
+ 'comments' => new external_multiple_structure(
+ new external_single_structure([
+ 'id' => new external_value(PARAM_INT, 'Comment id'),
+ 'sortorder' => new external_value(PARAM_INT, 'The sortorder of this comment'),
+ 'description' => new external_value(PARAM_RAW, 'The comment value'),
+ ]),
+ 'Frequently used comments'
+ ),
+ 'timecreated' => new external_value(PARAM_INT, 'The time that the grade was created'),
+ 'timemodified' => new external_value(PARAM_INT, 'The time that the grade was last updated'),
+ ]),
+ 'warnings' => new external_warnings(),
+ ]);
+ }
+
+ /**
+ * Get a formatted version of the remark/description/etc.
+ *
+ * @param context $context
+ * @param int $definitionid
+ * @param string $filearea The file area of the field
+ * @param string $text The text to be formatted
+ * @param int $format The input format of the string
+ * @return string
+ */
+ protected static function get_formatted_text(context $context, int $definitionid, string $filearea, string $text, int $format): string {
+ $formatoptions = [
+ 'noclean' => false,
+ 'trusted' => false,
+ 'filter' => true,
+ ];
+
+ [$newtext, ] = external_format_text($text, $format, $context, 'grading', $filearea, $definitionid, $formatoptions);
+
+ return $newtext;
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Web services relating to fetching of a marking guide for the grading panel.
+ *
+ * @package gradingform_guide
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_guide\grades\grader\gradingpanel\external;
+
+global $CFG;
+
+use coding_exception;
+use context;
+use core_grades\component_gradeitem as gradeitem;
+use core_grades\component_gradeitems;
+use core_user;
+use external_api;
+use external_function_parameters;
+use external_single_structure;
+use external_value;
+use moodle_exception;
+require_once($CFG->dirroot.'/grade/grading/form/guide/lib.php');
+
+/**
+ * Web services relating to storing of a marking guide for the grading panel.
+ *
+ * @package gradingform_guide
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class store extends external_api {
+
+ /**
+ * Describes the parameters for storing the grading panel for a simple grade.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.8
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters ([
+ 'component' => new external_value(
+ PARAM_ALPHANUMEXT,
+ 'The name of the component',
+ VALUE_REQUIRED
+ ),
+ 'contextid' => new external_value(
+ PARAM_INT,
+ 'The ID of the context being graded',
+ VALUE_REQUIRED
+ ),
+ 'itemname' => new external_value(
+ PARAM_ALPHANUM,
+ 'The grade item itemname being graded',
+ VALUE_REQUIRED
+ ),
+ 'gradeduserid' => new external_value(
+ PARAM_INT,
+ 'The ID of the user show',
+ VALUE_REQUIRED
+ ),
+ 'formdata' => new external_value(
+ PARAM_RAW,
+ 'The serialised form data representing the grade',
+ VALUE_REQUIRED
+ ),
+ ]);
+ }
+
+ /**
+ * Fetch the data required to build a grading panel for a simple grade.
+ *
+ * @param string $component
+ * @param int $contextid
+ * @param string $itemname
+ * @param int $gradeduserid
+ * @param string $formdata
+ * @return array
+ * @throws \dml_exception
+ * @throws \invalid_parameter_exception
+ * @throws \restricted_context_exception
+ * @throws coding_exception
+ * @throws moodle_exception
+ * @since Moodle 3.8
+ */
+ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid, string $formdata): array {
+ global $USER;
+
+ [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ 'formdata' => $formdata,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ 'formdata' => $formdata,
+ ]);
+
+ // Validate the context.
+ $context = context::instance_by_id($contextid);
+ self::validate_context($context);
+
+ // Validate that the supplied itemname is a gradable item.
+ if (!component_gradeitems::is_valid_itemname($component, $itemname)) {
+ throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component");
+ }
+
+ // Fetch the gradeitem instance.
+ $gradeitem = gradeitem::instance($component, $context, $itemname);
+
+ // Validate that this gradeitem is actually enabled.
+ if (!$gradeitem->is_grading_enabled()) {
+ throw new moodle_exception("Grading is not enabled for {$itemname} in this context");
+ }
+
+ // Fetch the record for the graded user.
+ $gradeduser = core_user::get_user($gradeduserid);
+
+ // Require that this user can save grades.
+ $gradeitem->require_user_can_grade($gradeduser, $USER);
+
+ if (MARKING_GUIDE !== $gradeitem->get_advanced_grading_method()) {
+ throw new moodle_exception(
+ "The {$itemname} item in {$component}/{$contextid} is not configured for advanced grading with a marking guide"
+ );
+ }
+
+ // Parse the serialised string into an object.
+ $data = [];
+ parse_str($formdata, $data);
+
+ // Grade.
+ $gradeitem->store_grade_from_formdata($gradeduser, $USER, (object) $data);
+
+ return fetch::get_fetch_data($gradeitem, $gradeduser);
+ }
+
+ /**
+ * Describes the data returned from the external function.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.8
+ */
+ public static function execute_returns(): external_single_structure {
+ return fetch::execute_returns();
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * External functions and service definitions for the Marking Guide advanced grading form.
+ *
+ * @package gradingform_guide
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+$functions = [
+ 'gradingform_guide_grader_gradingpanel_fetch' => [
+ 'classname' => 'gradingform_guide\\grades\\grader\\gradingpanel\\external\\fetch',
+ 'methodname' => 'execute',
+ 'description' => 'Fetch the data required to display the grader grading panel, ' .
+ 'creating the grade item if required',
+ 'type' => 'write',
+ 'ajax' => true,
+ ],
+ 'gradingform_guide_grader_gradingpanel_store' => [
+ 'classname' => 'gradingform_guide\\grades\\grader\\gradingpanel\\external\\store',
+ 'methodname' => 'execute',
+ 'description' => 'Store the grading data for a user from the grader grading panel.',
+ 'type' => 'write',
+ 'ajax' => true,
+ ],
+];
defined('MOODLE_INTERNAL') || die();
$string['addcomment'] = 'Add frequently used comment';
+$string['additionalcomments'] = 'Additional comments';
$string['addcriterion'] = 'Add criterion';
$string['alwaysshowdefinition'] = 'Show guide definition to students';
$string['backtoediting'] = 'Back to editing';
$string['maxscore'] = 'Maximum score';
$string['name'] = 'Name';
$string['needregrademessage'] = 'The marking guide definition was changed after this student had been graded. The student can not see this marking guide until you check the marking guide and update the grade.';
+$string['outof'] = 'Out of {$a}';
$string['pluginname'] = 'Marking guide';
$string['previewmarkingguide'] = 'Preview marking guide';
$string['privacy:metadata:criterionid'] = 'An identifier to a criterion for advanced marking.';
require_once($CFG->dirroot.'/grade/grading/form/lib.php');
+/** guide: Used to compare our gradeitem_type against. */
+const MARKING_GUIDE = 'guide';
+
/**
* This controller encapsulates the guide grading logic
*
return $html;
}
}
+
+/**
+ * Get the icon mapping for font-awesome.
+ *
+ * @return array
+ */
+function gradingform_guide_get_fontawesome_icon_map(): array {
+ return [
+ 'gradingform_guide:info' => 'fa-info-circle',
+ 'gradingform_guide:plus' => 'fa-plus',
+ ];
+}
--- /dev/null
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+ <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 14c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6zm1.2-2c0 .5-.5 1-1 1h-.4c-.5 0-1-.5-1-1V7.4c0-.5.5-1 1-1h.5c.5 0 1 .5 1 1V12zm0-7.8c0 .7-.6 1.2-1.2 1.2-.7 0-1.2-.6-1.2-1.2C6.8 3.5 7.3 3 8 3s1.2.5 1.2 1.2z" fill="#999"/></svg>
\ No newline at end of file
--- /dev/null
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [\r
+ <!ENTITY ns_flows "http://ns.adobe.com/Flows/1.0/">\r
+]><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" preserveAspectRatio="xMinYMid meet" overflow="visible"><path d="M11 4.5H7.5V1c0-.5-.5-1-1-1h-1c-.5 0-1 .5-1 1v3.5H1c-.5 0-1 .5-1 1v1c0 .5.5 1 1 1h3.5V11c0 .5.5 1 1 1h1c.5 0 1-.5 1-1V7.5H11c.6 0 1-.5 1-1v-1c0-.5-.4-1-1-1z" fill="#999"/></svg>
\ No newline at end of file
max-height: 80vh;
overflow-y: auto;
}
+.gradingform_guide-frequent-comments {
+ position: absolute;
+ top: 7px;
+ right: 0;
+}
--- /dev/null
+.gradingform_guide-fac {
+ position: absolute;
+ right: -5px;
+ top: 5px;
+}
--- /dev/null
+{{!
+ 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/>.
+}}
+{{!
+ @template gradingform_rubric/grades/grader/gradingpanel
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * instanceid: Instance of the module this grading form belongs too
+ * criterion: A gradeable item in the Marking Guide
+ * name: Name of the gradeable item
+ * id: ID of the gradeable item
+ * description: Description shown to students for this gradeable item
+ * descriptionmarkers: Description shown to teachers for this gradeable item
+ * maxscore: Max allowable assinable points for this item
+ * score: Current score assigned to the learner for this item
+ * remark: Text input for the teacher to relay to the student
+ * hascomments: Flag for frequently used comments
+ * comments: Array of frequently used comments
+ * description: Description of a frequently used comment
+
+ Example context (json):
+ {
+ "instanceid": "42",
+ "criterion": [
+ {
+ "name": "Motivation",
+ "id": 13,
+ "description": "Show your motivation to rock climbing",
+ "descriptionmarkers": "Does the student show interest in climbing?",
+ "maxscore": 37,
+ "score": 20,
+ "remark": "That's great!",
+ "hascomments": true,
+ "comments": [
+ {"description": "Great work!"},
+ {"description": "You should really try it before jumping to conclusions"}
+ ]
+ }
+ ]
+ }
+}}
+<form id="gradingform_guide-{{uniqid}}">
+ <input type="hidden" name="instanceid" value="{{instanceid}}">
+ {{#criterion}}
+ <div data-gradingform-guide-role="criterion">
+ <h5>
+ {{name}}
+ <a
+ href="#gradingform_guide-{{uniqid}}-criteria-{{id}}-description"
+ aria-controls="gradingform_guide-{{uniqid}}-criteria-{{id}}-description"
+ aria-expanded="false"
+ data-toggle="collapse"
+ role="button"
+ >
+ {{# pix }} info, gradingform_guide {{/ pix }}
+ </a>
+ </h5>
+ <div class="collapse" id="gradingform_guide-{{uniqid}}-criteria-{{id}}-description">
+ <div class="border p-3 mb-3 bg-white rounded">
+ {{{description}}}
+ {{#descriptionmarkers}}
+ <hr>
+ {{{descriptionmarkers}}}
+ {{/descriptionmarkers}}
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="gradingform_guide-{{uniqid}}-criteria-{{id}}-score">{{#str}}outof, gradingform_guide, {{maxscore}}{{/str}}</label>
+ <input class="form-control" type="number" name="advancedgrading[criteria][{{id}}][score]" value="{{score}}"
+ id="gradingform_guide-{{uniqid}}-criteria-{{id}}-score"
+ aria-describedby="gradingform_guide-{{uniqid}}-help-{{id}}-score">
+ <small id="gradingform_guide-{{uniqid}}-help-{{id}}-score" class="sr-only">{{#str}}grade_help, gradingform_guide{{/str}}</small>
+ </div>
+ <div class="form-group ">
+ <label for="gradingform_guide-{{uniqid}}-criteria-{{id}}-remark">{{#str}}additionalcomments, gradingform_guide{{/str}}</label>
+ <div class="input-group mb-3 form-inset form-inset-right">
+ <textarea class="form-control" type="text" name="advancedgrading[criteria][{{id}}][remark]"
+ id="gradingform_guide-{{uniqid}}-criteria-{{id}}-remark"
+ aria-describedby="gradingform_guide-{{uniqid}}-help-{{id}}-remark"
+ data-gradingform-guide-role="remark"
+ >{{remark}}</textarea>
+ {{#hascomments}}
+ <a
+ class="form-inset-item"
+ href="#gradingform_guide-{{uniqid}}-criteria-{{id}}-remark-frequent-comments"
+ aria-controls="gradingform_guide-{{uniqid}}-criteria-{{id}}-remark-frequent-comments"
+ aria-expanded="false"
+ data-toggle="collapse"
+ role="button"
+ >
+ {{#pix}}plus, gradingform_guide{{/pix}}
+ </a>
+ {{/hascomments}}
+ </div>
+ {{#hascomments}}
+ <div class="collapse" id="gradingform_guide-{{uniqid}}-criteria-{{id}}-remark-frequent-comments">
+ <div data-gradingform_guide-frequent-comments="gradingform_guide-{{uniqid}}-criteria-{{id}}-remark">
+ <div class="list-group">
+ {{#comments}}
+ <button type="button" class="list-group-item list-group-item-action" data-gradingform_guide-role="frequent-comment">{{description}}</button>
+ {{/comments}}
+ </div>
+ </div>
+ </div>
+ {{/hascomments}}
+ <small id="gradingform_guide-{{uniqid}}-help-{{id}}-remark" class="sr-only">{{#str}}grade_help, gradingform_guide{{/str}}</small>
+ </div>
+ </div>
+ {{/criterion}}
+</form>
+{{#js}}
+require(['gradingform_guide/grades/grader/gradingpanel/comments'], function(Comments) {
+ Comments.init('gradingform_guide-{{uniqid}}');
+});
+{{/js}}
--- /dev/null
+<?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/>.
+
+/**
+ * Coverage information for the gradingform_guide plugin.
+ *
+ * @package gradingform_guide
+ * @category test
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Coverage information for the gradingform_guide plugin.
+ *
+ * @package gradingform_guide
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+return new class extends phpunit_coverage_info {
+ // Array The list of folders relative to the plugin root to whitelist in coverage generation.
+ protected $whitelistfolders = [
+ 'classes',
+ 'tests/generator',
+ ];
+};
* @param context_module $context
* @return gradingform_guide_controller
*/
- public function get_test_guide(context_module $context): gradingform_guide_controller {
+ public function get_test_guide(
+ context_module $context,
+ string $component = 'mod_assign',
+ string $areaname = 'submission'
+ ): gradingform_guide_controller {
$generator = \testing_util::get_data_generator();
$gradinggenerator = $generator->get_plugin_generator('core_grading');
- $controller = $gradinggenerator->create_instance($context, 'mod_assign', 'submission', 'guide');
+ $controller = $gradinggenerator->create_instance($context, $component, $areaname, 'guide');
$generator = \testing_util::get_data_generator();
$guidegenerator = $generator->get_plugin_generator('gradingform_guide');
--- /dev/null
+<?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/>.
+
+/**
+ * Unit tests for core_grades\component_gradeitems;
+ *
+ * @package gradingform_guide
+ * @category test
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_guide\grades\grader\gradingpanel\external;
+
+use advanced_testcase;
+use coding_exception;
+use core_grades\component_gradeitem;
+use external_api;
+use mod_forum\local\entities\forum as forum_entity;
+use moodle_exception;
+
+/**
+ * Unit tests for core_grades\component_gradeitems;
+ *
+ * @package gradingform_guide
+ * @category test
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fetch_test extends advanced_testcase {
+
+ public static function setupBeforeClass(): void {
+ global $CFG;
+ require_once("{$CFG->libdir}/externallib.php");
+ }
+
+ /**
+ * Ensure that an execute with an invalid component is rejected.
+ */
+ public function test_execute_invalid_component(): void {
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component");
+ fetch::execute('mod_invalid', 1, 'foo', 2);
+ }
+
+ /**
+ * Ensure that an execute with an invalid itemname on a valid component is rejected.
+ */
+ public function test_execute_invalid_itemname(): void {
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component");
+ fetch::execute('mod_forum', 1, 'foo', 2);
+ }
+
+ /**
+ * Ensure that an execute against a different grading method is rejected.
+ */
+ public function test_execute_incorrect_type(): void {
+ $this->resetAfterTest();
+
+ $forum = $this->get_forum_instance([
+ // Negative numbers mean a scale.
+ 'grade_forum' => 5,
+ ]);
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+ $this->expectException(moodle_exception::class);
+ $this->expectExceptionMessage("not configured for advanced grading with a marking guide");
+ fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id);
+ }
+
+ /**
+ * Ensure that an execute against the correct grading method returns the current state of the user.
+ */
+ public function test_execute_fetch_empty(): void {
+ $this->resetAfterTest();
+
+ [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ] = $this->get_test_data();
+
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+ $result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id);
+ $result = external_api::clean_returnvalue(fetch::execute_returns(), $result);
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('templatename', $result);
+
+ $this->assertEquals('gradingform_guide/grades/grader/gradingpanel', $result['templatename']);
+
+ $this->assertArrayHasKey('grade', $result);
+ $this->assertIsArray($result['grade']);
+
+ $this->assertIsInt($result['grade']['timecreated']);
+ $this->assertArrayHasKey('timemodified', $result['grade']);
+ $this->assertIsInt($result['grade']['timemodified']);
+
+ $this->assertArrayHasKey('warnings', $result);
+ $this->assertIsArray($result['warnings']);
+ $this->assertEmpty($result['warnings']);
+
+ $this->assertArrayHasKey('criterion', $result['grade']);
+ $criteria = $result['grade']['criterion'];
+ $this->assertCount(count($definition->guide_criteria), $criteria);
+ foreach ($criteria as $criterion) {
+ $this->assertArrayHasKey('id', $criterion);
+ $criterionid = $criterion['id'];
+ $sourcecriterion = $definition->guide_criteria[$criterionid];
+
+ $this->assertArrayHasKey('name', $criterion);
+ $this->assertEquals($sourcecriterion['shortname'], $criterion['name']);
+
+ $this->assertArrayHasKey('maxscore', $criterion);
+ $this->assertEquals($sourcecriterion['maxscore'], $criterion['maxscore']);
+
+ $this->assertArrayHasKey('description', $criterion);
+ $this->assertEquals($sourcecriterion['description'], $criterion['description']);
+
+ $this->assertArrayHasKey('descriptionmarkers', $criterion);
+ $this->assertEquals($sourcecriterion['descriptionmarkers'], $criterion['descriptionmarkers']);
+
+ $this->assertArrayHasKey('score', $criterion);
+ $this->assertEmpty($criterion['score']);
+
+ $this->assertArrayHasKey('remark', $criterion);
+ $this->assertEmpty($criterion['remark']);
+ }
+ }
+
+ /**
+ * Ensure that an execute against the correct grading method returns the current state of the user.
+ */
+ public function test_execute_fetch_graded(): void {
+ $this->resetAfterTest();
+ $generator = \testing_util::get_data_generator();
+ $guidegenerator = $generator->get_plugin_generator('gradingform_guide');
+
+ [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ] = $this->get_test_data();
+
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+ $grade = $gradeitem->get_grade_for_user($student, $teacher);
+ $instance = $gradeitem->get_advanced_grading_instance($teacher, $grade);
+
+ $submissiondata = $guidegenerator->get_test_form_data($controller, (int) $student->id,
+ 10, 'Propper good speling',
+ 0, 'ASCII art is not a picture'
+ );
+
+ $gradeitem->store_grade_from_formdata($student, $teacher, (object) [
+ 'instanceid' => $instance->get_id(),
+ 'advancedgrading' => $submissiondata,
+ ]);
+
+ $result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id);
+ $result = external_api::clean_returnvalue(fetch::execute_returns(), $result);
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('templatename', $result);
+
+ $this->assertEquals('gradingform_guide/grades/grader/gradingpanel', $result['templatename']);
+
+ $this->assertArrayHasKey('grade', $result);
+ $this->assertIsArray($result['grade']);
+
+ $this->assertIsInt($result['grade']['timecreated']);
+ $this->assertArrayHasKey('timemodified', $result['grade']);
+ $this->assertIsInt($result['grade']['timemodified']);
+
+ $this->assertArrayHasKey('warnings', $result);
+ $this->assertIsArray($result['warnings']);
+ $this->assertEmpty($result['warnings']);
+
+ $this->assertArrayHasKey('criterion', $result['grade']);
+ $criteria = $result['grade']['criterion'];
+ $this->assertCount(count($definition->guide_criteria), $criteria);
+ foreach ($criteria as $criterion) {
+ $this->assertArrayHasKey('id', $criterion);
+ $criterionid = $criterion['id'];
+ $sourcecriterion = $definition->guide_criteria[$criterionid];
+
+ $this->assertArrayHasKey('name', $criterion);
+ $this->assertEquals($sourcecriterion['shortname'], $criterion['name']);
+
+ $this->assertArrayHasKey('maxscore', $criterion);
+ $this->assertEquals($sourcecriterion['maxscore'], $criterion['maxscore']);
+
+ $this->assertArrayHasKey('description', $criterion);
+ $this->assertEquals($sourcecriterion['description'], $criterion['description']);
+
+ $this->assertArrayHasKey('descriptionmarkers', $criterion);
+ $this->assertEquals($sourcecriterion['descriptionmarkers'], $criterion['descriptionmarkers']);
+
+ $this->assertArrayHasKey('score', $criterion);
+ $this->assertArrayHasKey('remark', $criterion);
+ }
+
+ $this->assertEquals(10, $criteria[0]['score']);
+ $this->assertEquals('Propper good speling', $criteria[0]['remark']);
+ $this->assertEquals(0, $criteria[1]['score']);
+ $this->assertEquals('ASCII art is not a picture', $criteria[1]['remark']);
+ }
+
+ /**
+ * Get a forum instance.
+ *
+ * @param array $config
+ * @return forum_entity
+ */
+ protected function get_forum_instance(array $config = []): forum_entity {
+ $this->resetAfterTest();
+
+ $datagenerator = $this->getDataGenerator();
+ $course = $datagenerator->create_course();
+ $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id]));
+
+ $vaultfactory = \mod_forum\local\container::get_vault_factory();
+ $vault = $vaultfactory->get_forum_vault();
+
+ return $vault->get_from_id((int) $forum->id);
+ }
+
+ /**
+ * Get test data for forums graded using a marking guide.
+ *
+ * @return array
+ */
+ protected function get_test_data(): array {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $generator = \testing_util::get_data_generator();
+ $guidegenerator = $generator->get_plugin_generator('gradingform_guide');
+
+ $forum = $this->get_forum_instance();
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ $this->setUser($teacher);
+ $controller = $guidegenerator->get_test_guide($forum->get_context(), 'forum', 'forum');
+ $definition = $controller->get_definition();
+
+ $DB->set_field('forum', 'grade_forum', count($definition->guide_criteria), ['id' => $forum->get_id()]);
+ return [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ];
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Unit tests for core_grades\component_gradeitems;
+ *
+ * @package gradingform_guide
+ * @category test
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_guide\grades\grader\gradingpanel\external;
+
+use advanced_testcase;
+use coding_exception;
+use core_grades\component_gradeitem;
+use external_api;
+use mod_forum\local\entities\forum as forum_entity;
+use moodle_exception;
+
+/**
+ * Unit tests for core_grades\component_gradeitems;
+ *
+ * @package gradingform_guide
+ * @category test
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class store_test extends advanced_testcase {
+
+ public static function setupBeforeClass(): void {
+ global $CFG;
+ require_once("{$CFG->libdir}/externallib.php");
+ }
+
+ /**
+ * Ensure that an execute with an invalid component is rejected.
+ */
+ public function test_execute_invalid_component(): void {
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component");
+ store::execute('mod_invalid', 1, 'foo', 2, 'formdata');
+ }
+
+ /**
+ * Ensure that an execute with an invalid itemname on a valid component is rejected.
+ */
+ public function test_execute_invalid_itemname(): void {
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component");
+ store::execute('mod_forum', 1, 'foo', 2, 'formdata');
+ }
+
+ /**
+ * Ensure that an execute against a different grading method is rejected.
+ */
+ public function test_execute_incorrect_type(): void {
+ $this->resetAfterTest();
+
+ $forum = $this->get_forum_instance([
+ 'grade_forum' => 5,
+ ]);
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+ $this->expectException(moodle_exception::class);
+ $this->expectExceptionMessage("not configured for advanced grading with a marking guide");
+ store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, 'formdata');
+ }
+
+ /**
+ * Ensure that an execute against a different grading method is rejected.
+ */
+ public function test_execute_disabled(): void {
+ $this->resetAfterTest();
+
+ $forum = $this->get_forum_instance();
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+ $this->expectException(moodle_exception::class);
+ $this->expectExceptionMessage("Grading is not enabled");
+ store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, 'formdata');
+ }
+
+ /**
+ * Ensure that an execute against the correct grading method returns the current state of the user.
+ */
+ public function test_execute_store_graded(): void {
+ $this->resetAfterTest();
+ $generator = \testing_util::get_data_generator();
+ $guidegenerator = $generator->get_plugin_generator('gradingform_guide');
+
+ [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ] = $this->get_test_data();
+
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+ $grade = $gradeitem->get_grade_for_user($student, $teacher);
+ $instance = $gradeitem->get_advanced_grading_instance($teacher, $grade);
+
+ $submissiondata = $guidegenerator->get_test_form_data($controller, (int) $student->id,
+ 10, 'Propper good speling',
+ 0, 'ASCII art is not a picture'
+ );
+
+ $formdata = http_build_query((object) [
+ 'instanceid' => $instance->get_id(),
+ 'advancedgrading' => $submissiondata,
+ ], '', '&');
+
+ $result = store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, $formdata);
+ $result = external_api::clean_returnvalue(store::execute_returns(), $result);
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('templatename', $result);
+
+ $this->assertEquals('gradingform_guide/grades/grader/gradingpanel', $result['templatename']);
+
+ $this->assertArrayHasKey('grade', $result);
+ $this->assertIsArray($result['grade']);
+
+ $this->assertIsInt($result['grade']['timecreated']);
+ $this->assertArrayHasKey('timemodified', $result['grade']);
+ $this->assertIsInt($result['grade']['timemodified']);
+
+ $this->assertArrayHasKey('warnings', $result);
+ $this->assertIsArray($result['warnings']);
+ $this->assertEmpty($result['warnings']);
+
+ $this->assertArrayHasKey('criterion', $result['grade']);
+ $criteria = $result['grade']['criterion'];
+ $this->assertCount(count($definition->guide_criteria), $criteria);
+ foreach ($criteria as $criterion) {
+ $this->assertArrayHasKey('id', $criterion);
+ $criterionid = $criterion['id'];
+ $sourcecriterion = $definition->guide_criteria[$criterionid];
+
+ $this->assertArrayHasKey('name', $criterion);
+ $this->assertEquals($sourcecriterion['shortname'], $criterion['name']);
+
+ $this->assertArrayHasKey('maxscore', $criterion);
+ $this->assertEquals($sourcecriterion['maxscore'], $criterion['maxscore']);
+
+ $this->assertArrayHasKey('description', $criterion);
+ $this->assertEquals($sourcecriterion['description'], $criterion['description']);
+
+ $this->assertArrayHasKey('descriptionmarkers', $criterion);
+ $this->assertEquals($sourcecriterion['descriptionmarkers'], $criterion['descriptionmarkers']);
+
+ $this->assertArrayHasKey('score', $criterion);
+ $this->assertArrayHasKey('remark', $criterion);
+ }
+
+ $this->assertEquals(10, $criteria[0]['score']);
+ $this->assertEquals('Propper good speling', $criteria[0]['remark']);
+ $this->assertEquals(0, $criteria[1]['score']);
+ $this->assertEquals('ASCII art is not a picture', $criteria[1]['remark']);
+ }
+
+ /**
+ * Get a forum instance.
+ *
+ * @param array $config
+ * @return forum_entity
+ */
+ protected function get_forum_instance(array $config = []): forum_entity {
+ $this->resetAfterTest();
+
+ $datagenerator = $this->getDataGenerator();
+ $course = $datagenerator->create_course();
+ $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id]));
+
+ $vaultfactory = \mod_forum\local\container::get_vault_factory();
+ $vault = $vaultfactory->get_forum_vault();
+
+ return $vault->get_from_id((int) $forum->id);
+ }
+
+ /**
+ * Get test data for forums graded using a marking guide.
+ *
+ * @return array
+ */
+ protected function get_test_data(): array {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $generator = \testing_util::get_data_generator();
+ $guidegenerator = $generator->get_plugin_generator('gradingform_guide');
+
+ $forum = $this->get_forum_instance();
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ $this->setUser($teacher);
+ $controller = $guidegenerator->get_test_guide($forum->get_context(), 'forum', 'forum');
+ $definition = $controller->get_definition();
+
+ $DB->set_field('forum', 'grade_forum', count($definition->guide_criteria), ['id' => $forum->get_id()]);
+ return [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ];
+ }
+}
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'gradingform_guide';
-$plugin->version = 2019052000;
+$plugin->version = 2019100300;
$plugin->requires = 2019051100;
-$plugin->maturity = MATURITY_STABLE;
\ No newline at end of file
+$plugin->maturity = MATURITY_STABLE;
* @param int $raterid
* @param int $itemid
* @return gradingform_instance
+ * @throws dml_exception
*/
public function get_or_create_instance($instanceid, $raterid, $itemid) {
+ if (!is_numeric($instanceid)) {
+ $instanceid = null;
+ }
+ return $this->fetch_instance($raterid, $itemid, $instanceid);
+ }
+
+ /**
+ * If an instanceid is specified and grading instance exists and it is created by this rater for
+ * this item, then the instance is returned.
+ *
+ * If instanceid is not known, then null can be passed to fetch the current instance matchign the specified raterid
+ * and itemid.
+ *
+ * If the instanceid is falsey, or no instance was found, then create a new instance for the specified rater and item.
+ *
+ * @param int $raterid
+ * @param int $itemid
+ * @param int $instanceid
+ * @return gradingform_instance
+ * @throws dml_exception
+ */
+ public function fetch_instance(int $raterid, int $itemid, ?int $instanceid): gradingform_instance {
global $DB;
- if ($instanceid &&
- $instance = $DB->get_record('grading_instances', array('id' => $instanceid, 'raterid' => $raterid, 'itemid' => $itemid), '*', IGNORE_MISSING)) {
- return $this->get_instance($instance);
+
+ $instance = null;
+ if (null === $instanceid) {
+ if ($instance = $this->get_current_instance($raterid, $itemid)) {
+ return $instance;
+ }
+ $instanceid = $instancerecord->id ?? null;
+ }
+
+ if (!empty($instanceid)) {
+ $instance = $DB->get_record('grading_instances', [
+ 'id' => $instanceid,
+ 'raterid' => $raterid,
+ 'itemid' => $itemid,
+ ], '*', IGNORE_MISSING);
+
+ if ($instance) {
+ return $this->get_instance($instance);
+ }
}
+
return $this->create_instance($raterid, $itemid);
}
--- /dev/null
+// 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/>.
+
+/**
+ * Grading panel for gradingform_rubric.
+ *
+ * @module gradingform_rubric/grades/grader/gradingpanel
+ * @package gradingform_rubric
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+import {call as fetchMany} from 'core/ajax';
+import {normaliseResult} from 'core_grades/grades/grader/gradingpanel/normalise';
+
+
+// Note: We use jQuery.serializer here until we can rewrite Ajax to use XHR.send()
+import jQuery from 'jquery';
+
+/**
+ * For a given component, contextid, itemname & gradeduserid we can fetch the currently assigned grade.
+ *
+ * @param {String} component
+ * @param {Number} contextid
+ * @param {String} itemname
+ * @param {Number} gradeduserid
+ *
+ * @returns {Promise}
+ */
+export const fetchCurrentGrade = (component, contextid, itemname, gradeduserid) => {
+ return fetchMany([{
+ methodname: `gradingform_rubric_grader_gradingpanel_fetch`,
+ args: {
+ component,
+ contextid,
+ itemname,
+ gradeduserid,
+ },
+ }])[0];
+};
+
+/**
+ * For a given component, contextid, itemname & gradeduserid we can store the currently assigned grade in a given form.
+ *
+ * @param {String} component
+ * @param {Number} contextid
+ * @param {String} itemname
+ * @param {Number} gradeduserid
+ * @param {HTMLElement} rootNode
+ *
+ * @returns {Promise}
+ */
+export const storeCurrentGrade = async(component, contextid, itemname, gradeduserid, rootNode) => {
+ const form = rootNode.querySelector('form');
+
+ return normaliseResult(await fetchMany([{
+ methodname: `gradingform_rubric_grader_gradingpanel_store`,
+ args: {
+ component,
+ contextid,
+ itemname,
+ gradeduserid,
+ formdata: jQuery(form).serialize(),
+ },
+ }])[0]);
+};
--- /dev/null
+<?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/>.
+
+/**
+ * Web services relating to fetching of a rubric for the grading panel.
+ *
+ * @package gradingform_rubric
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_rubric\grades\grader\gradingpanel\external;
+
+global $CFG;
+
+use coding_exception;
+use context;
+use core_grades\component_gradeitem as gradeitem;
+use core_grades\component_gradeitems;
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use external_warnings;
+use stdClass;
+use moodle_exception;
+require_once($CFG->dirroot.'/grade/grading/form/rubric/lib.php');
+
+/**
+ * Web services relating to fetching of a rubric for the grading panel.
+ *
+ * @package gradingform_rubric
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fetch extends external_api {
+
+ /**
+ * Describes the parameters for fetching the grading panel for a simple grade.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.8
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters ([
+ 'component' => new external_value(
+ PARAM_ALPHANUMEXT,
+ 'The name of the component',
+ VALUE_REQUIRED
+ ),
+ 'contextid' => new external_value(
+ PARAM_INT,
+ 'The ID of the context being graded',
+ VALUE_REQUIRED
+ ),
+ 'itemname' => new external_value(
+ PARAM_ALPHANUM,
+ 'The grade item itemname being graded',
+ VALUE_REQUIRED
+ ),
+ 'gradeduserid' => new external_value(
+ PARAM_INT,
+ 'The ID of the user show',
+ VALUE_REQUIRED
+ ),
+ ]);
+ }
+
+ /**
+ * Fetch the data required to build a grading panel for a simple grade.
+ *
+ * @param string $component
+ * @param int $contextid
+ * @param string $itemname
+ * @param int $gradeduserid
+ * @return array
+ * @since Moodle 3.8
+ */
+ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid): array {
+ [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ ]);
+
+ // Validate the context.
+ $context = context::instance_by_id($contextid);
+ self::validate_context($context);
+
+ // Validate that the supplied itemname is a gradable item.
+ if (!component_gradeitems::is_valid_itemname($component, $itemname)) {
+ throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component");
+ }
+
+ // Fetch the gradeitem instance.
+ $gradeitem = gradeitem::instance($component, $context, $itemname);
+
+ if (RUBRIC !== $gradeitem->get_advanced_grading_method()) {
+ throw new moodle_exception(
+ "The {$itemname} item in {$component}/{$contextid} is not configured for advanced grading with a rubric"
+ );
+ }
+
+ // Fetch the actual data.
+ $gradeduser = \core_user::get_user($gradeduserid);
+
+ return self::get_fetch_data($gradeitem, $gradeduser);
+ }
+
+ /**
+ * Get the data to be fetched and create the structure ready for Mustache.
+ *
+ * @param gradeitem $gradeitem
+ * @param stdClass $gradeduser
+ * @return array
+ */
+ public static function get_fetch_data(gradeitem $gradeitem, stdClass $gradeduser): array {
+ global $USER;
+
+ // Set up all the controllers etc that we'll be needing.
+ $grade = $gradeitem->get_grade_for_user($gradeduser, $USER);
+ $instance = $gradeitem->get_advanced_grading_instance($USER, $grade);
+ $controller = $instance->get_controller();
+ $definition = $controller->get_definition();
+ $fillings = $instance->get_rubric_filling();
+ $context = $controller->get_context();
+ $definitionid = (int) $definition->id;
+
+ $teacherdescription = self::get_formatted_text(
+ $context,
+ $definitionid,
+ 'description',
+ $definition->description,
+ (int) $definition->descriptionformat
+ );
+
+ $criterion = [];
+ if ($definition->rubric_criteria) {
+ // Iterate over the defined criterion in the rubric and map out what we need to render each item.
+ $criterion = array_map(function($criterion) use ($definitionid, $fillings, $context) {
+ // The general structure we'll be returning, we still need to get the remark (if any) and the levels associated.
+ $result = [
+ 'id' => $criterion['id'],
+ 'description' => self::get_formatted_text(
+ $context,
+ $definitionid,
+ 'description',
+ $criterion['description'],
+ (int) $criterion['descriptionformat']
+ ),
+ ];
+
+ // Do we have an existing grade filling? if so lets get the remark associated to this criteria.
+ $filling = [];
+ if (array_key_exists($criterion['id'], $fillings['criteria'])) {
+ $filling = $fillings['criteria'][$criterion['id']];
+ $result['remark'] = self::get_formatted_text($context,
+ $definitionid,
+ 'remark',
+ $filling['remark'],
+ (int) $filling['remarkformat']
+ );
+ }
+
+ // Lets build the levels within a criteria and figure out what needs to go where.
+ $result['levels'] = array_map(function($level) use ($criterion, $filling, $context, $definitionid) {
+ // The bulk of what'll be returned can be defined easily we'll add to this further down.
+ $result = [
+ 'id' => $level['id'],
+ 'criterionid' => $criterion['id'],
+ 'score' => $level['score'],
+ 'definition' => self::get_formatted_text(
+ $context,
+ $definitionid,
+ 'definition',
+ $level['definition'],
+ (int) $level['definitionformat']
+ ),
+ 'checked' => null,
+ ];
+
+ // Consult the grade filling to see if a level has been selected and if it is the current level.
+ if (array_key_exists('levelid', $filling) && $filling['levelid'] == $level['id']) {
+ $result['checked'] = true;
+ }
+
+ return $result;
+ }, $criterion['levels']);
+
+ $nulllevel = [
+ 'id' => null,
+ 'criterionid' => $criterion['id'],
+ 'score' => '-',
+ 'definition' => 'Not set',
+ 'checked' => null,
+ ];
+ // Consult the grade filling to see if a level has been selected and if it is the current level.
+ if (array_key_exists('levelid', $filling) && $filling['levelid'] == 0) {
+ $nulllevel['checked'] = true;
+ }
+
+ array_unshift($result['levels'], $nulllevel);
+
+ return $result;
+ }, $definition->rubric_criteria);
+ }
+
+ return [
+ 'templatename' => 'gradingform_rubric/grades/grader/gradingpanel',
+ 'grade' => [
+ 'instanceid' => $instance->get_id(),
+ 'criteria' => $criterion,
+ 'rubricmode' => 'evaluate editable',
+ 'teacherdescription' => $teacherdescription,
+ 'canedit' => false,
+ 'timecreated' => $grade->timecreated,
+ 'timemodified' => $grade->timemodified,
+ ],
+ 'warnings' => [],
+ ];
+ }
+
+ /**
+ * Describes the data returned from the external function.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.8
+ */
+ public static function execute_returns(): external_single_structure {
+ return new external_single_structure([
+ 'templatename' => new external_value(PARAM_SAFEPATH, 'The template to use when rendering this data'),
+ 'grade' => new external_single_structure([
+ 'instanceid' => new external_value(PARAM_INT, 'The id of the current grading instance'),
+ 'rubricmode' => new external_value(PARAM_RAW, 'The mode i.e. evaluate editable'),
+ 'canedit' => new external_value(PARAM_BOOL, 'Can the user edit this'),
+ 'criteria' => new external_multiple_structure(
+ new external_single_structure([
+ 'id' => new external_value(PARAM_INT, 'ID of the Criteria'),
+ 'description' => new external_value(PARAM_RAW, 'Description of the Criteria'),
+ 'remark' => new external_value(PARAM_RAW, 'Any remarks for this criterion for the user being assessed', VALUE_OPTIONAL),
+ 'levels' => new external_multiple_structure(new external_single_structure([
+ 'id' => new external_value(PARAM_INT, 'ID of level'),
+ 'criterionid' => new external_value(PARAM_INT, 'ID of the criterion this matches to'),
+ 'score' => new external_value(PARAM_RAW, 'What this level is worth'),
+ 'definition' => new external_value(PARAM_RAW, 'Definition of the level'),
+ 'checked' => new external_value(PARAM_BOOL, 'Selected flag'),
+ ])),
+ ])
+ ),
+ 'timecreated' => new external_value(PARAM_INT, 'The time that the grade was created'),
+ 'timemodified' => new external_value(PARAM_INT, 'The time that the grade was last updated'),
+ ]),
+ 'warnings' => new external_warnings(),
+ ]);
+ }
+
+ /**
+ * Get a formatted version of the remark/description/etc.
+ *
+ * @param context $context
+ * @param int $definitionid
+ * @param string $filearea The file area of the field
+ * @param string $text The text to be formatted
+ * @param int $format The input format of the string
+ * @return string
+ */
+ protected static function get_formatted_text(context $context, int $definitionid, string $filearea, string $text, int $format): string {
+ $formatoptions = [
+ 'noclean' => false,
+ 'trusted' => false,
+ 'filter' => true,
+ ];
+ [$newtext, ] = external_format_text($text, $format, $context, 'grading', $filearea, $definitionid, $formatoptions);
+ return $newtext;
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Web services relating to fetching of a rubric for the grading panel.
+ *
+ * @package gradingform_rubric
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_rubric\grades\grader\gradingpanel\external;
+
+global $CFG;
+
+use coding_exception;
+use context;
+use core_grades\component_gradeitem as gradeitem;
+use core_grades\component_gradeitems;
+use external_api;
+use external_function_parameters;
+use external_single_structure;
+use external_value;
+use moodle_exception;
+require_once($CFG->dirroot.'/grade/grading/form/rubric/lib.php');
+
+/**
+ * Web services relating to storing of a rubric for the grading panel.
+ *
+ * @package gradingform_rubric
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class store extends external_api {
+
+ /**
+ * Describes the parameters for storing the grading panel for a simple grade.
+ *
+ * @return external_function_parameters
+ * @since Moodle 3.8
+ */
+ public static function execute_parameters(): external_function_parameters {
+ return new external_function_parameters ([
+ 'component' => new external_value(
+ PARAM_ALPHANUMEXT,
+ 'The name of the component',
+ VALUE_REQUIRED
+ ),
+ 'contextid' => new external_value(
+ PARAM_INT,
+ 'The ID of the context being graded',
+ VALUE_REQUIRED
+ ),
+ 'itemname' => new external_value(
+ PARAM_ALPHANUM,
+ 'The grade item itemname being graded',
+ VALUE_REQUIRED
+ ),
+ 'gradeduserid' => new external_value(
+ PARAM_INT,
+ 'The ID of the user show',
+ VALUE_REQUIRED
+ ),
+ 'formdata' => new external_value(
+ PARAM_RAW,
+ 'The serialised form data representing the grade',
+ VALUE_REQUIRED
+ ),
+ ]);
+ }
+
+ /**
+ * Fetch the data required to build a grading panel for a simple grade.
+ *
+ * @param string $component
+ * @param int $contextid
+ * @param string $itemname
+ * @param int $gradeduserid
+ * @param string $formdata
+ * @return array
+ * @since Moodle 3.8
+ */
+ public static function execute(string $component, int $contextid, string $itemname, int $gradeduserid, string $formdata): array {
+ global $USER;
+
+ [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ 'formdata' => $formdata,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'component' => $component,
+ 'contextid' => $contextid,
+ 'itemname' => $itemname,
+ 'gradeduserid' => $gradeduserid,
+ 'formdata' => $formdata,
+ ]);
+
+ // Validate the context.
+ $context = context::instance_by_id($contextid);
+ self::validate_context($context);
+
+ // Validate that the supplied itemname is a gradable item.
+ if (!component_gradeitems::is_valid_itemname($component, $itemname)) {
+ throw new coding_exception("The '{$itemname}' item is not valid for the '{$component}' component");
+ }
+
+ // Fetch the gradeitem instance.
+ $gradeitem = gradeitem::instance($component, $context, $itemname);
+
+ // Validate that this gradeitem is actually enabled.
+ if (!$gradeitem->is_grading_enabled()) {
+ throw new moodle_exception("Grading is not enabled for {$itemname} in this context");
+ }
+
+ // Fetch the record for the graded user.
+ $gradeduser = \core_user::get_user($gradeduserid);
+
+ // Require that this user can save grades.
+ $gradeitem->require_user_can_grade($gradeduser, $USER);
+
+ if (RUBRIC !== $gradeitem->get_advanced_grading_method()) {
+ throw new moodle_exception(
+ "The {$itemname} item in {$component}/{$contextid} is not configured for advanced grading with a rubric"
+ );
+ }
+
+ // Parse the serialised string into an object.
+ $data = [];
+ parse_str($formdata, $data);
+
+ // Grade.
+ $gradeitem->store_grade_from_formdata($gradeduser, $USER, (object) $data);
+
+ // Fetch the updated grade back out.
+ $grade = $gradeitem->get_grade_for_user($gradeduser, $USER);
+
+ return fetch::get_fetch_data($gradeitem, $gradeduser);
+ }
+
+ /**
+ * Describes the data returned from the external function.
+ *
+ * @return external_single_structure
+ * @since Moodle 3.8
+ */
+ public static function execute_returns(): external_single_structure {
+ return fetch::execute_returns();
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Rubric external functions and service definitions.
+ *
+ * @package gradingform_rubric
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die;
+
+$functions = [
+ 'gradingform_rubric_grader_gradingpanel_fetch' => [
+ 'classname' => 'gradingform_rubric\\grades\\grader\\gradingpanel\\external\\fetch',
+ 'methodname' => 'execute',
+ 'description' => 'Fetch the data required to display the grader grading panel, ' .
+ 'creating the grade item if required',
+ 'type' => 'write',
+ 'ajax' => true,
+ ],
+ 'gradingform_rubric_grader_gradingpanel_store' => [
+ 'classname' => 'gradingform_rubric\\grades\\grader\\gradingpanel\\external\\store',
+ 'methodname' => 'execute',
+ 'description' => 'Store the grading data for a user from the grader grading panel.',
+ 'type' => 'write',
+ 'ajax' => true,
+ ],
+];
+
+
$string['name'] = 'Name';
$string['needregrademessage'] = 'The rubric definition was changed after this student had been graded. The student can not see this rubric until you check the rubric and update the grade.';
$string['pluginname'] = 'Rubric';
+$string['pointsvalue'] = '{$a} points';
$string['previewrubric'] = 'Preview rubric';
$string['privacy:metadata:criterionid'] = 'An identifier for a specific criterion being graded.';
$string['privacy:metadata:fillingssummary'] = 'Stores information about the user\'s grade created by the rubric.';
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/grade/grading/form/lib.php');
+require_once($CFG->dirroot.'/lib/filelib.php');
+
+/** rubric: Used to compare our gradeitem_type against. */
+const RUBRIC = 'rubric';
/**
* This controller encapsulates the rubric grading logic
position: relative;
float: right;
}
+
+.gradingpanel-gradingform_rubric [aria-checked="true"] {
+ border: 1px solid black;
+}
--- /dev/null
+{{!
+ 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/>.
+}}
+{{!
+ @template gradingform_rubric/grades/grader/gradingpanel
+
+ Classes required for JS:
+ * none
+
+ Data attributes required for JS:
+ * none
+
+ Context variables required for this template:
+ * instanceid: Instance of the module this grading form belongs too
+ * criteria: A gradeable item in the Marking Guide
+ * id: The ID of the criteria
+ * description: Description of the criteria
+ * levels: The level that a criteria can be graded at
+ * criterionid: The ID of the criteria
+ * checked: Flag for if this is the currently selected level
+ * definition: Definition of the level
+ * remark: Text input for the teacher to relay to the student
+
+ Example context (json):
+ {
+ "instanceid": "42",
+ "criteria": [
+ {
+ "id": 13,
+ "description": "Show your motivation to rock climbing",
+ "levels": [
+ {
+ "criterionid": 13,
+ "checked": true,
+ "definition": "Great work!"
+ }
+ ],
+ "remark": "That's great!"
+ }
+ ]
+ }
+}}
+<form id="gradingform_rubric-{{uniqid}}">
+ <input type="hidden" name="instanceid" value="{{instanceid}}">
+ <div id="rubric-advancedgrading-{{uniqid}}" class="criterion">
+ {{#criteria}}
+ <div class="d-block mb-2">
+ <h5 class="d-inline px-0 font-weight-bold mb-0">{{{description}}}</h5>
+ <button class="d-inline btn p-0 font-weight-bold mb-0 pull-right collapse"
+ type="button"
+ data-toggle="collapse"
+ data-target="#criteria-{{id}}"
+ aria-expanded="true"
+ aria-controls="criteria-{{id}}">
+ </button>
+ </div>
+ <div class="collapse show" id="criteria-{{id}}">
+ {{#levels}}
+ <div class="form-check">
+ <input class="form-check-input level"
+ type="radio"
+ name="advancedgrading[criteria][{{criterionid}}][levelid]"
+ id="advancedgrading-criteria-{{criterionid}}-levels-{{id}}-definition"
+ value="{{id}}"
+ {{#checked}}
+ aria-checked="true"
+ tabindex="0"
+ checked
+ {{/checked}}
+ {{^checked}}
+ aria-checked="false"
+ tabindex="-1"
+ {{/checked}}
+ >
+ <label class="w-100" for="advancedgrading-criteria-{{criterionid}}-levels-{{id}}-definition">
+ <label class="font-weight-bold">
+ {{{definition}}}
+ </label>
+ <label class="pull-right font-weight-bold">
+ {{#str}}pointsvalue, gradingform_rubric, {{score}}{{/str}}
+ </label>
+ </label>
+ </div>
+ {{/levels}}
+ <div class="form-group">
+ <label for="advancedgrading-criteria-{{id}}-remark">Additional feedback</label>
+ <textarea class="form-control" name="advancedgrading[criteria][{{id}}][remark]" id="advancedgrading-criteria-{{id}}-remark" cols="10" rows="5">{{{remark}}}</textarea>
+ </div>
+ </div>
+ {{/criteria}}
+ </div>
+</form>
--- /dev/null
+<?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/>.
+
+/**
+ * Coverage information for the gradingform_rubric plugin.
+ *
+ * @package gradingform_rubric
+ * @category test
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Coverage information for the gradingform_rubric plugin.
+ *
+ * @package gradingform_rubric
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+return new class extends phpunit_coverage_info {
+ // Array The list of folders relative to the plugin root to whitelist in coverage generation.
+ protected $whitelistfolders = [
+ 'classes',
+ 'tests/generator',
+ ];
+};
--- /dev/null
+<?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/>.
+
+/**
+ * Unit tests for core_grades\component_gradeitems;
+ *
+ * @package gradingform_rubric
+ * @category test
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_rubric\grades\grader\gradingpanel\external;
+
+use advanced_testcase;
+use coding_exception;
+use core_grades\component_gradeitem;
+use external_api;
+use mod_forum\local\entities\forum as forum_entity;
+use moodle_exception;
+
+/**
+ * Unit tests for core_grades\component_gradeitems;
+ *
+ * @package gradingform_rubric
+ * @category test
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class fetch_test extends advanced_testcase {
+
+ public static function setupBeforeClass(): void {
+ global $CFG;
+ require_once("{$CFG->libdir}/externallib.php");
+ }
+
+ /**
+ * Ensure that an execute with an invalid component is rejected.
+ */
+ public function test_execute_invalid_component(): void {
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component");
+ fetch::execute('mod_invalid', 1, 'foo', 2);
+ }
+
+ /**
+ * Ensure that an execute with an invalid itemname on a valid component is rejected.
+ */
+ public function test_execute_invalid_itemname(): void {
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component");
+ fetch::execute('mod_forum', 1, 'foo', 2);
+ }
+
+ /**
+ * Ensure that an execute against a different grading method is rejected.
+ */
+ public function test_execute_incorrect_type(): void {
+ $this->resetAfterTest();
+
+ $forum = $this->get_forum_instance([
+ // Negative numbers mean a scale.
+ 'grade_forum' => 5,
+ ]);
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+ $this->expectException(moodle_exception::class);
+ $this->expectExceptionMessage("not configured for advanced grading with a rubric");
+ fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id);
+ }
+
+ /**
+ * Ensure that an execute against the correct grading method returns the current state of the user.
+ */
+ public function test_execute_fetch_empty(): void {
+ $this->resetAfterTest();
+
+ [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ] = $this->get_test_data();
+
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+ $result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id);
+ $result = external_api::clean_returnvalue(fetch::execute_returns(), $result);
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('templatename', $result);
+
+ $this->assertEquals('gradingform_rubric/grades/grader/gradingpanel', $result['templatename']);
+
+ $this->assertArrayHasKey('grade', $result);
+ $this->assertIsArray($result['grade']);
+
+ $this->assertIsInt($result['grade']['timecreated']);
+ $this->assertArrayHasKey('timemodified', $result['grade']);
+ $this->assertIsInt($result['grade']['timemodified']);
+
+ $this->assertArrayHasKey('warnings', $result);
+ $this->assertIsArray($result['warnings']);
+ $this->assertEmpty($result['warnings']);
+
+ $this->assertArrayHasKey('criteria', $result['grade']);
+ $criteria = $result['grade']['criteria'];
+ $this->assertCount(count($definition->rubric_criteria), $criteria);
+ foreach ($criteria as $criterion) {
+ $this->assertArrayHasKey('id', $criterion);
+ $criterionid = $criterion['id'];
+ $sourcecriterion = $definition->rubric_criteria[$criterionid];
+
+ $this->assertArrayHasKey('description', $criterion);
+ $this->assertEquals($sourcecriterion['description'], $criterion['description']);
+
+ $this->assertArrayHasKey('levels', $criterion);
+
+ $levels = $criterion['levels'];
+ foreach ($levels as $level) {
+ $levelid = $level['id'];
+ if (!isset($levelid)) {
+ continue;
+ }
+ $sourcelevel = $sourcecriterion['levels'][$levelid];
+
+ $this->assertArrayHasKey('criterionid', $level);
+ $this->assertEquals($criterionid, $level['criterionid']);
+
+ $this->assertArrayHasKey('checked', $level);
+
+ $this->assertArrayHasKey('definition', $level);
+ $this->assertEquals($sourcelevel['definition'], $level['definition']);
+
+ $this->assertArrayHasKey('score', $level);
+ $this->assertEquals($sourcelevel['score'], $level['score']);
+ }
+ }
+ }
+
+ /**
+ * Ensure that an execute against the correct grading method returns the current state of the user.
+ */
+ public function test_execute_fetch_graded(): void {
+ $this->resetAfterTest();
+ $generator = \testing_util::get_data_generator();
+ $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
+
+ [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ] = $this->get_test_data();
+
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+ $grade = $gradeitem->get_grade_for_user($student, $teacher);
+ $instance = $gradeitem->get_advanced_grading_instance($teacher, $grade);
+
+ $submissiondata = $rubricgenerator->get_test_form_data($controller, (int) $student->id,
+ 0, 'Too many mistakes. Please try again.',
+ 2, 'Great number of pictures. Well done.'
+ );
+
+ $gradeitem->store_grade_from_formdata($student, $teacher, (object) [
+ 'instanceid' => $instance->get_id(),
+ 'advancedgrading' => $submissiondata,
+ ]);
+
+ $result = fetch::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id);
+ $result = external_api::clean_returnvalue(fetch::execute_returns(), $result);
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('templatename', $result);
+
+ $this->assertEquals('gradingform_rubric/grades/grader/gradingpanel', $result['templatename']);
+
+ $this->assertArrayHasKey('grade', $result);
+ $this->assertIsArray($result['grade']);
+
+ $this->assertIsInt($result['grade']['timecreated']);
+ $this->assertArrayHasKey('timemodified', $result['grade']);
+ $this->assertIsInt($result['grade']['timemodified']);
+
+ $this->assertArrayHasKey('warnings', $result);
+ $this->assertIsArray($result['warnings']);
+ $this->assertEmpty($result['warnings']);
+
+ $this->assertArrayHasKey('criteria', $result['grade']);
+ $criteria = $result['grade']['criteria'];
+ $this->assertCount(count($definition->rubric_criteria), $criteria);
+ foreach ($criteria as $criterion) {
+ $this->assertArrayHasKey('id', $criterion);
+ $criterionid = $criterion['id'];
+ $sourcecriterion = $definition->rubric_criteria[$criterionid];
+
+ $this->assertArrayHasKey('description', $criterion);
+ $this->assertEquals($sourcecriterion['description'], $criterion['description']);
+
+ $this->assertArrayHasKey('remark', $criterion);
+
+ $this->assertArrayHasKey('levels', $criterion);
+
+ $levels = $criterion['levels'];
+ foreach ($levels as $level) {
+ $levelid = $level['id'];
+ if (!isset($levelid)) {
+ continue;
+ }
+ $sourcelevel = $sourcecriterion['levels'][$levelid];
+
+ $this->assertArrayHasKey('criterionid', $level);
+ $this->assertEquals($criterionid, $level['criterionid']);
+
+ $this->assertArrayHasKey('checked', $level);
+
+ $this->assertArrayHasKey('definition', $level);
+ $this->assertEquals($sourcelevel['definition'], $level['definition']);
+
+ $this->assertArrayHasKey('score', $level);
+ $this->assertEquals($sourcelevel['score'], $level['score']);
+ }
+
+ }
+
+ $this->assertEquals(1, $criteria[0]['levels'][1]['checked']);
+ $this->assertEquals('Too many mistakes. Please try again.', $criteria[0]['remark']);
+ $this->assertEquals(1, $criteria[1]['levels'][3]['checked']);
+ $this->assertEquals('Great number of pictures. Well done.', $criteria[1]['remark']);
+ }
+
+ /**
+ * Get a forum instance.
+ *
+ * @param array $config
+ * @return forum_entity
+ */
+ protected function get_forum_instance(array $config = []): forum_entity {
+ $this->resetAfterTest();
+
+ $datagenerator = $this->getDataGenerator();
+ $course = $datagenerator->create_course();
+ $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id]));
+
+ $vaultfactory = \mod_forum\local\container::get_vault_factory();
+ $vault = $vaultfactory->get_forum_vault();
+
+ return $vault->get_from_id((int) $forum->id);
+ }
+
+ /**
+ * Get test data for forums graded using a rubric.
+ *
+ * @return array
+ */
+ protected function get_test_data(): array {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $generator = \testing_util::get_data_generator();
+ $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
+
+ $forum = $this->get_forum_instance();
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ $this->setUser($teacher);
+ $controller = $rubricgenerator->get_test_rubric($forum->get_context(), 'forum', 'forum');
+ $definition = $controller->get_definition();
+
+ $DB->set_field('forum', 'grade_forum', count($definition->rubric_criteria), ['id' => $forum->get_id()]);
+ return [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ];
+ }
+}
--- /dev/null
+<?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/>.
+
+/**
+ * Unit tests for core_grades\component_gradeitems;
+ *
+ * @package gradingform_rubric
+ * @category test
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+declare(strict_types = 1);
+
+namespace gradingform_rubric\grades\grader\gradingpanel\external;
+
+use advanced_testcase;
+use coding_exception;
+use core_grades\component_gradeitem;
+use external_api;
+use mod_forum\local\entities\forum as forum_entity;
+use moodle_exception;
+
+/**
+ * Unit tests for core_grades\component_gradeitems;
+ *
+ * @package gradingform_rubric
+ * @category test
+ * @copyright 2019 Mathew May <mathew.solutions>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class store_test extends advanced_testcase {
+
+ public static function setupBeforeClass(): void {
+ global $CFG;
+ require_once("{$CFG->libdir}/externallib.php");
+ }
+
+ /**
+ * Ensure that an execute with an invalid component is rejected.
+ */
+ public function test_execute_invalid_component(): void {
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_invalid' component");
+ store::execute('mod_invalid', 1, 'foo', 2, 'formdata');
+ }
+
+ /**
+ * Ensure that an execute with an invalid itemname on a valid component is rejected.
+ */
+ public function test_execute_invalid_itemname(): void {
+ $this->resetAfterTest();
+ $user = $this->getDataGenerator()->create_user();
+ $this->setUser($user);
+
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("The 'foo' item is not valid for the 'mod_forum' component");
+ store::execute('mod_forum', 1, 'foo', 2, 'formdata');
+ }
+
+ /**
+ * Ensure that an execute against a different grading method is rejected.
+ */
+ public function test_execute_incorrect_type(): void {
+ $this->resetAfterTest();
+
+ $forum = $this->get_forum_instance([
+ 'grade_forum' => 5,
+ ]);
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+ $this->expectException(moodle_exception::class);
+ store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, 'formdata');
+ }
+
+ /**
+ * Ensure that an execute against a different grading method is rejected.
+ */
+ public function test_execute_disabled(): void {
+ $this->resetAfterTest();
+
+ $forum = $this->get_forum_instance();
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+
+ $this->expectException(moodle_exception::class);
+ $this->expectExceptionMessage("Grading is not enabled");
+ store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, 'formdata');
+ }
+
+ /**
+ * Ensure that an execute against the correct grading method returns the current state of the user.
+ */
+ public function test_execute_store_graded(): void {
+ $this->resetAfterTest();
+ $generator = \testing_util::get_data_generator();
+ $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
+
+ [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ] = $this->get_test_data();
+
+ $this->setUser($teacher);
+
+ $gradeitem = component_gradeitem::instance('mod_forum', $forum->get_context(), 'forum');
+ $grade = $gradeitem->get_grade_for_user($student, $teacher);
+ $instance = $gradeitem->get_advanced_grading_instance($teacher, $grade);
+
+ $submissiondata = $rubricgenerator->get_test_form_data($controller, (int) $student->id,
+ 0, 'Too many mistakes. Please try again.',
+ 2, 'Great number of pictures. Well done.'
+ );
+
+ $formdata = http_build_query((object) [
+ 'instanceid' => $instance->get_id(),
+ 'advancedgrading' => $submissiondata,
+ ], '', '&');
+
+ $result = store::execute('mod_forum', (int) $forum->get_context()->id, 'forum', (int) $student->id, $formdata);
+ $result = external_api::clean_returnvalue(store::execute_returns(), $result);
+
+ $this->assertIsArray($result);
+ $this->assertArrayHasKey('templatename', $result);
+
+ $this->assertEquals('gradingform_rubric/grades/grader/gradingpanel', $result['templatename']);
+
+ $this->assertArrayHasKey('grade', $result);
+ $this->assertIsArray($result['grade']);
+
+ $this->assertIsInt($result['grade']['timecreated']);
+ $this->assertArrayHasKey('timemodified', $result['grade']);
+ $this->assertIsInt($result['grade']['timemodified']);
+
+ $this->assertArrayHasKey('warnings', $result);
+ $this->assertIsArray($result['warnings']);
+ $this->assertEmpty($result['warnings']);
+
+ $this->assertArrayHasKey('criteria', $result['grade']);
+ $criteria = $result['grade']['criteria'];
+ $this->assertCount(count($definition->rubric_criteria), $criteria);
+ foreach ($criteria as $criterion) {
+ $this->assertArrayHasKey('id', $criterion);
+ $criterionid = $criterion['id'];
+ $sourcecriterion = $definition->rubric_criteria[$criterionid];
+
+ $this->assertArrayHasKey('description', $criterion);
+ $this->assertEquals($sourcecriterion['description'], $criterion['description']);
+
+ $this->assertArrayHasKey('remark', $criterion);
+
+ $this->assertArrayHasKey('levels', $criterion);
+
+ $levels = $criterion['levels'];
+ foreach ($levels as $level) {
+ $levelid = $level['id'];
+ if (!isset($levelid)) {
+ continue;
+ }
+ $sourcelevel = $sourcecriterion['levels'][$levelid];
+
+ $this->assertArrayHasKey('criterionid', $level);
+ $this->assertEquals($criterionid, $level['criterionid']);
+
+ $this->assertArrayHasKey('checked', $level);
+
+ $this->assertArrayHasKey('definition', $level);
+ $this->assertEquals($sourcelevel['definition'], $level['definition']);
+
+ $this->assertArrayHasKey('score', $level);
+ $this->assertEquals($sourcelevel['score'], $level['score']);
+ }
+
+ }
+
+ $this->assertEquals(1, $criteria[0]['levels'][1]['checked']);
+ $this->assertEquals('Too many mistakes. Please try again.', $criteria[0]['remark']);
+ $this->assertEquals(1, $criteria[1]['levels'][3]['checked']);
+ $this->assertEquals('Great number of pictures. Well done.', $criteria[1]['remark']);
+ }
+
+ /**
+ * Get a forum instance.
+ *
+ * @param array $config
+ * @return forum_entity
+ */
+ protected function get_forum_instance(array $config = []): forum_entity {
+ $this->resetAfterTest();
+
+ $datagenerator = $this->getDataGenerator();
+ $course = $datagenerator->create_course();
+ $forum = $datagenerator->create_module('forum', array_merge($config, ['course' => $course->id]));
+
+ $vaultfactory = \mod_forum\local\container::get_vault_factory();
+ $vault = $vaultfactory->get_forum_vault();
+
+ return $vault->get_from_id((int) $forum->id);
+ }
+
+ /**
+ * Get test data for forums graded using a rubric.
+ *
+ * @return array
+ */
+ protected function get_test_data(): array {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $generator = \testing_util::get_data_generator();
+ $rubricgenerator = $generator->get_plugin_generator('gradingform_rubric');
+
+ $forum = $this->get_forum_instance();
+ $course = $forum->get_course_record();
+ $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
+ $student = $this->getDataGenerator()->create_and_enrol($course, 'student');
+
+ $this->setUser($teacher);
+ $controller = $rubricgenerator->get_test_rubric($forum->get_context(), 'forum', 'forum');
+ $definition = $controller->get_definition();
+
+ $DB->set_field('forum', 'grade_forum', count($definition->rubric_criteria), ['id' => $forum->get_id()]);
+ return [
+ 'forum' => $forum,
+ 'controller' => $controller,
+ 'definition' => $definition,
+ 'student' => $student,
+ 'teacher' => $teacher,
+ ];
+ }
+}
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'gradingform_rubric';
-$plugin->version = 2019052000;
+$plugin->version = 2019052006;
$plugin->requires = 2019051100;
defined('MOODLE_INTERNAL') || die();
+use core_grades\component_gradeitems;
+
/**
* Factory method returning an instance of the 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');
}
--- /dev/null
+<?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/>.
+
+/**
+ * Coverage information for the core_grading subsystem.
+ *
+ * @package core_grading
+ * @category test
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Coverage information for the core_grading subsystem.
+ *
+ * @package core_grading
+ * @copyright 2019 Andrew Nicols <andrew@nicols.co.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+return new class extends phpunit_coverage_info {
+ // Array The list of folders relative to the plugin root to whitelist in coverage generation.
+ protected $whitelistfolders = [
+ 'classes',
+ 'tests/generator',
+ ];
+};
--- /dev/null
+{{!
+ 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/>.
+}}
+{{!
+ @template core_grades/grades/grader/gradingpanel/point
+
+ Point-based grading template for use in the grading panel.
+
+ Context variables required for this template:
+
+ Example context (json):
+ {
+ "grade": 47
+ }
+}}
+<form>
+ <div class="form-group">
+ <label for="core_grades-grade-{{uniqid}}">{{#str}}grade, moodle{{/str}}</label>
+ <input class="form-control" type="number" name="grade" value="{{grade}}" id="core_grades-grade-{{uniqid}}" aria-describedby="core_grades-help-{{uniqid}}">
+ <small id="core_grades-help-{{uniqid}}" class="form-text text-muted">{{#str}}grade_help, core_grades{{/str}}</small>
+ </div>
+</form>
--- /dev/null
+{{!
+ 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/>.
+}}
+{{!
+ @template core_grades/grades/grader/gradingpanel/scale
+
+ Scale-based grading template for use in the grading panel.
+
+ Context variables required for this template:
+
+ Example context (json):
+ {
+ "value": 1,
+ "selected": true,
+ "title": "Motivational"
+ }
+}}
+<form>
+ <div class="form-group">
+ <label for="core_grades-grade-{{uniqid}}">{{#str}}grade, moodle{{/str}}</label>
+ <select class="form-control" name="grade" id="core_grades-grade-{{uniqid}}" aria-describedby="core_grades-help-{{uniqid}}">
+ <option value="-1">{{#str}} nograde, moodle{{/str}}</option>
+ {{#options}}
+ <option value="{{value}}" {{#selected}}selected{{/selected}}>{{title}}</option>
+ {{/options}}
+ </select>
+ <small id="core_grades-help-{{uniqid}}" class="form-text text-muted">{{#str}}grade_help, core_grades{{/str}}</small>
+ </div>
+</form>