MDL-68241 mod_h5pactivity: add grading attempts options

This commit is contained in:
Ferran Recio
2020-05-14 09:13:56 +02:00
parent 206e179df5
commit e28b40695d
20 changed files with 1754 additions and 68 deletions
@@ -41,7 +41,7 @@ class backup_h5pactivity_activity_structure_step extends backup_activity_structu
// Replace with the attributes and final elements that the element will handle.
$attributes = ['id'];
$finalelements = ['name', 'timecreated', 'timemodified', 'intro',
'introformat', 'grade', 'displayoptions'];
'introformat', 'grade', 'displayoptions', 'enabletracking', 'grademethod'];
$root = new backup_nested_element('h5pactivity', $attributes, $finalelements);
$attempts = new backup_nested_element('attempts');
+76 -10
View File
@@ -36,18 +36,22 @@ defined('MOODLE_INTERNAL') || die();
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt {
/** @var stdClass the h5pactivity_attempts record. */
private $record;
/** @var boolean if the DB statement has been updated. */
private $scoreupdated = false;
/**
* Create a new attempt object.
*
* @param stdClass $record the h5pactivity_attempts record
*/
protected function __construct(stdClass $record) {
public function __construct(stdClass $record) {
$this->record = $record;
$this->results = null;
}
@@ -199,12 +203,15 @@ class attempt {
}
// If no subcontent provided, results are propagated to the attempt itself.
if (empty($subcontent) && $record->rawscore) {
$this->record->rawscore = $record->rawscore;
$this->record->maxscore = $record->maxscore;
$this->record->duration = $record->duration;
$this->record->completion = $record->completion ?? null;
$this->record->success = $record->success ?? null;
if (empty($subcontent)) {
$this->set_duration($record->duration);
$this->set_completion($record->completion ?? null);
$this->set_success($record->success ?? null);
// If Maxscore is not empty means that the rawscore is valid (even if it's 0)
// and scaled score can be calculated.
if ($record->maxscore) {
$this->set_score($record->rawscore, $record->maxscore);
}
}
// Refresh current attempt.
return $this->save();
@@ -218,9 +225,56 @@ class attempt {
public function save(): bool {
global $DB;
$this->record->timemodified = time();
// Calculate scaled score.
if ($this->scoreupdated) {
if (empty($this->record->maxscore)) {
$this->record->scaled = 0;
} else {
$this->record->scaled = $this->record->rawscore / $this->record->maxscore;
}
}
return $DB->update_record('h5pactivity_attempts', $this->record);
}
/**
* Set the attempt score.
*
* @param int|null $rawscore the attempt rawscore
* @param int|null $maxscore the attempt maxscore
*/
public function set_score(?int $rawscore, ?int $maxscore): void {
$this->record->rawscore = $rawscore;
$this->record->maxscore = $maxscore;
$this->scoreupdated = true;
}
/**
* Set the attempt duration.
*
* @param int|null $duration the attempt duration
*/
public function set_duration(?int $duration): void {
$this->record->duration = $duration;
}
/**
* Set the attempt completion.
*
* @param int|null $completion the attempt completion
*/
public function set_completion(?int $completion): void {
$this->record->completion = $completion;
}
/**
* Set the attempt success.
*
* @param int|null $success the attempt success
*/
public function set_success(?int $success): void {
$this->record->success = $success;
}
/**
* Delete the current attempt results from the DB.
*/
@@ -376,15 +430,15 @@ class attempt {
* @return int the rawscore value
*/
public function get_rawscore(): int {
return $this->record->maxscore;
return $this->record->rawscore;
}
/**
* Return the attempt duration.
*
* @return int the duration value
* @return int|null the duration value
*/
public function get_duration(): int {
public function get_duration(): ?int {
return $this->record->duration;
}
@@ -405,4 +459,16 @@ class attempt {
public function get_success(): ?int {
return $this->record->success;
}
/**
* Return if the attempt has been modified.
*
* Note: adding a result only add track information unless the statement does
* not specify subcontent. In this case this will update also the statement.
*
* @return bool if the attempt score have been modified
*/
public function get_scoreupdated(): bool {
return $this->scoreupdated;
}
}
+214
View File
@@ -0,0 +1,214 @@
<?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/>.
/**
* H5P activity grader class.
*
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\local;
use context_module;
use cm_info;
use moodle_recordset;
use stdClass;
/**
* Class for handling H5P activity grading.
*
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class grader {
/** @var stdClass course_module record. */
private $instance;
/** @var string idnumber course_modules idnumber. */
private $idnumber;
/**
* Class contructor.
*
* @param stdClass $instance H5Pactivity instance object
* @param string $idnumber course_modules idnumber
*/
public function __construct(stdClass $instance, string $idnumber = '') {
$this->instance = $instance;
$this->idnumber = $idnumber;
}
/**
* Delete grade item for given mod_h5pactivity instance.
*
* @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED
*/
public function grade_item_delete(): ?int {
global $CFG;
require_once($CFG->libdir.'/gradelib.php');
return grade_update('mod/h5pactivity', $this->instance->course, 'mod', 'h5pactivity',
$this->instance->id, 0, null, ['deleted' => 1]);
}
/**
* Creates or updates grade item for the given mod_h5pactivity instance.
*
* @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
* @return int 0 if ok, error code otherwise
*/
public function grade_item_update($grades = null): int {
global $CFG;
require_once($CFG->libdir.'/gradelib.php');
$item = [];
$item['itemname'] = clean_param($this->instance->name, PARAM_NOTAGS);
$item['gradetype'] = GRADE_TYPE_VALUE;
if (!empty($this->idnumber)) {
$item['idnumber'] = $this->idnumber;
}
if ($this->instance->grade > 0) {
$item['gradetype'] = GRADE_TYPE_VALUE;
$item['grademax'] = $this->instance->grade;
$item['grademin'] = 0;
} else if ($this->instance->grade < 0) {
$item['gradetype'] = GRADE_TYPE_SCALE;
$item['scaleid'] = -$this->instance->grade;
} else {
$item['gradetype'] = GRADE_TYPE_NONE;
}
if ($grades === 'reset') {
$item['reset'] = true;
$grades = null;
}
return grade_update('mod/h5pactivity', $this->instance->course, 'mod',
'h5pactivity', $this->instance->id, 0, $grades, $item);
}
/**
* Update grades in the gradebook.
*
* @param int $userid Update grade of specific user only, 0 means all participants.
*/
public function update_grades(int $userid = 0): void {
// Scaled and none grading doesn't have grade calculation.
if ($this->instance->grade <= 0) {
$this->grade_item_update();
return;
}
// Populate array of grade objects indexed by userid.
$grades = $this->get_user_grades_for_gradebook($userid);
if (!empty($grades)) {
$this->grade_item_update($grades);
} else {
$this->grade_item_update();
}
}
/**
* Get an updated list of user grades and feedback for the gradebook.
*
* @param int $userid int or 0 for all users
* @return array of grade data formated for the gradebook api
* The data required by the gradebook api is userid,
* rawgrade,
* feedback,
* feedbackformat,
* usermodified,
* dategraded,
* datesubmitted
*/
private function get_user_grades_for_gradebook(int $userid = 0): array {
$grades = [];
// In case of using manual grading this update must delete previous automatic gradings.
if ($this->instance->grademethod == manager::GRADEMANUAL || !$this->instance->enabletracking) {
return $this->get_user_grades_for_deletion($userid);
}
$manager = manager::create_from_instance($this->instance);
$scores = $manager->get_users_scaled_score($userid);
if (!$scores) {
return $grades;
}
// Maxgrade depends on the type of grade used:
// - grade > 0: regular quantitative grading.
// - grade = 0: no grading.
// - grade < 0: scale used.
$maxgrade = floatval($this->instance->grade);
// Convert scaled scores into gradebok compatible objects.
foreach ($scores as $userid => $score) {
$grades[$userid] = [
'userid' => $userid,
'rawgrade' => $maxgrade * $score->scaled,
'dategraded' => $score->timemodified,
'datesubmitted' => $score->timemodified,
];
}
return $grades;
}
/**
* Get an deletion list of user grades and feedback for the gradebook.
*
* This method is used to delete all autmatic gradings when grading method is set to manual.
*
* @param int $userid int or 0 for all users
* @return array of grade data formated for the gradebook api
* The data required by the gradebook api is userid,
* rawgrade (null to delete),
* dategraded,
* datesubmitted
*/
private function get_user_grades_for_deletion (int $userid = 0): array {
$grades = [];
if ($userid) {
$grades[$userid] = [
'userid' => $userid,
'rawgrade' => null,
'dategraded' => time(),
'datesubmitted' => time(),
];
} else {
$manager = manager::create_from_instance($this->instance);
$users = get_enrolled_users($manager->get_context(), 'mod/h5pactivity:submit');
foreach ($users as $user) {
$grades[$user->id] = [
'userid' => $user->id,
'rawgrade' => null,
'dategraded' => time(),
'datesubmitted' => time(),
];
}
}
return $grades;
}
}
+237
View File
@@ -0,0 +1,237 @@
<?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/>.
/**
* H5P activity manager class
*
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\local;
use context_module;
use cm_info;
use moodle_recordset;
use stdClass;
/**
* Class manager for H5P activity
*
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manager {
/** No automathic grading using attempt results. */
const GRADEMANUAL = 0;
/** Use highest attempt results for grading. */
const GRADEHIGHESTATTEMPT = 1;
/** Use average attempt results for grading. */
const GRADEAVERAGEATTEMPT = 2;
/** Use last attempt results for grading. */
const GRADELASTATTEMPT = 3;
/** Use first attempt results for grading. */
const GRADEFIRSTATTEMPT = 4;
/** @var stdClass course_module record. */
private $instance;
/** @var context_module the current context. */
private $context;
/** @var cm_info course_modules record. */
private $coursemodule;
/**
* Class contructor.
*
* @param cm_info $coursemodule course module info object
* @param stdClass $instance H5Pactivity instance object.
*/
public function __construct(cm_info $coursemodule, stdClass $instance) {
$this->coursemodule = $coursemodule;
$this->instance = $instance;
$this->context = context_module::instance($coursemodule->id);
$this->instance->cmidnumber = $coursemodule->idnumber;
}
/**
* Create a manager instance from an instance record.
*
* @param stdClass $instance a h5pactivity record
* @return manager
*/
public static function create_from_instance(stdClass $instance): self {
$coursemodule = get_coursemodule_from_instance('h5pactivity', $instance->id);
// Ensure that $this->coursemodule is a cm_info object.
$coursemodule = cm_info::create($coursemodule);
return new self($coursemodule, $instance);
}
/**
* Create a manager instance from an course_modules record.
*
* @param stdClass|cm_info $coursemodule a h5pactivity record
* @return manager
*/
public static function create_from_coursemodule($coursemodule): self {
global $DB;
// Ensure that $this->coursemodule is a cm_info object.
$coursemodule = cm_info::create($coursemodule);
$instance = $DB->get_record('h5pactivity', ['id' => $coursemodule->instance], '*', MUST_EXIST);
return new self($coursemodule, $instance);
}
/**
* Return the available grading methods.
* @return string[] an array "option value" => "option description"
*/
public static function get_grading_methods(): array {
return [
self::GRADEHIGHESTATTEMPT => get_string('grade_highest_attempt', 'mod_h5pactivity'),
self::GRADEAVERAGEATTEMPT => get_string('grade_average_attempt', 'mod_h5pactivity'),
self::GRADELASTATTEMPT => get_string('grade_last_attempt', 'mod_h5pactivity'),
self::GRADEFIRSTATTEMPT => get_string('grade_first_attempt', 'mod_h5pactivity'),
self::GRADEMANUAL => get_string('grade_manual', 'mod_h5pactivity'),
];
}
/**
* Check if tracking is enabled in a particular h5pactivity for a specific user.
*
* @param stdClass|null $user user record (default $USER)
* @return bool if tracking is enabled in this activity
*/
public function is_tracking_enabled(stdClass $user = null): bool {
global $USER;
if (!$this->instance->enabletracking) {
return false;
}
if (empty($user)) {
$user = $USER;
}
return has_capability('mod/h5pactivity:submit', $this->context, $user, false);
}
/**
* Return a relation of userid and the valid attempt's scaled score.
*
* The returned elements contain a record
* of userid, scaled value, attemptid and timemodified. In case the grading method is "GRADEAVERAGEATTEMPT"
* the attemptid will be zero. In case that tracking is disabled or grading method is "GRADEMANUAL"
* the method will return null.
*
* @param int $userid a specific userid or 0 for all user attempts.
* @return array|null of userid, scaled value and, if exists, the attempt id
*/
public function get_users_scaled_score(int $userid = 0): ?array {
global $DB;
$scaled = [];
if (!$this->instance->enabletracking) {
return null;
}
if ($this->instance->grademethod == self::GRADEMANUAL) {
return null;
}
$sql = '';
// General filter.
$where = 'a.h5pactivityid = :h5pactivityid';
$params['h5pactivityid'] = $this->instance->id;
if ($userid) {
$where .= ' AND a.userid = :userid';
$params['userid'] = $userid;
}
// Average grading needs aggregation query.
if ($this->instance->grademethod == self::GRADEAVERAGEATTEMPT) {
$sql = "SELECT a.userid, AVG(a.scaled) AS scaled, 0 AS attemptid, MAX(timemodified) AS timemodified
FROM {h5pactivity_attempts} a
WHERE $where AND a.completion = 1
GROUP BY a.userid";
}
if (empty($sql)) {
// Decide which attempt is used for the calculation.
$condition = [
self::GRADEHIGHESTATTEMPT => "a.scaled < b.scaled",
self::GRADELASTATTEMPT => "a.attempt < b.attempt",
self::GRADEFIRSTATTEMPT => "a.attempt > b.attempt",
];
$join = $condition[$this->instance->grademethod] ?? $condition[self::GRADEHIGHESTATTEMPT];
$sql = "SELECT a.userid, a.scaled, MAX(a.id) AS attemptid, MAX(a.timemodified) AS timemodified
FROM {h5pactivity_attempts} a
LEFT JOIN {h5pactivity_attempts} b ON a.h5pactivityid = b.h5pactivityid
AND a.userid = b.userid AND b.completion = 1
AND $join
WHERE $where AND b.id IS NULL AND a.completion = 1
GROUP BY a.userid, a.scaled";
}
return $DB->get_records_sql($sql, $params);
}
/**
* Return the current context.
*
* @return context_module
*/
public function get_context(): context_module {
return $this->context;
}
/**
* Return the current context.
*
* @return stdClass the instance record
*/
public function get_instance(): stdClass {
return $this->instance;
}
/**
* Return the current cm_info.
*
* @return cm_info the course module
*/
public function get_coursemodule(): cm_info {
return $this->coursemodule;
}
/**
* Return the specific grader object for this activity.
*
* @return grader
*/
public function get_grader(): grader {
$idnumber = $this->coursemodule->idnumber ?? '';
return new grader($this->instance, $idnumber);
}
}
+14 -4
View File
@@ -26,6 +26,7 @@
namespace mod_h5pactivity\xapi;
use mod_h5pactivity\local\attempt;
use mod_h5pactivity\local\manager;
use mod_h5pactivity\event\statement_received;
use core_xapi\local\statement;
use core_xapi\handler as handler_base;
@@ -34,6 +35,8 @@ use context_module;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/mod/h5pactivity/lib.php');
/**
* Class xapi_handler for H5P statements.
*
@@ -94,15 +97,18 @@ class handler extends handler_base {
if (!has_capability('mod/h5pactivity:view', $context, $user)) {
return null;
}
if (!has_capability('mod/h5pactivity:submit', $context, $user, false)) {
return null;
}
$cm = get_coursemodule_from_id('h5pactivity', $context->instanceid, 0, false);
if (!$cm) {
return null;
}
$manager = manager::create_from_coursemodule($cm);
if (!$manager->is_tracking_enabled($user)) {
return null;
}
// For now, attempts are only processed on a single batch starting with the final "completed"
// and "answered" statements (this could change in the future). This initial statement have no
// subcontent defined as they are the main finishing statement. For this reason, this statement
@@ -122,7 +128,11 @@ class handler extends handler_base {
return null;
}
// TODO: update grading if necessary.
// Update activity if necessary.
if ($attempt->get_scoreupdated()) {
$grader = $manager->get_grader();
$grader->update_grades($user->id);
}
// Convert into a Moodle event.
$minstatement = $statement->minify();
+4 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="mod/h5pactivity/db" VERSION="20200414" COMMENT="XMLDB file for Moodle mod_h5pactivity"
<XMLDB PATH="mod/h5pactivity/db" VERSION="20200422" COMMENT="XMLDB file for Moodle mod_h5pactivity"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
>
@@ -15,6 +15,8 @@
<FIELD NAME="introformat" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The format of the intro field."/>
<FIELD NAME="grade" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="displayoptions" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="H5P Button display options"/>
<FIELD NAME="enabletracking" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Enable xAPI tracking"/>
<FIELD NAME="grademethod" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Which H5P attempt is used for grading"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
@@ -31,6 +33,7 @@
<FIELD NAME="attempt" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Attempt number"/>
<FIELD NAME="rawscore" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="maxscore" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="scaled" TYPE="number" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="5" COMMENT="Number 0..1 that reflects the performance of the learner"/>
<FIELD NAME="duration" TYPE="int" LENGTH="10" NOTNULL="false" DEFAULT="0" SEQUENCE="false" COMMENT="Number of second inverted in that attempt (provided by the statement)"/>
<FIELD NAME="completion" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Store the xAPI tracking completion result."/>
<FIELD NAME="success" TYPE="int" LENGTH="1" NOTNULL="false" SEQUENCE="false" COMMENT="Store the xAPI tracking success result."/>
+43
View File
@@ -190,5 +190,48 @@ function xmldb_h5pactivity_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2020041400, 'h5pactivity');
}
if ($oldversion < 2020041401) {
// Define field enabletracking to be added to h5pactivity.
$table = new xmldb_table('h5pactivity');
$field = new xmldb_field('enabletracking', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '1', 'displayoptions');
// Conditionally launch add field enabletracking.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field grademethod to be added to h5pactivity.
$field = new xmldb_field('grademethod', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, '1', 'enabletracking');
// Conditionally launch add field grademethod.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field scaled to be added to h5pactivity_attempts.
$table = new xmldb_table('h5pactivity_attempts');
$field = new xmldb_field('scaled', XMLDB_TYPE_NUMBER, '10, 5', null, XMLDB_NOTNULL, null, '0', 'maxscore');
// Conditionally launch add field scaled.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Calculate all scaled values from current attempts.
$rs = $DB->get_recordset('h5pactivity_attempts');
foreach ($rs as $record) {
if (empty($record->maxscore)) {
continue;
}
$record->scaled = $record->rawscore / $record->maxscore;
$DB->update_record('h5pactivity_attempts', $record);
}
$rs->close();
// H5pactivity savepoint reached.
upgrade_mod_savepoint(true, 2020041401, 'h5pactivity');
}
return true;
}
+17
View File
@@ -27,16 +27,32 @@ defined('MOODLE_INTERNAL') || die();
$string['areapackage'] = 'Package file';
$string['attempt'] = 'Attempt';
$string['attempts'] = 'Attempts';
$string['deleteallattempts'] = 'Delete all H5P attempts';
$string['displayexport'] = 'Allow download';
$string['displayembed'] = 'Embed button';
$string['displaycopyright'] = 'Copyright button';
$string['enabletracking'] = 'Enable attempt tracking';
$string['grade_grademethod'] = 'Grading method';
$string['grade_grademethod_help'] = 'When using point grading, the following methods are available for calculating the final grade:
* Highest grade of all attempts
* Average (mean) grade of all attempts
* First attempt (all other attempts are ignored)
* Last attempt (all other attempts are ignored)
* Don\'t use attempts for grading (disable grading calculation)';
$string['grade_manual'] = 'Don\'t calculate a grade';
$string['grade_highest_attempt'] = 'Highest grade';
$string['grade_average_attempt'] = 'Average grade';
$string['grade_last_attempt'] = 'Last attempt';
$string['grade_first_attempt'] = 'First attempt';
$string['h5pactivity:addinstance'] = 'Add a new H5P';
$string['h5pactivity:submit'] = 'Submit H5P attempts';
$string['h5pactivity:view'] = 'View H5P';
$string['h5pactivityfieldset'] = 'H5P settings';
$string['h5pactivityname'] = 'H5P';
$string['h5pactivitysettings'] = 'Settings';
$string['h5pattempts'] = 'Attempt options';
$string['h5pdisplay'] = 'H5P options';
$string['modulename'] = 'H5P';
$string['modulename_help'] = 'H5P is an abbreviation for HTML5 Package - interactive content such as presentations, videos and other multimedia, questions, quizzes, games and more. The H5P activity enables H5P to be uploaded and added to a course.
@@ -59,4 +75,5 @@ $string['privacy:metadata:userid'] = 'The ID of the user who accessed the H5P ac
$string['privacy:metadata:xapi_track'] = 'Attempt tracking information';
$string['privacy:metadata:xapi_track_results'] = 'Attempt results tracking information';
$string['statement_received'] = 'xAPI statement received';
$string['tracking_messages'] = 'Some H5P provide attempt tracking data for advanced reporting such as number of attempts, responses and grades. Note: Some H5P don\'t provide attempt tracking data. In such cases, the following settings will have no effect.';
$string['view'] = 'View';
+41 -42
View File
@@ -22,6 +22,9 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_h5pactivity\local\manager;
use mod_h5pactivity\local\grader;
defined('MOODLE_INTERNAL') || die();
/**
@@ -113,10 +116,15 @@ function h5pactivity_update_instance(stdClass $data, mod_h5pactivity_mod_form $m
h5pactivity_set_mainfile($data);
// Extra fields required in grade related functions.
// Update gradings if grading method or tracking are modified.
$data->cmid = $data->coursemodule;
h5pactivity_grade_item_update($data);
h5pactivity_update_grades($data);
$moduleinstance = $DB->get_record('h5pactivity', ['id' => $data->id]);
if (($moduleinstance->grademethod != $data->grademethod)
|| $data->enabletracking != $moduleinstance->enabletracking) {
h5pactivity_update_grades($data);
} else {
h5pactivity_grade_item_update($data);
}
return $DB->update_record('h5pactivity', $data);
}
@@ -169,33 +177,10 @@ function h5pactivity_scale_used_anywhere(int $scaleid): bool {
* @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
* @return int int 0 if ok, error code otherwise
*/
function h5pactivity_grade_item_update(stdClass $moduleinstance, $grades=null): int {
global $CFG;
require_once($CFG->libdir.'/gradelib.php');
$item = [];
$item['itemname'] = clean_param($moduleinstance->name, PARAM_NOTAGS);
$item['gradetype'] = GRADE_TYPE_VALUE;
if (isset($moduleinstance->cmidnumber)) {
$item['idnumber'] = $moduleinstance->cmidnumber;
}
if ($moduleinstance->grade > 0) {
$item['gradetype'] = GRADE_TYPE_VALUE;
$item['grademax'] = $moduleinstance->grade;
$item['grademin'] = 0;
} else if ($moduleinstance->grade < 0) {
$item['gradetype'] = GRADE_TYPE_SCALE;
$item['scaleid'] = -$moduleinstance->grade;
} else {
$item['gradetype'] = GRADE_TYPE_NONE;
}
if ($grades === 'reset') {
$params['reset'] = true;
$grades = null;
}
return grade_update('mod/h5pactivity', $moduleinstance->course, 'mod',
'h5pactivity', $moduleinstance->id, 0, null, $item);
function h5pactivity_grade_item_update(stdClass $moduleinstance, $grades = null): int {
$idnumber = $moduleinstance->idnumber ?? '';
$grader = new grader($moduleinstance, $idnumber);
return $grader->grade_item_update($grades);
}
/**
@@ -205,11 +190,9 @@ function h5pactivity_grade_item_update(stdClass $moduleinstance, $grades=null):
* @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED
*/
function h5pactivity_grade_item_delete(stdClass $moduleinstance): ?int {
global $CFG;
require_once($CFG->libdir.'/gradelib.php');
return grade_update('mod/h5pactivity', $moduleinstance->course, 'mod', 'h5pactivity',
$moduleinstance->id, 0, null, ['deleted' => 1]);
$idnumber = $moduleinstance->idnumber ?? '';
$grader = new grader($moduleinstance, $idnumber);
return $grader->grade_item_delete();
}
/**
@@ -221,13 +204,29 @@ function h5pactivity_grade_item_delete(stdClass $moduleinstance): ?int {
* @param int $userid Update grade of specific user only, 0 means all participants.
*/
function h5pactivity_update_grades(stdClass $moduleinstance, int $userid = 0): void {
global $CFG;
require_once($CFG->libdir.'/gradelib.php');
$idnumber = $moduleinstance->idnumber ?? '';
$grader = new grader($moduleinstance, $idnumber);
$grader->update_grades($userid);
}
// Populate array of grade objects indexed by userid.
$grades = [];
grade_update('mod/h5pactivity', $moduleinstance->course, 'mod',
'h5pactivity', $moduleinstance->id, 0, $grades);
/**
* Rescale all grades for this activity and push the new grades to the gradebook.
*
* @param stdClass $course Course db record
* @param stdClass $cm Course module db record
* @param float $oldmin
* @param float $oldmax
* @param float $newmin
* @param float $newmax
* @return bool true if reescale is successful
*/
function h5pactivity_rescale_activity_grades(stdClass $course, stdClass $cm, float $oldmin,
float $oldmax, float $newmin, float $newmax): bool {
$manager = manager::create_from_coursemodule($cm);
$grader = $manager->get_grader();
$grader->update_grades();
return true;
}
/**
@@ -306,7 +305,7 @@ function h5pactivity_reset_gradebook(int $courseid, string $type=''): void {
if ($activities = $DB->get_records_sql($sql, [$courseid])) {
foreach ($activities as $activity) {
h5pactivity_grade_item_update($activity, true);
h5pactivity_grade_item_update($activity, 'reset');
}
}
}
+22
View File
@@ -22,6 +22,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_h5pactivity\local\manager;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/course/moodleform_mod.php');
@@ -85,6 +87,22 @@ class mod_h5pactivity_mod_form extends moodleform_mod {
// Add standard grading elements.
$this->standard_grading_coursemodule_elements();
// Attempt options.
$mform->addElement('header', 'h5pattempts', get_string('h5pattempts', 'mod_h5pactivity'));
$mform->addElement('static', 'trackingwarning', '', get_string('tracking_messages', 'mod_h5pactivity'));
$options = [1 => get_string('yes'), 0 => get_string('no')];
$mform->addElement('select', 'enabletracking', get_string('enabletracking', 'mod_h5pactivity'), $options);
$mform->setDefault('enabletracking', 1);
$options = manager::get_grading_methods();
$mform->addElement('select', 'grademethod', get_string('grade_grademethod', 'mod_h5pactivity'), $options);
$mform->setType('grademethod', PARAM_INT);
$mform->hideIf('grademethod', 'enabletracking', 'neq', 1);
$mform->disabledIf('grademethod', 'grade[modgrade_type]', 'neq', 'point');
$mform->addHelpButton('grademethod', 'grade_grademethod', 'mod_h5pactivity');
// Add standard elements.
$this->standard_coursemodule_elements();
@@ -178,5 +196,9 @@ class mod_h5pactivity_mod_form extends moodleform_mod {
$config = \core_h5p\helper::decode_display_options($core);
}
$data->displayoptions = \core_h5p\helper::get_display_options($core, $config);
if (!isset($data->enabletracking)) {
$data->enabletracking = 0;
}
}
}
@@ -0,0 +1,50 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript
Feature: Set up attempt grading options into H5P activity
In order to use automatic grading in H5P activity
As a teacher
I need to be able to configure the attempt settings
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "H5P" to section "1"
Scenario: Default values should have tracking and grading
When the field "Type" matches value "Point"
Then the "Grading method" "select" should be enabled
Scenario: Scale grading should not have a grading method.
When I set the following fields to these values:
| Name | Awesome H5P package |
| Type | Scale |
Then the "Grading method" "select" should be disabled
Scenario: None grading should not have a grading method.
When I set the following fields to these values:
| Name | Awesome H5P package |
| Type | None |
Then the "Grading method" "select" should be disabled
Scenario: Point grading should have a grading method.
When I set the following fields to these values:
| Name | Awesome H5P package |
| Type | Point |
Then the "Grading method" "select" should be enabled
Scenario: Disable tracking should make grading method disappear.
When I set the following fields to these values:
| Name | Awesome H5P package |
| Enable attempt tracking | No |
Then "Grading method" "field" should not be visible
@@ -0,0 +1,174 @@
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe
Feature: Change grading options in an H5P activity
In order to let students do a H5P attempt
As a teacher
I need to define what students attempts are used for grading
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And the following "permission overrides" exist:
| capability | permission | role | contextlevel | reference |
| moodle/h5p:updatelibraries | Allow | editingteacher | System | |
And I log in as "teacher1"
And I am on "Course 1" course homepage with editing mode on
And I add a "H5P" to section "1"
And I set the following fields to these values:
| Name | Awesome H5P package |
| Description | Description |
And I upload "h5p/tests/fixtures/multiple-choice-2-6.h5p" file to "Package file" filemanager
And I click on "Save and display" "button"
And I log out
And I log in as "student1"
And I am on "Course 1" course homepage
And I follow "Awesome H5P package"
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
And I click on "Retry" "button" in the ".h5p-question-buttons" "css_element"
And I click on "Correct one" "text" in the ".h5p-question-content" "css_element"
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
And I switch to the main frame
# H5P does not allow to Retry if the user checks the correct answer, we need to refresh the page.
And I reload the page
And I switch to "h5p-player" class iframe
And I switch to "h5p-iframe" class iframe
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
And I switch to the main frame
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "Awesome H5P package"
@javascript
Scenario: Default grading is max attempt grade
When I navigate to "View > User report" in the course gradebook
And I set the field "Select all or one user" to "Student 1"
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | 100.00 | 100.00 % |
@javascript
Scenario: Change setting to first attempt
When I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Grading method | First attempt |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I set the field "Select all or one user" to "Student 1"
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | 0.00 | 0.00 % |
@javascript
Scenario: Change setting to first attempt
When I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Grading method | Last attempt |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I set the field "Select all or one user" to "Student 1"
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | 0.00 | 0.00 % |
@javascript
Scenario: Change setting to average attempt
When I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Grading method | Average grade |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I set the field "Select all or one user" to "Student 1"
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | 33.33 | 33.33 % |
@javascript
Scenario: Change setting to manual grading
When I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Grading method | Don't calculate a grade |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I set the field "Select all or one user" to "Student 1"
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | - | - |
@javascript
Scenario: Disable tracking
When I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Enable attempt tracking | No |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I set the field "Select all or one user" to "Student 1"
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Percentage |
| Awesome H5P package | - | - |
@javascript
Scenario: Reescale existing grades changing the maximum grade
# First we set to average and recalculate grades.
When I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Grading method | Average grade |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I set the field "Select all or one user" to "Student 1"
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Range | Percentage |
| Awesome H5P package | 33.33 | 0100 | 33.33 % |
# Now we modify the maximum grade with rescaling.
When I am on "Course 1" course homepage
And I follow "Awesome H5P package"
And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Rescale existing grades | Yes |
| Maximum grade | 50 |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I set the field "Select all or one user" to "Student 1"
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Range | Percentage |
| Awesome H5P package | 16.67 | 050 | 33.33 % |
@javascript
Scenario: Change maximum grade without rescaling grade
# First we set to average and recalculate grades.
When I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Grading method | Average grade |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I set the field "Select all or one user" to "Student 1"
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Range | Percentage |
| Awesome H5P package | 33.33 | 0100 | 33.33 % |
# Now we modify the maximum grade with rescaling.
When I am on "Course 1" course homepage
And I follow "Awesome H5P package"
And I navigate to "Edit settings" in current page administration
And I set the following fields to these values:
| Rescale existing grades | No |
| Maximum grade | 50 |
And I click on "Save and return to course" "button"
And I navigate to "View > User report" in the course gradebook
And I set the field "Select all or one user" to "Student 1"
Then the following should exist in the "user-grade" table:
| Grade item | Grade | Range | Percentage |
| Awesome H5P package | 33.33 | 050 | 66.67 % |
+8
View File
@@ -22,6 +22,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_h5pactivity\local\manager;
defined('MOODLE_INTERNAL') || die();
@@ -63,6 +65,12 @@ class mod_h5pactivity_generator extends testing_module_generator {
$config = \core_h5p\helper::decode_display_options($core);
$record->displayoptions = \core_h5p\helper::get_display_options($core, $config);
}
if (!isset($record->enabletracking)) {
$record->enabletracking = 1;
}
if (!isset($record->grademethod)) {
$record->grademethod = manager::GRADEHIGHESTATTEMPT;
}
// The 'packagefile' value corresponds to the draft file area ID. If not specified, create from packagefilepath.
if (empty($record->packagefile)) {
+8 -1
View File
@@ -23,6 +23,8 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_h5pactivity\local\manager;
defined('MOODLE_INTERNAL') || die();
/**
@@ -54,10 +56,15 @@ class mod_h5pactivity_generator_testcase extends advanced_testcase {
$this->assertTrue(array_key_exists($activity->id, $records));
// Create a second one with different name and dusplay options.
$params = ['course' => $course->id, 'name' => 'Another h5pactivity', 'displayoptions' => 6];
$params = [
'course' => $course->id, 'name' => 'Another h5pactivity', 'displayoptions' => 6,
'enabletracking' => 0, 'grademethod' => manager::GRADELASTATTEMPT,
];
$activity = $this->getDataGenerator()->create_module('h5pactivity', $params);
$records = $DB->get_records('h5pactivity', ['course' => $course->id], 'id');
$this->assertEquals(6, $activity->displayoptions);
$this->assertEquals(0, $activity->enabletracking);
$this->assertEquals(manager::GRADELASTATTEMPT, $activity->grademethod);
$this->assertEquals(2, count($records));
$this->assertEquals('Another h5pactivity', $records[$activity->id]->name);
+111 -1
View File
@@ -15,7 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* mod_h5pactivity generator tests
* mod_h5pactivity attempt tests
*
* @package mod_h5pactivity
* @category test
@@ -138,6 +138,7 @@ class attempt_testcase extends \advanced_testcase {
$this->assertEquals(0, $attempt->get_duration());
$this->assertNull($attempt->get_completion());
$this->assertNull($attempt->get_success());
$this->assertFalse($attempt->get_scoreupdated());
$statement = $this->generate_statement($hasdefinition, $hasresult);
$result = $attempt->save_statement($statement, $subcontent);
@@ -148,6 +149,11 @@ class attempt_testcase extends \advanced_testcase {
$this->assertEquals($results[4], $attempt->get_duration());
$this->assertEquals($results[5], $attempt->get_completion());
$this->assertEquals($results[6], $attempt->get_success());
if ($results[5]) {
$this->assertTrue($attempt->get_scoreupdated());
} else {
$this->assertFalse($attempt->get_scoreupdated());
}
}
/**
@@ -298,6 +304,110 @@ class attempt_testcase extends \advanced_testcase {
];
}
/**
* Test set_score method.
*
*/
public function test_set_score(): void {
global $DB;
list($cm, $student, $course) = $this->generate_testing_scenario();
// Generate one attempt.
$attempt = $this->generate_full_attempt($student, $cm);
$dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
$this->assertEquals($dbattempt->rawscore, $attempt->get_rawscore());
$this->assertEquals(2, $dbattempt->rawscore);
$this->assertEquals($dbattempt->maxscore, $attempt->get_maxscore());
$this->assertEquals(2, $dbattempt->maxscore);
$this->assertEquals(1, $dbattempt->scaled);
// Set attempt score.
$attempt->set_score(5, 10);
$this->assertEquals(5, $attempt->get_rawscore());
$this->assertEquals(10, $attempt->get_maxscore());
$this->assertTrue($attempt->get_scoreupdated());
// Save new score into DB.
$attempt->save();
$dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
$this->assertEquals($dbattempt->rawscore, $attempt->get_rawscore());
$this->assertEquals(5, $dbattempt->rawscore);
$this->assertEquals($dbattempt->maxscore, $attempt->get_maxscore());
$this->assertEquals(10, $dbattempt->maxscore);
$this->assertEquals(0.5, $dbattempt->scaled);
}
/**
* Test set_duration method.
*
* @dataProvider basic_setters_data
* @param string $attribute the stribute to test
* @param int $oldvalue attribute old value
* @param int $newvalue attribute new expected value
*/
public function test_basic_setters(string $attribute, int $oldvalue, int $newvalue): void {
global $DB;
list($cm, $student, $course) = $this->generate_testing_scenario();
// Generate one attempt.
$attempt = $this->generate_full_attempt($student, $cm);
$setmethod = 'set_'.$attribute;
$getmethod = 'get_'.$attribute;
$dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
$this->assertEquals($dbattempt->$attribute, $attempt->$getmethod());
$this->assertEquals($oldvalue, $dbattempt->$attribute);
// Set attempt attribute.
$attempt->$setmethod($newvalue);
$this->assertEquals($newvalue, $attempt->$getmethod());
// Save new score into DB.
$attempt->save();
$dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
$this->assertEquals($dbattempt->$attribute, $attempt->$getmethod());
$this->assertEquals($newvalue, $dbattempt->$attribute);
// Set null $attribute.
$attempt->$setmethod(null);
$this->assertNull($attempt->$getmethod());
// Save new score into DB.
$attempt->save();
$dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]);
$this->assertEquals($dbattempt->$attribute, $attempt->$getmethod());
$this->assertNull($dbattempt->$attribute);
}
/**
* Data provider for testing basic setters.
*
* @return array
*/
public function basic_setters_data(): array {
return [
'Set attempt duration' => [
'duration', 25, 35
],
'Set attempt completion' => [
'completion', 1, 0
],
'Set attempt success' => [
'success', 1, 0
],
];
}
/**
* Generate a fake attempt with two results.
*
+360
View File
@@ -0,0 +1,360 @@
<?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/>.
/**
* mod_h5pactivity grader tests
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\local;
use grade_item;
use stdClass;
/**
* Grader tests class for mod_h5pactivity.
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class grader_testcase extends \advanced_testcase {
/**
* Setup to ensure that fixtures are loaded.
*/
public static function setupBeforeClass(): void {
global $CFG;
require_once($CFG->libdir.'/gradelib.php');
}
/**
* Test for grade item delete.
*/
public function test_grade_item_delete() {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
$grader = new grader($activity);
// Force a user grade.
$this->generate_fake_attempt($activity, $user, 5, 10);
$grader->update_grades($user->id);
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
$this->assertNotEquals(0, count($gradeinfo->items));
$this->assertArrayHasKey($user->id, $gradeinfo->items[0]->grades);
$grader->grade_item_delete();
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
$this->assertEquals(0, count($gradeinfo->items));
}
/**
* Test for grade item update.
*
* @dataProvider grade_item_update_data
* @param int $newgrade new activity grade
* @param bool $reset if has to reset grades
* @param string $idnumber the new idnumber
*/
public function test_grade_item_update(int $newgrade, bool $reset, string $idnumber) {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$user = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Force a user initial grade.
$grader = new grader($activity);
$this->generate_fake_attempt($activity, $user, 5, 10);
$grader->update_grades($user->id);
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
$this->assertNotEquals(0, count($gradeinfo->items));
$item = array_shift($gradeinfo->items);
$this->assertArrayHasKey($user->id, $item->grades);
$this->assertEquals(50, round($item->grades[$user->id]->grade));
// Module grade value determine the way gradebook acts. That means that the expected
// result depends on this value.
// - Grade > 0: regular max grade value.
// - Grade = 0: no grading is used (but grademax remains the same).
// - Grade < 0: a scaleid is used (value = -scaleid).
if ($newgrade > 0) {
$grademax = $newgrade;
$scaleid = null;
$usergrade = ($newgrade > 50) ? 50 : $newgrade;
} else if ($newgrade == 0) {
$grademax = 100;
$scaleid = null;
$usergrade = null; // No user grades expected.
} else if ($newgrade < 0) {
$scale = $this->getDataGenerator()->create_scale(array("scale" => "value1, value2, value3"));
$newgrade = -1 * $scale->id;
$grademax = 3;
$scaleid = $scale->id;
$usergrade = 3; // 50 value will ve converted to "value 3" on scale.
}
// Update grade item.
$activity->grade = $newgrade;
// In case a reset is need, usergrade will be empty.
if ($reset) {
$param = 'reset';
$usergrade = null;
} else {
// Individual user gradings will be tested as a subcall of update_grades.
$param = null;
}
$grader = new grader($activity, $idnumber);
$grader->grade_item_update($param);
// Check new grade item and grades.
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id);
$item = array_shift($gradeinfo->items);
$this->assertEquals($scaleid, $item->scaleid);
$this->assertEquals($grademax, $item->grademax);
$this->assertArrayHasKey($user->id, $item->grades);
if ($usergrade) {
$this->assertEquals($usergrade, round($item->grades[$user->id]->grade));
} else {
$this->assertEmpty($item->grades[$user->id]->grade);
}
if (!empty($idnumber)) {
$gradeitem = grade_item::fetch(['idnumber' => $idnumber, 'courseid' => $course->id]);
$this->assertInstanceOf('grade_item', $gradeitem);
}
}
/**
* Data provider for test_grade_item_update.
*
* @return array
*/
public function grade_item_update_data(): array {
return [
'Change idnumber' => [
100, false, 'newidnumber'
],
'Increase max grade to 110' => [
110, false, ''
],
'Decrease max grade to 80' => [
40, false, ''
],
'Decrease max grade to 40 (less than actual grades)' => [
40, false, ''
],
'Reset grades' => [
100, true, ''
],
'Disable grades' => [
0, false, ''
],
'Use scales' => [
-1, false, ''
],
'Use scales with reset' => [
-1, true, ''
],
];
}
/**
* Test for grade update.
*
* @dataProvider update_grades_data
* @param int $newgrade the new activity grade
* @param bool $all if has to be applied to all students or just to one
* @param int $completion 1 all student have the activity completed, 0 one have incompleted
* @param array $results expected results (user1 grade, user2 grade)
*/
public function test_update_grades(int $newgrade, bool $all, int $completion, array $results) {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$user1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
$user2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
// Force a user initial grade.
$grader = new grader($activity);
$this->generate_fake_attempt($activity, $user1, 5, 10);
$this->generate_fake_attempt($activity, $user2, 3, 12, $completion);
$grader->update_grades();
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, [$user1->id, $user2->id]);
$this->assertNotEquals(0, count($gradeinfo->items));
$item = array_shift($gradeinfo->items);
$this->assertArrayHasKey($user1->id, $item->grades);
$this->assertArrayHasKey($user2->id, $item->grades);
$this->assertEquals(50, $item->grades[$user1->id]->grade);
// Uncompleted attempts does not generate grades.
if ($completion) {
$this->assertEquals(25, $item->grades[$user2->id]->grade);
} else {
$this->assertNull($item->grades[$user2->id]->grade);
}
// Module grade value determine the way gradebook acts. That means that the expected
// result depends on this value.
// - Grade > 0: regular max grade value.
// - Grade <= 0: no grade calculation is used (scale and no grading).
if ($newgrade < 0) {
$scale = $this->getDataGenerator()->create_scale(array("scale" => "value1, value2, value3"));
$activity->grade = -1 * $scale->id;
} else {
$activity->grade = $newgrade;
}
$userid = ($all) ? 0 : $user1->id;
$grader = new grader($activity);
$grader->update_grades($userid);
// Check new grade item and grades.
$gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, [$user1->id, $user2->id]);
$item = array_shift($gradeinfo->items);
$this->assertArrayHasKey($user1->id, $item->grades);
$this->assertArrayHasKey($user2->id, $item->grades);
$this->assertEquals($results[0], $item->grades[$user1->id]->grade);
$this->assertEquals($results[1], $item->grades[$user2->id]->grade);
}
/**
* Data provider for test_grade_item_update.
*
* @return array
*/
public function update_grades_data(): array {
return [
// Quantitative grade, all attempts completed.
'Same grademax, all users, all completed' => [
100, true, 1, [50, 25]
],
'Same grademax, one user, all completed' => [
100, false, 1, [50, 25]
],
'Increade max, all users, all completed' => [
200, true, 1, [100, 50]
],
'Increade max, one user, all completed' => [
200, false, 1, [100, 25]
],
'Decrease max, all users, all completed' => [
50, true, 1, [25, 12.5]
],
'Decrease max, one user, all completed' => [
50, false, 1, [25, 25]
],
// Quantitative grade, some attempts not completed.
'Same grademax, all users, not completed' => [
100, true, 0, [50, null]
],
'Same grademax, one user, not completed' => [
100, false, 0, [50, null]
],
'Increade max, all users, not completed' => [
200, true, 0, [100, null]
],
'Increade max, one user, not completed' => [
200, false, 0, [100, null]
],
'Decrease max, all users, not completed' => [
50, true, 0, [25, null]
],
'Decrease max, one user, not completed' => [
50, false, 0, [25, null]
],
// No grade (no grade will be used).
'No grade, all users, all completed' => [
0, true, 1, [null, null]
],
'No grade, one user, all completed' => [
0, false, 1, [null, null]
],
'No grade, all users, not completed' => [
0, true, 0, [null, null]
],
'No grade, one user, not completed' => [
0, false, 0, [null, null]
],
// Scale (grate item will updated but without regrading).
'Scale, all users, all completed' => [
-1, true, 1, [3, 3]
],
'Scale, one user, all completed' => [
-1, false, 1, [3, 3]
],
'Scale, all users, not completed' => [
-1, true, 0, [3, null]
],
'Scale, one user, not completed' => [
-1, false, 0, [3, null]
],
];
}
/**
* Create a fake attempt for a specific user.
*
* @param stdClass $activity activity instance record.
* @param stdClass $user user record
* @param int $rawscore score obtained
* @param int $maxscore attempt max score
* @param int $completion 1 for activity completed, 0 for not completed yet
* @return stdClass the attempt record
*/
private function generate_fake_attempt(stdClass $activity, stdClass $user,
int $rawscore, int $maxscore, int $completion = 1): stdClass {
global $DB;
$attempt = (object)[
'h5pactivityid' => $activity->id,
'userid' => $user->id,
'timecreated' => 10,
'timemodified' => 20,
'attempt' => 1,
'rawscore' => $rawscore,
'maxscore' => $maxscore,
'duration' => 2,
'completion' => $completion,
'success' => 0,
];
$attempt->scaled = $attempt->rawscore / $attempt->maxscore;
$attempt->id = $DB->insert_record('h5pactivity_attempts', $attempt);
return $attempt;
}
}
@@ -0,0 +1,356 @@
<?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/>.
/**
* mod_h5pactivity manager tests
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\local;
use context_module;
use stdClass;
/**
* Manager tests class for mod_h5pactivity.
*
* @package mod_h5pactivity
* @category test
* @copyright 2020 Ferran Recio <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manager_testcase extends \advanced_testcase {
/**
* Test for static create methods.
*/
public function test_create() {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST);
$context = context_module::instance($cm->id);
$manager = manager::create_from_instance($activity);
$manageractivity = $manager->get_instance();
$this->assertEquals($activity->id, $manageractivity->id);
$managercm = $manager->get_coursemodule();
$this->assertEquals($cm->id, $managercm->id);
$managercontext = $manager->get_context();
$this->assertEquals($context->id, $managercontext->id);
$manager = manager::create_from_coursemodule($cm);
$manageractivity = $manager->get_instance();
$this->assertEquals($activity->id, $manageractivity->id);
$managercm = $manager->get_coursemodule();
$this->assertEquals($cm->id, $managercm->id);
$managercontext = $manager->get_context();
$this->assertEquals($context->id, $managercontext->id);
}
/**
* Test for is_tracking_enabled.
*
* @dataProvider is_tracking_enabled_data
* @param bool $login if the user is logged in
* @param string $role user role in course
* @param int $enabletracking if tracking is enabled
* @param bool $expected expected result
*/
public function test_is_tracking_enabled(bool $login, string $role, int $enabletracking, bool $expected) {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course, 'enabletracking' => $enabletracking]);
$user = $this->getDataGenerator()->create_and_enrol($course, $role);
if ($login) {
$this->setUser($user);
$param = null;
} else {
$param = $user;
}
$manager = manager::create_from_instance($activity);
$this->assertEquals($expected, $manager->is_tracking_enabled($param));
}
/**
* Data provider for is_tracking_enabled.
*
* @return array
*/
public function is_tracking_enabled_data(): array {
return [
'Logged student, tracking enabled' => [
true, 'student', 1, true
],
'Logged student, tracking disabled' => [
true, 'student', 0, false
],
'Logged teacher, tracking enabled' => [
true, 'editingteacher', 1, false
],
'Logged teacher, tracking disabled' => [
true, 'editingteacher', 0, false
],
'No logged student, tracking enabled' => [
true, 'student', 1, true
],
'No logged student, tracking disabled' => [
true, 'student', 0, false
],
'No logged teacher, tracking enabled' => [
true, 'editingteacher', 1, false
],
'No logged teacher, tracking disabled' => [
true, 'editingteacher', 0, false
],
];
}
/**
* Test for get_users_scaled_score.
*
* @dataProvider get_users_scaled_score_data
* @param int $enabletracking if tracking is enabled
* @param int $gradingmethod new grading method
* @param array $result1 student 1 results (scaled, timemodified, attempt number)
* @param array $result2 student 2 results (scaled, timemodified, attempt number)
*/
public function test_get_users_scaled_score(int $enabletracking, int $gradingmethod, array $result1, array $result2) {
global $DB;
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity',
['course' => $course, 'enabletracking' => $enabletracking, 'grademethod' => $gradingmethod]);
// Generate two users with 4 attempts each.
$user1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
$this->generate_fake_attempts($activity, $user1, 1);
$user2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
$this->generate_fake_attempts($activity, $user2, 2);
$manager = manager::create_from_instance($activity);
// Get all users scaled scores.
$scaleds = $manager->get_users_scaled_score();
// No results will be returned if tracking is dsabled or manual grading method is defined.
if (empty($result1)) {
$this->assertNull($scaleds);
return;
}
$this->assertCount(2, $scaleds);
// Check expected user1 scaled score.
$scaled = $scaleds[$user1->id];
$this->assertEquals($user1->id, $scaled->userid);
$this->assertEquals($result1[0], $scaled->scaled);
$this->assertEquals($result1[1], $scaled->timemodified);
if ($result1[2]) {
$attempt = $DB->get_record('h5pactivity_attempts', ['id' => $scaled->attemptid]);
$this->assertEquals($attempt->h5pactivityid, $activity->id);
$this->assertEquals($attempt->userid, $scaled->userid);
$this->assertEquals($attempt->scaled, round($scaled->scaled, 5));
$this->assertEquals($attempt->timemodified, $scaled->timemodified);
$this->assertEquals($result1[2], $attempt->attempt);
} else {
$this->assertEquals(0, $scaled->attemptid);
}
// Check expected user2 scaled score.
$scaled = $scaleds[$user2->id];
$this->assertEquals($user2->id, $scaled->userid);
$this->assertEquals($result2[0], round($scaled->scaled, 5));
$this->assertEquals($result2[1], $scaled->timemodified);
if ($result2[2]) {
$attempt = $DB->get_record('h5pactivity_attempts', ['id' => $scaled->attemptid]);
$this->assertEquals($attempt->h5pactivityid, $activity->id);
$this->assertEquals($attempt->userid, $scaled->userid);
$this->assertEquals($attempt->scaled, $scaled->scaled);
$this->assertEquals($attempt->timemodified, $scaled->timemodified);
$this->assertEquals($result2[2], $attempt->attempt);
} else {
$this->assertEquals(0, $scaled->attemptid);
}
// Now check a single user record.
$scaleds = $manager->get_users_scaled_score($user2->id);
$this->assertCount(1, $scaleds);
$scaled2 = $scaleds[$user2->id];
$this->assertEquals($scaled->userid, $scaled2->userid);
$this->assertEquals($scaled->scaled, $scaled2->scaled);
$this->assertEquals($scaled->attemptid, $scaled2->attemptid);
$this->assertEquals($scaled->timemodified, $scaled2->timemodified);
}
/**
* Data provider for get_users_scaled_score.
*
* @return array
*/
public function get_users_scaled_score_data(): array {
return [
'Tracking with max attempt method' => [
1, manager::GRADEHIGHESTATTEMPT, [1.00000, 31, 2], [0.66667, 32, 2]
],
'Tracking with average attempt method' => [
1, manager::GRADEAVERAGEATTEMPT, [0.61111, 51, 0], [0.52222, 52, 0]
],
'Tracking with last attempt method' => [
1, manager::GRADELASTATTEMPT, [0.33333, 51, 3], [0.40000, 52, 3]
],
'Tracking with first attempt method' => [
1, manager::GRADEFIRSTATTEMPT, [0.50000, 11, 1], [0.50000, 12, 1]
],
'Tracking with manual attempt grading' => [
1, manager::GRADEMANUAL, [], []
],
'No tracking with max attempt method' => [
0, manager::GRADEHIGHESTATTEMPT, [], []
],
'No tracking with average attempt method' => [
0, manager::GRADEAVERAGEATTEMPT, [], []
],
'No tracking with last attempt method' => [
0, manager::GRADELASTATTEMPT, [], []
],
'No tracking with first attempt method' => [
0, manager::GRADEFIRSTATTEMPT, [], []
],
'No tracking with manual attempt grading' => [
0, manager::GRADEMANUAL, [], []
],
];
}
/**
* Test static get_grading_methods.
*/
public function test_get_grading_methods() {
$methods = manager::get_grading_methods();
$this->assertCount(5, $methods);
$this->assertNotEmpty($methods[manager::GRADEHIGHESTATTEMPT]);
$this->assertNotEmpty($methods[manager::GRADEAVERAGEATTEMPT]);
$this->assertNotEmpty($methods[manager::GRADELASTATTEMPT]);
$this->assertNotEmpty($methods[manager::GRADEFIRSTATTEMPT]);
$this->assertNotEmpty($methods[manager::GRADEMANUAL]);
}
/**
* Test get_grader method.
*/
public function test_get_grader() {
$this->resetAfterTest();
$this->setAdminUser();
$course = $this->getDataGenerator()->create_course();
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
$cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST);
$context = context_module::instance($cm->id);
$manager = manager::create_from_instance($activity);
$grader = $manager->get_grader();
$this->assertInstanceOf('mod_h5pactivity\local\grader', $grader);
}
/**
* Insert fake attempt data into h5pactiviyt_attempts.
*
* This function insert 4 attempts. 3 of them finished with different gradings
* and timestamps and 1 unfinished.
*
* @param stdClass $activity the activity record
* @param stdClass $user user record
* @param int $basescore a score to be used to generate all attempts
*/
private function generate_fake_attempts(stdClass $activity, stdClass $user, int $basescore) {
global $DB;
$attempt = (object)[
'h5pactivityid' => $activity->id,
'userid' => $user->id,
'timecreated' => $basescore,
'timemodified' => ($basescore + 10),
'attempt' => 1,
'rawscore' => $basescore,
'maxscore' => ($basescore + $basescore),
'duration' => $basescore,
'completion' => 1,
'success' => 1,
];
$attempt->scaled = $attempt->rawscore / $attempt->maxscore;
$DB->insert_record('h5pactivity_attempts', $attempt);
$attempt = (object)[
'h5pactivityid' => $activity->id,
'userid' => $user->id,
'timecreated' => ($basescore + 20),
'timemodified' => ($basescore + 30),
'attempt' => 2,
'rawscore' => $basescore,
'maxscore' => ($basescore + $basescore - 1),
'duration' => $basescore,
'completion' => 1,
'success' => 1,
];
$attempt->scaled = $attempt->rawscore / $attempt->maxscore;
$DB->insert_record('h5pactivity_attempts', $attempt);
$attempt = (object)[
'h5pactivityid' => $activity->id,
'userid' => $user->id,
'timecreated' => ($basescore + 40),
'timemodified' => ($basescore + 50),
'attempt' => 3,
'rawscore' => $basescore,
'maxscore' => ($basescore + $basescore + 1),
'duration' => $basescore,
'completion' => 1,
'success' => 0,
];
$attempt->scaled = $attempt->rawscore / $attempt->maxscore;
$DB->insert_record('h5pactivity_attempts', $attempt);
// Unfinished attempt.
$attempt = (object)[
'h5pactivityid' => $activity->id,
'userid' => $user->id,
'timecreated' => ($basescore + 60),
'timemodified' => ($basescore + 60),
'attempt' => 4,
'rawscore' => $basescore,
'maxscore' => $basescore,
'duration' => $basescore,
];
$DB->insert_record('h5pactivity_attempts', $attempt);
}
}
+2
View File
@@ -111,6 +111,8 @@ class mod_h5pactivity_restore_testcase extends advanced_testcase {
$this->assertEquals($activity->introformat, $activity2->introformat);
$this->assertEquals($activity->grade, $activity2->grade);
$this->assertEquals($activity->displayoptions, $activity2->displayoptions);
$this->assertEquals($activity->enabletracking, $activity2->enabletracking);
$this->assertEquals($activity->grademethod, $activity2->grademethod);
// Compare attempts.
if ($content && $userdata) {
+1 -1
View File
@@ -25,5 +25,5 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_h5pactivity';
$plugin->version = 2020041400;
$plugin->version = 2020041401;
$plugin->requires = 2020013000;
+15 -7
View File
@@ -22,6 +22,12 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
use mod_h5pactivity\local\manager;
use mod_h5pactivity\event\course_module_viewed;
use core_h5p\factory;
use core_h5p\player;
use core_h5p\helper;
require(__DIR__.'/../../config.php');
require_once(__DIR__.'/lib.php');
require_once($CFG->libdir.'/completionlib.php');
@@ -32,11 +38,13 @@ list ($course, $cm) = get_course_and_cm_from_cmid($id, 'h5pactivity');
require_login($course, true, $cm);
$moduleinstance = $DB->get_record('h5pactivity', ['id' => $cm->instance], '*', MUST_EXIST);
$manager = manager::create_from_coursemodule($cm);
$context = context_module::instance($cm->id);
$moduleinstance = $manager->get_instance();
$event = \mod_h5pactivity\event\course_module_viewed::create([
$context = $manager->get_context();
$event = course_module_viewed::create([
'objectid' => $moduleinstance->id,
'context' => $context
]);
@@ -49,9 +57,9 @@ $completion = new completion_info($course);
$completion->set_module_viewed($cm);
// Convert display options to a valid object.
$factory = new \core_h5p\factory();
$factory = new factory();
$core = $factory->get_core();
$config = \core_h5p\helper::decode_display_options($core, $moduleinstance->displayoptions);
$config = core_h5p\helper::decode_display_options($core, $moduleinstance->displayoptions);
// Instantiate player.
$fs = get_file_storage();
@@ -73,7 +81,7 @@ $PAGE->set_context($context);
echo $OUTPUT->header();
echo $OUTPUT->heading(format_string($moduleinstance->name));
if (has_capability('mod/h5pactivity:submit', $context, null, false)) {
if ($manager->is_tracking_enabled()) {
$trackcomponent = 'mod_h5pactivity';
} else {
$trackcomponent = '';
@@ -81,6 +89,6 @@ if (has_capability('mod/h5pactivity:submit', $context, null, false)) {
echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING);
}
echo \core_h5p\player::display($fileurl, $config, true, $trackcomponent);
echo player::display($fileurl, $config, true, $trackcomponent);
echo $OUTPUT->footer();