MDL-68241 mod_h5pactivity: add grading attempts options
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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."/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,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 | 0–100 | 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 | 0–50 | 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 | 0–100 | 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 | 0–50 | 66.67 % |
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -25,5 +25,5 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'mod_h5pactivity';
|
||||
$plugin->version = 2020041400;
|
||||
$plugin->version = 2020041401;
|
||||
$plugin->requires = 2020013000;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user