// 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');
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <ferran@moodle.com>
+ * @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;
}
}
// 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();
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.
*/
* @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;
}
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;
+ }
}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * H5P activity grader class.
+ *
+ * @package mod_h5pactivity
+ * @since Moodle 3.9
+ * @copyright 2020 Ferran Recio <ferran@moodle.com>
+ * @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 <ferran@moodle.com>
+ * @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;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * H5P activity manager class
+ *
+ * @package mod_h5pactivity
+ * @since Moodle 3.9
+ * @copyright 2020 Ferran Recio <ferran@moodle.com>
+ * @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 <ferran@moodle.com>
+ * @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);
+ }
+}
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;
defined('MOODLE_INTERNAL') || die();
+require_once($CFG->dirroot.'/mod/h5pactivity/lib.php');
+
/**
* Class xapi_handler for H5P statements.
*
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
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();
<?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"
>
<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"/>
<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."/>
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;
}
$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.
$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';
* @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();
/**
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);
}
* @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);
}
/**
* @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();
}
/**
* @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;
}
/**
if ($activities = $DB->get_records_sql($sql, [$courseid])) {
foreach ($activities as $activity) {
- h5pactivity_grade_item_update($activity, true);
+ h5pactivity_grade_item_update($activity, 'reset');
}
}
}
* @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');
// 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();
$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;
+ }
}
}
--- /dev/null
+@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
--- /dev/null
+@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 % |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+use mod_h5pactivity\local\manager;
+
defined('MOODLE_INTERNAL') || die();
$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)) {
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
+use mod_h5pactivity\local\manager;
+
defined('MOODLE_INTERNAL') || die();
/**
$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);
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
- * mod_h5pactivity generator tests
+ * mod_h5pactivity attempt tests
*
* @package mod_h5pactivity
* @category test
$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);
$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());
+ }
}
/**
];
}
+ /**
+ * 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.
*
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * mod_h5pactivity grader tests
+ *
+ * @package mod_h5pactivity
+ * @category test
+ * @copyright 2020 Ferran Recio <ferran@moodle.com>
+ * @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 <ferran@moodle.com>
+ * @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;
+ }
+}
--- /dev/null
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * mod_h5pactivity manager tests
+ *
+ * @package mod_h5pactivity
+ * @category test
+ * @copyright 2020 Ferran Recio <ferran@moodle.com>
+ * @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 <ferran@moodle.com>
+ * @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);
+ }
+}
$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) {
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_h5pactivity';
-$plugin->version = 2020041400;
+$plugin->version = 2020041401;
$plugin->requires = 2020013000;
* @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');
require_login($course, true, $cm);
-$moduleinstance = $DB->get_record('h5pactivity', ['id' => $cm->instance], '*', MUST_EXIST);
+$manager = manager::create_from_coursemodule($cm);
+
+$moduleinstance = $manager->get_instance();
-$context = context_module::instance($cm->id);
+$context = $manager->get_context();
-$event = \mod_h5pactivity\event\course_module_viewed::create([
+$event = course_module_viewed::create([
'objectid' => $moduleinstance->id,
'context' => $context
]);
$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();
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 = '';
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();