MDL-67788 mod_h5pactivity: add xAPI attempts track to activity
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Plugin event classes are defined here.
|
||||
* H5P Activity list viewed event.
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @copyright 2020 Ferran Recio <[email protected]>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Plugin event classes are defined here.
|
||||
* H5P activity viewed.
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @copyright 2020 Ferran Recio <[email protected]>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?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 send an xAPI tracking statement.
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @copyright 2020 Ferran Recio <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace mod_h5pactivity\event;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* The statement_received event class.
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @copyright 2020 Ferran Recio <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class statement_received extends \core\event\base {
|
||||
|
||||
/**
|
||||
* Init method.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function init(): void {
|
||||
$this->data['objecttable'] = 'h5pactivity';
|
||||
$this->data['crud'] = 'u';
|
||||
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns localised general event name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_name() {
|
||||
return get_string('statement_received', 'mod_h5pactivity');
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace add_to_log() statement.
|
||||
*
|
||||
* @return array of parameters to be passed to legacy add_to_log() function.
|
||||
*/
|
||||
protected function get_legacy_logdata() {
|
||||
return [$this->courseid, 'h5pactivity', 'statement received', 'grade.php?user=' . $this->userid,
|
||||
0, $this->contextinstanceid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns non-localised description of what happened.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_description() {
|
||||
return "The user with the id '$this->userid' send a tracking statement " .
|
||||
"for a H5P activity with the course module id '$this->contextinstanceid'.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL related to the action
|
||||
*
|
||||
* @return \moodle_url
|
||||
*/
|
||||
public function get_url() {
|
||||
return new \moodle_url('/mod/h5pactivity/grade.php',
|
||||
['id' => $this->contextinstanceid, 'user' => $this->userid]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used when restoring course logs where it is required that we
|
||||
* map the objectid to it's new value in the new course.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_objectid_mapping() {
|
||||
return ['db' => 'h5pactivity', 'restore' => 'h5pactivity'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
<?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 attempt object
|
||||
*
|
||||
* @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 stdClass;
|
||||
use core_xapi\local\statement;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Class attempt for H5P activity
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @since Moodle 3.9
|
||||
* @copyright 2020 Ferran Recio <[email protected]>
|
||||
*/
|
||||
class attempt {
|
||||
|
||||
/** @var stdClass the h5pactivity_attempts record. */
|
||||
private $record;
|
||||
|
||||
/**
|
||||
* Create a new attempt object.
|
||||
*
|
||||
* @param stdClass $record the h5pactivity_attempts record
|
||||
*/
|
||||
protected function __construct(stdClass $record) {
|
||||
$this->record = $record;
|
||||
$this->results = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new user attempt in a specific H5P activity.
|
||||
*
|
||||
* @param stdClass $user a user record
|
||||
* @param stdClass $cm a course_module record
|
||||
* @return attempt|null a new attempt object or null if fail
|
||||
*/
|
||||
public static function new_attempt(stdClass $user, stdClass $cm): ?attempt {
|
||||
global $DB;
|
||||
$record = new stdClass();
|
||||
$record->h5pactivityid = $cm->instance;
|
||||
$record->userid = $user->id;
|
||||
$record->timecreated = time();
|
||||
$record->timemodified = $record->timecreated;
|
||||
$record->rawscore = 0;
|
||||
$record->maxscore = 0;
|
||||
|
||||
// Get last attempt number.
|
||||
$conditions = ['h5pactivityid' => $cm->instance, 'userid' => $user->id];
|
||||
$countattempts = $DB->count_records('h5pactivity_attempts', $conditions);
|
||||
$record->attempt = $countattempts + 1;
|
||||
|
||||
$record->id = $DB->insert_record('h5pactivity_attempts', $record);
|
||||
if (!$record->id) {
|
||||
return null;
|
||||
}
|
||||
return new attempt($record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last user attempt in a specific H5P activity.
|
||||
*
|
||||
* If no previous attempt exists, it generates a new one.
|
||||
*
|
||||
* @param stdClass $user a user record
|
||||
* @param stdClass $cm a course_module record
|
||||
* @return attempt|null a new attempt object or null if some problem accured
|
||||
*/
|
||||
public static function last_attempt(stdClass $user, stdClass $cm): ?attempt {
|
||||
global $DB;
|
||||
$conditions = ['h5pactivityid' => $cm->instance, 'userid' => $user->id];
|
||||
$records = $DB->get_records('h5pactivity_attempts', $conditions, 'attempt DESC', '*', 0, 1);
|
||||
if (empty($records)) {
|
||||
return self::new_attempt($user, $cm);
|
||||
}
|
||||
return new attempt(array_shift($records));
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipe all attempt data for specific course_module and an optional user.
|
||||
*
|
||||
* @param stdClass $cm a course_module record
|
||||
* @param stdClass $user a user record
|
||||
*/
|
||||
public static function delete_all_attempts(stdClass $cm, stdClass $user = null): void {
|
||||
global $DB;
|
||||
|
||||
$where = 'a.h5pactivityid = :h5pactivityid';
|
||||
$conditions = ['h5pactivityid' => $cm->instance];
|
||||
if (!empty($user)) {
|
||||
$where .= ' AND a.userid = :userid';
|
||||
$conditions['userid'] = $user->id;
|
||||
}
|
||||
|
||||
$DB->delete_records_select('h5pactivity_attempts_results', "attemptid IN (
|
||||
SELECT a.id
|
||||
FROM {h5pactivity_attempts} a
|
||||
WHERE $where)", $conditions);
|
||||
|
||||
$DB->delete_records('h5pactivity_attempts', $conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific attempt.
|
||||
*
|
||||
* @param attempt $attempt the attempt object to delete
|
||||
*/
|
||||
public static function delete_attempt(attempt $attempt): void {
|
||||
global $DB;
|
||||
$attempt->delete_results();
|
||||
$DB->delete_records('h5pactivity_attempts', ['id' => $attempt->get_id()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new result statement into the attempt.
|
||||
*
|
||||
* It also updates the rawscore and maxscore if necessary.
|
||||
*
|
||||
* @param statement $statement the xAPI statement object
|
||||
* @param string $subcontent = '' optional subcontent identifier
|
||||
* @return bool if it can save the statement into db
|
||||
*/
|
||||
public function save_statement(statement $statement, string $subcontent = ''): bool {
|
||||
global $DB;
|
||||
|
||||
// Check statement data.
|
||||
$xapiobject = $statement->get_object();
|
||||
if (empty($xapiobject)) {
|
||||
return false;
|
||||
}
|
||||
$xapiresult = $statement->get_result();
|
||||
$xapidefinition = $xapiobject->get_definition();
|
||||
if (empty($xapidefinition) || empty($xapiresult)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$xapicontext = $statement->get_context();
|
||||
if ($xapicontext) {
|
||||
$context = $xapicontext->get_data();
|
||||
} else {
|
||||
$context = new stdClass();
|
||||
}
|
||||
$definition = $xapidefinition->get_data();
|
||||
$result = $xapiresult->get_data();
|
||||
|
||||
// Insert attempt_results record.
|
||||
$record = new stdClass();
|
||||
$record->attemptid = $this->record->id;
|
||||
$record->subcontent = $subcontent;
|
||||
$record->timecreated = time();
|
||||
$record->interactiontype = $definition->interactionType ?? 'other';
|
||||
$record->description = $this->get_description_from_definition($definition);
|
||||
$record->correctpattern = $this->get_correctpattern_from_definition($definition);
|
||||
$record->response = $result->response ?? '';
|
||||
$record->additionals = $this->get_additionals($definition, $context);
|
||||
$record->rawscore = 0;
|
||||
$record->maxscore = 0;
|
||||
if (isset($result->score)) {
|
||||
$record->rawscore = $result->score->raw ?? 0;
|
||||
$record->maxscore = $result->score->max ?? 0;
|
||||
}
|
||||
if (!$DB->insert_record('h5pactivity_attempts_results', $record)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
// Refresh current attempt.
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current attempt record into DB.
|
||||
*
|
||||
* @return bool true if update is succesful
|
||||
*/
|
||||
public function save(): bool {
|
||||
global $DB;
|
||||
$this->record->timemodified = time();
|
||||
return $DB->update_record('h5pactivity_attempts', $this->record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the current attempt results from the DB.
|
||||
*/
|
||||
public function delete_results(): void {
|
||||
global $DB;
|
||||
$conditions = ['attemptid' => $this->record->id];
|
||||
$DB->delete_records('h5pactivity_attempts_results', $conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return de number of results stored in this attempt.
|
||||
*
|
||||
* @return int the number of results stored in this attempt.
|
||||
*/
|
||||
public function count_results(): int {
|
||||
global $DB;
|
||||
$conditions = ['attemptid' => $this->record->id];
|
||||
return $DB->count_records('h5pactivity_attempts_results', $conditions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get additional data for some interaction types.
|
||||
*
|
||||
* @param stdClass $definition the statement object definition data
|
||||
* @param stdClass $context the statement optional context
|
||||
* @return string JSON encoded additional information
|
||||
*/
|
||||
private function get_additionals(stdClass $definition, stdClass $context): string {
|
||||
$additionals = [];
|
||||
$interactiontype = $definition->interactionType ?? 'other';
|
||||
switch ($interactiontype) {
|
||||
case 'choice':
|
||||
case 'sequencing':
|
||||
$additionals['choices'] = $definition->choices ?? [];
|
||||
break;
|
||||
|
||||
case 'matching':
|
||||
$additionals['source'] = $definition->source ?? [];
|
||||
$additionals['target'] = $definition->target ?? [];
|
||||
break;
|
||||
|
||||
case 'likert':
|
||||
$additionals['scale'] = $definition->scale ?? [];
|
||||
break;
|
||||
|
||||
case 'performance':
|
||||
$additionals['steps'] = $definition->steps ?? [];
|
||||
break;
|
||||
}
|
||||
|
||||
$additionals['extensions'] = $definition->extensions ?? new stdClass();
|
||||
|
||||
// Add context extensions.
|
||||
$additionals['contextExtensions'] = $context->extensions ?? new stdClass();
|
||||
|
||||
if (empty($additionals)) {
|
||||
return '';
|
||||
}
|
||||
return json_encode($additionals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the result description from statement object definition.
|
||||
*
|
||||
* In principle, H5P package can send a multilang description but the reality
|
||||
* is that most activities only send the "en_US" description if any and the
|
||||
* activity does not have any control over it.
|
||||
*
|
||||
* @param stdClass $definition the statement object definition
|
||||
* @return string The available description if any
|
||||
*/
|
||||
private function get_description_from_definition(stdClass $definition): string {
|
||||
if (!isset($definition->description)) {
|
||||
return '';
|
||||
}
|
||||
$translations = (array) $definition->description;
|
||||
if (empty($translations)) {
|
||||
return '';
|
||||
}
|
||||
// By default, H5P packages only send "en-US" descriptions.
|
||||
return $translations['en-US'] ?? array_shift($translations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the correct pattern from statement object definition.
|
||||
*
|
||||
* The correct pattern depends on the type of content and the plugin
|
||||
* has no control over it so we just store it in case that the statement
|
||||
* data have it.
|
||||
*
|
||||
* @param stdClass $definition the statement object definition
|
||||
* @return string The correct pattern if any
|
||||
*/
|
||||
private function get_correctpattern_from_definition(stdClass $definition): string {
|
||||
if (!isset($definition->correctResponsesPattern)) {
|
||||
return '';
|
||||
}
|
||||
// Only arrays are allowed.
|
||||
if (is_array($definition->correctResponsesPattern)) {
|
||||
return json_encode($definition->correctResponsesPattern);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the attempt number.
|
||||
*
|
||||
* @return int the attempt number
|
||||
*/
|
||||
public function get_attempt(): int {
|
||||
return $this->record->attempt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the attempt ID.
|
||||
*
|
||||
* @return int the attempt id
|
||||
*/
|
||||
public function get_id(): int {
|
||||
return $this->record->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the attempt user ID.
|
||||
*
|
||||
* @return int the attempt userid
|
||||
*/
|
||||
public function get_userid(): int {
|
||||
return $this->record->userid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the attempt H5P activity ID.
|
||||
*
|
||||
* @return int the attempt userid
|
||||
*/
|
||||
public function get_h5pactivityid(): int {
|
||||
return $this->record->h5pactivityid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the attempt maxscore.
|
||||
*
|
||||
* @return int the maxscore value
|
||||
*/
|
||||
public function get_maxscore(): int {
|
||||
return $this->record->maxscore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the attempt rawscore.
|
||||
*
|
||||
* @return int the rawscore value
|
||||
*/
|
||||
public function get_rawscore(): int {
|
||||
return $this->record->maxscore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* The xapi_handler for xAPI statements.
|
||||
*
|
||||
* @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\xapi;
|
||||
|
||||
use mod_h5pactivity\local\attempt;
|
||||
use mod_h5pactivity\event\statement_received;
|
||||
use core_xapi\local\statement;
|
||||
use core_xapi\handler as handler_base;
|
||||
use core\event\base as event_base;
|
||||
use context_module;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Class xapi_handler for H5P statements.
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @since Moodle 3.9
|
||||
* @copyright 2020 Ferran Recio <[email protected]>
|
||||
*/
|
||||
class handler extends handler_base {
|
||||
|
||||
/**
|
||||
* Convert a statement object into a Moodle xAPI Event.
|
||||
*
|
||||
* If a statement is accepted by the xAPI webservice the component must provide
|
||||
* an event to handle that statement, otherwise the statement will be rejected.
|
||||
*
|
||||
* @param statement $statement
|
||||
* @return core\event\base|null a Moodle event to trigger
|
||||
*/
|
||||
public function statement_to_event(statement $statement): ?event_base {
|
||||
|
||||
// Only process statements with results.
|
||||
$xapiresult = $statement->get_result();
|
||||
if (empty($xapiresult)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Statements can contain any verb, for security reasons each
|
||||
// plugin needs to filter it's own specific verbs. For now the only verbs the H5P
|
||||
// plugin keeps track on are "answered" and "completed" because they are realted to grading.
|
||||
// In the future this list can be increased to track more user interactions.
|
||||
$validvalues = [
|
||||
'http://adlnet.gov/expapi/verbs/answered',
|
||||
'http://adlnet.gov/expapi/verbs/completed',
|
||||
];
|
||||
$xapiverbid = $statement->get_verb_id();
|
||||
if (!in_array($xapiverbid, $validvalues)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Validate object.
|
||||
$xapiobject = $statement->get_activity_id();
|
||||
|
||||
// H5P add some extra params to ID to define subcontents.
|
||||
$parts = explode('?', $xapiobject, 2);
|
||||
$contextid = array_shift($parts);
|
||||
$subcontent = str_replace('subContentId=', '', array_shift($parts));
|
||||
if (empty($contextid) || !is_numeric($contextid)) {
|
||||
return null;
|
||||
}
|
||||
$context = \context::instance_by_id($contextid);
|
||||
if (!$context instanceof \context_module) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// As the activity does not accept group statement, the code can assume that the
|
||||
// statement user is valid (otherwise the xAPI library will reject the statement).
|
||||
$user = $statement->get_user();
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
// indicates a new attempt creation. This way, simpler H5P activies like multichoice can generate
|
||||
// an attempt each time the user answers while complex like question-set could group all questions
|
||||
// in a single attempt (using subcontents).
|
||||
if (empty($subcontent)) {
|
||||
$attempt = attempt::new_attempt($user, $cm);
|
||||
} else {
|
||||
$attempt = attempt::last_attempt($user, $cm);
|
||||
}
|
||||
if (!$attempt) {
|
||||
return null;
|
||||
}
|
||||
$result = $attempt->save_statement($statement, $subcontent);
|
||||
if (!$result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: update grading if necessary.
|
||||
|
||||
// Convert into a Moodle event.
|
||||
$minstatement = $statement->minify();
|
||||
$params = [
|
||||
'other' => $minstatement,
|
||||
'context' => $context,
|
||||
'objectid' => $cm->instance,
|
||||
'userid' => $user->id,
|
||||
];
|
||||
return statement_received::create($params);
|
||||
}
|
||||
}
|
||||
@@ -48,4 +48,12 @@ $capabilities = [
|
||||
],
|
||||
'clonepermissionsfrom' => 'moodle/course:manageactivities',
|
||||
],
|
||||
|
||||
'mod/h5pactivity:submit' => [
|
||||
'captype' => 'write',
|
||||
'contextlevel' => CONTEXT_MODULE,
|
||||
'archetypes' => [
|
||||
'student' => CAP_ALLOW
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<XMLDB PATH="mod/h5pactivity/db" VERSION="20200227" COMMENT="XMLDB file for Moodle mod_h5pactivity"
|
||||
<XMLDB PATH="mod/h5pactivity/db" VERSION="20200410" COMMENT="XMLDB file for Moodle mod_h5pactivity"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../lib/xmldb/xmldb.xsd"
|
||||
>
|
||||
@@ -9,8 +9,8 @@
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="course" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="ID of the course this activity is part of."/>
|
||||
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The name of the activity module instance"/>
|
||||
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Timestamp of when the instance was added to the course."/>
|
||||
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Timestamp of when the instance was last modified."/>
|
||||
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Timestamp of when the instance was added to the course."/>
|
||||
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Timestamp of when the instance was last modified."/>
|
||||
<FIELD NAME="intro" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Activity description."/>
|
||||
<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"/>
|
||||
@@ -21,5 +21,49 @@
|
||||
<KEY NAME="fk_course" TYPE="foreign" FIELDS="course" REFTABLE="course" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="h5pactivity_attempts" COMMENT="Users attempts inside H5P activities">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="h5pactivityid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="H5P activity ID"/>
|
||||
<FIELD NAME="userid" TYPE="int" LENGTH="20" NOTNULL="true" SEQUENCE="false" COMMENT="Attempt user ID"/>
|
||||
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<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"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="fk_h5pactivityid" TYPE="foreign" FIELDS="h5pactivityid" REFTABLE="h5pactivity" REFFIELDS="id"/>
|
||||
<KEY NAME="uq_activityuserattempt" TYPE="unique" FIELDS="h5pactivityid, userid, attempt" COMMENT="Ensure a user cannot repeat the same attempt on the same activity"/>
|
||||
</KEYS>
|
||||
<INDEXES>
|
||||
<INDEX NAME="timecreated" UNIQUE="false" FIELDS="timecreated"/>
|
||||
<INDEX NAME="h5pactivityid-timecreated" UNIQUE="false" FIELDS="h5pactivityid, timecreated"/>
|
||||
<INDEX NAME="h5pactivityid-userid" UNIQUE="false" FIELDS="h5pactivityid, userid"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
<TABLE NAME="h5pactivity_attempts_results" COMMENT="H5Pactivities_attempts tracking info">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="attemptid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="h5pactivity_attempts ID"/>
|
||||
<FIELD NAME="subcontent" TYPE="char" LENGTH="128" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="interactiontype" TYPE="char" LENGTH="128" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="description" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="correctpattern" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Correct Pattern in xAPI format"/>
|
||||
<FIELD NAME="response" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="User response data in xAPI format"/>
|
||||
<FIELD NAME="additionals" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Extra subcontent information in JSON format"/>
|
||||
<FIELD NAME="rawscore" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="maxscore" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
<KEY NAME="fk_attemptid" TYPE="foreign" FIELDS="attemptid" REFTABLE="h5pactivity_attempts" REFFIELDS="id"/>
|
||||
</KEYS>
|
||||
<INDEXES>
|
||||
<INDEX NAME="attemptid-timecreated" UNIQUE="false" FIELDS="attemptid, timecreated"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
</XMLDB>
|
||||
@@ -0,0 +1,138 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* This file keeps track of upgrades to the h5pactivity module
|
||||
*
|
||||
* Sometimes, changes between versions involve
|
||||
* alterations to database structures and other
|
||||
* major things that may break installations.
|
||||
*
|
||||
* The upgrade function in this file will attempt
|
||||
* to perform all the necessary actions to upgrade
|
||||
* your older installation to the current version.
|
||||
*
|
||||
* If there's something it cannot do itself, it
|
||||
* will tell you what you need to do.
|
||||
*
|
||||
* The commands in here will all be database-neutral,
|
||||
* using the methods of database_manager class
|
||||
*
|
||||
* Please do not forget to use upgrade_set_timeout()
|
||||
* before any action that may take longer time to finish.
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @copyright 2020 Ferran Recio <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* H5P activity module upgrade.
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @copyright 2017 Stephen Bourget
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Function to upgrade mod_h5pactivity.
|
||||
* @param int $oldversion the version we are upgrading from
|
||||
* @return bool result
|
||||
*/
|
||||
function xmldb_h5pactivity_upgrade($oldversion) {
|
||||
global $DB;
|
||||
|
||||
$dbman = $DB->get_manager(); // Loads ddl manager and xmldb classes.
|
||||
|
||||
if ($oldversion < 2020032300) {
|
||||
|
||||
// Changing the default of field timecreated on table h5pactivity to drop it.
|
||||
$table = new xmldb_table('h5pactivity');
|
||||
$field = new xmldb_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'name');
|
||||
|
||||
// Launch change of default for field timecreated.
|
||||
$dbman->change_field_default($table, $field);
|
||||
|
||||
// Changing the default of field timemodified on table h5pactivity to drop it.
|
||||
$field = new xmldb_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null, 'timecreated');
|
||||
|
||||
// Launch change of default for field timemodified.
|
||||
$dbman->change_field_default($table, $field);
|
||||
|
||||
// Define table h5pactivity_attempts to be created.
|
||||
$table = new xmldb_table('h5pactivity_attempts');
|
||||
|
||||
// Adding fields to table h5pactivity_attempts.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('h5pactivityid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('userid', XMLDB_TYPE_INTEGER, '20', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('attempt', XMLDB_TYPE_INTEGER, '6', null, XMLDB_NOTNULL, null, '1');
|
||||
$table->add_field('rawscore', XMLDB_TYPE_INTEGER, '10', null, null, null, '0');
|
||||
$table->add_field('maxscore', XMLDB_TYPE_INTEGER, '10', null, null, null, '0');
|
||||
|
||||
// Adding keys to table h5pactivity_attempts.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
|
||||
$table->add_key('fk_h5pactivityid', XMLDB_KEY_FOREIGN, ['h5pactivityid'], 'h5pactivity', ['id']);
|
||||
$table->add_key('uq_activityuserattempt', XMLDB_KEY_UNIQUE, ['h5pactivityid', 'userid', 'attempt']);
|
||||
|
||||
// Adding indexes to table h5pactivity_attempts.
|
||||
$table->add_index('timecreated', XMLDB_INDEX_NOTUNIQUE, ['timecreated']);
|
||||
$table->add_index('h5pactivityid-timecreated', XMLDB_INDEX_NOTUNIQUE, ['h5pactivityid', 'timecreated']);
|
||||
$table->add_index('h5pactivityid-userid', XMLDB_INDEX_NOTUNIQUE, ['h5pactivityid', 'userid']);
|
||||
|
||||
// Conditionally launch create table for h5pactivity_attempts.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Define table h5pactivity_attempts_results to be created.
|
||||
$table = new xmldb_table('h5pactivity_attempts_results');
|
||||
|
||||
// Adding fields to table h5pactivity_attempts_results.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('attemptid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('subcontent', XMLDB_TYPE_CHAR, '128', null, null, null, null);
|
||||
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('interactiontype', XMLDB_TYPE_CHAR, '128', null, null, null, null);
|
||||
$table->add_field('description', XMLDB_TYPE_TEXT, null, null, null, null, null);
|
||||
$table->add_field('correctpattern', XMLDB_TYPE_TEXT, null, null, null, null, null);
|
||||
$table->add_field('response', XMLDB_TYPE_TEXT, null, null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('additionals', XMLDB_TYPE_TEXT, null, null, null, null, null);
|
||||
$table->add_field('rawscore', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
|
||||
$table->add_field('maxscore', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
|
||||
|
||||
// Adding keys to table h5pactivity_attempts_results.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
|
||||
$table->add_key('fk_attemptid', XMLDB_KEY_FOREIGN, ['attemptid'], 'h5pactivity_attempts', ['id']);
|
||||
|
||||
// Adding indexes to table h5pactivity_attempts_results.
|
||||
$table->add_index('attemptid-timecreated', XMLDB_INDEX_NOTUNIQUE, ['attemptid', 'timecreated']);
|
||||
|
||||
// Conditionally launch create table for h5pactivity_attempts_results.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// H5pactivity savepoint reached.
|
||||
upgrade_mod_savepoint(true, 2020032300, 'h5pactivity');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -26,10 +26,13 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$string['areapackage'] = 'Package file';
|
||||
$string['attempt'] = 'Attempt';
|
||||
$string['deleteallattempts'] = 'Delete all H5P attempts';
|
||||
$string['displayexport'] = 'Allow download';
|
||||
$string['displayembed'] = 'Embed button';
|
||||
$string['displaycopyright'] = 'Copyright button';
|
||||
$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';
|
||||
@@ -39,9 +42,13 @@ $string['modulename'] = 'H5P activity';
|
||||
$string['modulename_help'] = 'Use this module to use a H5P compatible content as a course activity.';
|
||||
$string['modulename_link'] = 'mod/h5pactivity/view';
|
||||
$string['modulenameplural'] = 'H5P activities';
|
||||
$string['myattempts'] = 'My attempts';
|
||||
$string['package'] = 'Package file';
|
||||
$string['package_help'] = 'The package file is a h5pfile containing H5P dynamic content.';
|
||||
$string['page-mod-h5pactivity-x'] = 'Any H5P module page';
|
||||
$string['pluginadministration'] = 'H5P administration';
|
||||
$string['pluginname'] = 'H5P activity';
|
||||
$string['previewmode'] = 'This content is displayed in preview mode. No attempt tracking will be stored.';
|
||||
$string['privacy:metadata'] = 'The H5P activity plugin does not store any personal data.';
|
||||
$string['statement_received'] = 'xAPI statement received';
|
||||
$string['view'] = 'View';
|
||||
|
||||
+54
-6
@@ -80,6 +80,7 @@ function h5pactivity_add_instance(stdClass $data, mod_h5pactivity_mod_form $mfor
|
||||
global $DB;
|
||||
|
||||
$data->timecreated = time();
|
||||
$data->timemodified = $data->timecreated;
|
||||
$cmid = $data->coursemodule;
|
||||
|
||||
$data->id = $DB->insert_record('h5pactivity', $data);
|
||||
@@ -229,18 +230,65 @@ function h5pactivity_update_grades(stdClass $moduleinstance, int $userid = 0): v
|
||||
'h5pactivity', $moduleinstance->id, 0, $grades);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of the function for printing the form elements that control
|
||||
* whether the course reset functionality affects the H5P activity.
|
||||
*
|
||||
* @param object $mform form passed by reference
|
||||
*/
|
||||
function h5pactivity_reset_course_form_definition(&$mform): void {
|
||||
$mform->addElement('header', 'h5pactivityheader', get_string('modulenameplural', 'mod_h5pactivity'));
|
||||
$mform->addElement('advcheckbox', 'reset_h5pactivity', get_string('deleteallattempts', 'mod_h5pactivity'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Course reset form defaults.
|
||||
*
|
||||
* @param stdClass $course the course object
|
||||
* @return array
|
||||
*/
|
||||
function h5pactivity_reset_course_form_defaults(stdClass $course): array {
|
||||
return ['reset_h5pactivity' => 1];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This function is used by the reset_course_userdata function in moodlelib.
|
||||
* This function will remove all assignment submissions and feedbacks in the database
|
||||
*
|
||||
* This function will remove all H5P attempts in the database
|
||||
* and clean up any related data.
|
||||
*
|
||||
* @param stdClass $data the data submitted from the reset course.
|
||||
* @return array
|
||||
* @return array of reseting status
|
||||
*/
|
||||
function h5pactivity_reset_userdata($data) {
|
||||
function h5pactivity_reset_userdata(stdClass $data): array {
|
||||
global $CFG, $DB;
|
||||
// TODO: When attempts are created this function will remove them.
|
||||
return [];
|
||||
$componentstr = get_string('modulenameplural', 'mod_h5pactivity');
|
||||
$status = [];
|
||||
if (!empty($data->reset_h5pactivity)) {
|
||||
$params = ['courseid' => $data->courseid];
|
||||
$sql = "SELECT a.id FROM {h5pactivity} a WHERE a.course=:courseid";
|
||||
if ($activities = $DB->get_records_sql($sql, $params)) {
|
||||
foreach ($activities as $activity) {
|
||||
$cm = get_coursemodule_from_instance('h5pactivity',
|
||||
$activity->id,
|
||||
$data->courseid,
|
||||
false,
|
||||
MUST_EXIST);
|
||||
mod_h5pactivity\local\attempt::delete_all_attempts ($cm);
|
||||
}
|
||||
}
|
||||
// Remove all grades from gradebook.
|
||||
if (empty($data->reset_gradebook_grades)) {
|
||||
h5pactivity_reset_gradebook($data->courseid, 'reset');
|
||||
}
|
||||
$status[] = [
|
||||
'component' => $componentstr,
|
||||
'item' => get_string('deleteallattempts', 'mod_h5pactivity'),
|
||||
'error' => false,
|
||||
];
|
||||
}
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -254,7 +302,7 @@ function h5pactivity_reset_gradebook(int $courseid, string $type=''): void {
|
||||
|
||||
$sql = "SELECT a.*, cm.idnumber as cmidnumber, a.course as courseid
|
||||
FROM {h5pactivity} a, {course_modules} cm, {modules} m
|
||||
WHERE m.name='h5pactivity' AND m.id=cm.module AND cm.instance=s.id AND s.course=?";
|
||||
WHERE m.name='h5pactivity' AND m.id=cm.module AND cm.instance=a.id AND a.course=?";
|
||||
|
||||
if ($activities = $DB->get_records_sql($sql, [$courseid])) {
|
||||
foreach ($activities as $activity) {
|
||||
|
||||
@@ -20,7 +20,6 @@ Feature: Add H5P activity
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
|
||||
|
||||
@javascript
|
||||
Scenario: Add a h5pactivity activity to a course
|
||||
When I add a "H5P activity" to section "1"
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe
|
||||
Feature: Do a H5P attempt
|
||||
In order to let students do a H5P attempt
|
||||
As a teacher
|
||||
I need to list students attempts on the log report
|
||||
|
||||
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 activity" 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
|
||||
|
||||
@javascript
|
||||
Scenario: View an H5P as a teacher
|
||||
When I click on "Save and display" "button"
|
||||
And I wait until the page is ready
|
||||
Then I should see "This content is displayed in preview mode"
|
||||
|
||||
@javascript
|
||||
Scenario: To an attempts and check on course log report
|
||||
When I click on "Save and return to course" "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 wait until the page is ready
|
||||
And I should not see "This content is displayed in preview mode"
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
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
|
||||
And I log out
|
||||
And I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage
|
||||
And I navigate to course participants
|
||||
And I follow "Student 1"
|
||||
Then I follow "Today's logs"
|
||||
And I should see "xAPI statement received"
|
||||
@@ -0,0 +1,73 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Events test.
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @copyright 2020 Ferran Recio <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace mod_h5pactivity\event;
|
||||
|
||||
use advanced_testcase;
|
||||
use context_course;
|
||||
use context_module;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* H5P activity events test cases.
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @copyright 2020 Ferran Recio <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class course_module_instance_list_viewed_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Test course_module_instance_list_viewed event.
|
||||
*/
|
||||
public function test_course_module_instance_list_viewed() {
|
||||
// There is no proper API to call to trigger this event, so what we are
|
||||
// doing here is simply making sure that the events returns the right information.
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$params = [
|
||||
'context' => context_course::instance($course->id)
|
||||
];
|
||||
$event = course_module_instance_list_viewed::create($params);
|
||||
|
||||
// Triggering and capturing the event.
|
||||
$sink = $this->redirectEvents();
|
||||
$event->trigger();
|
||||
$events = $sink->get_events();
|
||||
$this->assertCount(1, $events);
|
||||
$event = reset($events);
|
||||
|
||||
// Checking that the event contains the expected values.
|
||||
$this->assertInstanceOf('\mod_h5pactivity\event\course_module_instance_list_viewed', $event);
|
||||
$this->assertEquals(context_course::instance($course->id), $event->get_context());
|
||||
$expected = [$course->id, 'h5pactivity', 'view all', 'index.php?id='.$course->id, ''];
|
||||
$this->assertEventLegacyLogData($expected, $event);
|
||||
$this->assertEventContextNotUsed($event);
|
||||
}
|
||||
}
|
||||
+10
-40
@@ -22,6 +22,12 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace mod_h5pactivity\event;
|
||||
|
||||
use advanced_testcase;
|
||||
use context_course;
|
||||
use context_module;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
@@ -31,45 +37,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @copyright 2020 Ferran Recio <ferran@moodle.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class mod_h5pactivity_events_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Setup is called before calling test case.
|
||||
*/
|
||||
public function setUp() {
|
||||
// Must be a non-guest user to create h5pactivities.
|
||||
$this->setAdminUser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test course_module_instance_list_viewed event.
|
||||
*/
|
||||
public function test_course_module_instance_list_viewed() {
|
||||
// There is no proper API to call to trigger this event, so what we are
|
||||
// doing here is simply making sure that the events returns the right information.
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$params = [
|
||||
'context' => context_course::instance($course->id)
|
||||
];
|
||||
$event = \mod_h5pactivity\event\course_module_instance_list_viewed::create($params);
|
||||
|
||||
// Triggering and capturing the event.
|
||||
$sink = $this->redirectEvents();
|
||||
$event->trigger();
|
||||
$events = $sink->get_events();
|
||||
$this->assertCount(1, $events);
|
||||
$event = reset($events);
|
||||
|
||||
// Checking that the event contains the expected values.
|
||||
$this->assertInstanceOf('\mod_h5pactivity\event\course_module_instance_list_viewed', $event);
|
||||
$this->assertEquals(context_course::instance($course->id), $event->get_context());
|
||||
$expected = [$course->id, 'h5pactivity', 'view all', 'index.php?id='.$course->id, ''];
|
||||
$this->assertEventLegacyLogData($expected, $event);
|
||||
$this->assertEventContextNotUsed($event);
|
||||
}
|
||||
class course_module_viewed_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Test course_module_viewed event.
|
||||
@@ -80,6 +48,8 @@ class mod_h5pactivity_events_testcase extends advanced_testcase {
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course->id]);
|
||||
|
||||
@@ -87,7 +57,7 @@ class mod_h5pactivity_events_testcase extends advanced_testcase {
|
||||
'context' => context_module::instance($activity->cmid),
|
||||
'objectid' => $activity->id
|
||||
];
|
||||
$event = \mod_h5pactivity\event\course_module_viewed::create($params);
|
||||
$event = course_module_viewed::create($params);
|
||||
|
||||
// Triggering and capturing the event.
|
||||
$sink = $this->redirectEvents();
|
||||
@@ -0,0 +1,83 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Events test.
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @copyright 2020 Ferran Recio <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace mod_h5pactivity\event;
|
||||
|
||||
use advanced_testcase;
|
||||
use context_course;
|
||||
use context_module;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* H5P activity events test cases.
|
||||
*
|
||||
* @package mod_h5pactivity
|
||||
* @copyright 2020 Ferran Recio <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class statement_received_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Test course_module_viewed event.
|
||||
*/
|
||||
public function test_statement_received() {
|
||||
global $USER;
|
||||
|
||||
$this->resetAfterTest();
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
// Must be a non-guest user to create h5pactivities.
|
||||
$this->setAdminUser();
|
||||
|
||||
// There is no proper API to call to trigger this event, so what we are
|
||||
// doing here is simply making sure that the events returns the right information.
|
||||
|
||||
$course = $this->getDataGenerator()->create_course();
|
||||
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course->id]);
|
||||
|
||||
$params = [
|
||||
'context' => context_module::instance($activity->cmid),
|
||||
'objectid' => $activity->id
|
||||
];
|
||||
$event = statement_received::create($params);
|
||||
|
||||
// Triggering and capturing the event.
|
||||
$sink = $this->redirectEvents();
|
||||
$event->trigger();
|
||||
$events = $sink->get_events();
|
||||
$this->assertCount(1, $events);
|
||||
$event = reset($events);
|
||||
|
||||
// Checking that the event contains the expected values.
|
||||
$this->assertInstanceOf('\mod_h5pactivity\event\statement_received', $event);
|
||||
$this->assertEquals(context_module::instance($activity->cmid), $event->get_context());
|
||||
$this->assertEquals($activity->id, $event->objectid);
|
||||
$expected = [$course->id, 'h5pactivity', 'statement received',
|
||||
'grade.php?user=' . $USER->id, 0, $activity->cmid];
|
||||
$this->assertEventLegacyLogData($expected, $event);
|
||||
$this->assertEventContextNotUsed($event);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,7 @@ class mod_h5pactivity_generator extends testing_module_generator {
|
||||
* @return stdClass record from module-defined table with additional field
|
||||
* cmid (corresponding id in course_modules table)
|
||||
*/
|
||||
public function create_instance($record = null, array $options = null) {
|
||||
public function create_instance($record = null, array $options = null): stdClass {
|
||||
global $CFG, $USER;
|
||||
// Ensure the record can be modified without affecting calling code.
|
||||
$record = (object)(array)$record;
|
||||
@@ -67,7 +67,7 @@ class mod_h5pactivity_generator extends testing_module_generator {
|
||||
// The 'packagefile' value corresponds to the draft file area ID. If not specified, create from packagefilepath.
|
||||
if (empty($record->packagefile)) {
|
||||
if (!isloggedin() || isguestuser()) {
|
||||
throw new coding_exception('Scorm generator requires a current user');
|
||||
throw new coding_exception('H5P activity generator requires a current user');
|
||||
}
|
||||
if (!file_exists($record->packagefilepath)) {
|
||||
throw new coding_exception("File {$record->packagefilepath} does not exist");
|
||||
@@ -88,4 +88,76 @@ class mod_h5pactivity_generator extends testing_module_generator {
|
||||
// Do work to actually add the instance.
|
||||
return parent::create_instance($record, (array)$options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creata a fake attempt
|
||||
* @param stdClass $instance object returned from create_instance() call
|
||||
* @param stdClass|array $record
|
||||
* @return stdClass generated object
|
||||
* @throws coding_exception if function is not implemented by module
|
||||
*/
|
||||
public function create_content($instance, $record = []) {
|
||||
global $DB, $USER;
|
||||
|
||||
$currenttime = time();
|
||||
$cmid = $record['cmid'];
|
||||
$userid = $record['userid'] ?? $USER->id;
|
||||
$conditions = ['h5pactivityid' => $instance->id, 'userid' => $userid];
|
||||
$attemptnum = $DB->count_records('h5pactivity_attempts', $conditions) + 1;
|
||||
$attempt = (object)[
|
||||
'h5pactivityid' => $instance->id,
|
||||
'userid' => $userid,
|
||||
'timecreated' => $currenttime,
|
||||
'timemodified' => $currenttime,
|
||||
'attempt' => $attemptnum,
|
||||
'rawscore' => 3,
|
||||
'maxscore' => 5,
|
||||
];
|
||||
$attempt->id = $DB->insert_record('h5pactivity_attempts', $attempt);
|
||||
|
||||
// Create 3 diferent tracking results.
|
||||
$result = (object)[
|
||||
'attemptid' => $attempt->id,
|
||||
'subcontent' => '',
|
||||
'timecreated' => $currenttime,
|
||||
'interactiontype' => 'compound',
|
||||
'description' => 'description for '.$userid,
|
||||
'correctpattern' => '',
|
||||
'response' => '',
|
||||
'additionals' => '{"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":'.
|
||||
$cmid.'},"contextExtensions":{}}',
|
||||
'rawscore' => 3,
|
||||
'maxscore' => 5,
|
||||
];
|
||||
$DB->insert_record('h5pactivity_attempts_results', $result);
|
||||
|
||||
$result->subcontent = 'bd03477a-90a1-486d-890b-0657d6e80ffd';
|
||||
$result->interactiontype = 'compound';
|
||||
$result->response = '0[,]5[,]2[,]3';
|
||||
$result->additionals = '{"choices":[{"id":"0","description":{"en-US":"Blueberry\n"}},'.
|
||||
'{"id":"1","description":{"en-US":"Raspberry\n"}},{"id":"5","description":'.
|
||||
'{"en-US":"Strawberry\n"}},{"id":"2","description":{"en-US":"Cloudberry\n"}},'.
|
||||
'{"id":"3","description":{"en-US":"Halle Berry\n"}},'.
|
||||
'{"id":"4","description":{"en-US":"Cocktail cherry\n"}}],'.
|
||||
'"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":'.$cmid.
|
||||
',"http:\/\/h5p.org\/x-api\/h5p-subContentId":"'.$result->interactiontype.
|
||||
'"},"contextExtensions":{}}';
|
||||
$result->rawscore = 1;
|
||||
$DB->insert_record('h5pactivity_attempts_results', $result);
|
||||
|
||||
$result->subcontent = '14fcc986-728b-47f3-915b-'.$userid;
|
||||
$result->interactiontype = 'matching';
|
||||
$result->response = '1[.]0[,]0[.]1[,]2[.]2';
|
||||
$result->additionals = '{"source":[{"id":"0","description":{"en-US":"A berry"}}'.
|
||||
',{"id":"1","description":{"en-US":"An orange berry"}},'.
|
||||
'{"id":"2","description":{"en-US":"A red berry"}}],'.
|
||||
'"target":[{"id":"0","description":{"en-US":"Cloudberry"}},'.
|
||||
'{"id":"1","description":{"en-US":"Blueberry"}},'.
|
||||
'{"id":"2","description":{"en-US":"Redcurrant\n"}}],'.
|
||||
'"contextExtensions":{}}';
|
||||
$result->rawscore = 2;
|
||||
$DB->insert_record('h5pactivity_attempts_results', $result);
|
||||
|
||||
return $attempt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
<?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 generator 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 \core_xapi\local\statement;
|
||||
use \core_xapi\local\statement\item;
|
||||
use \core_xapi\local\statement\item_agent;
|
||||
use \core_xapi\local\statement\item_activity;
|
||||
use \core_xapi\local\statement\item_definition;
|
||||
use \core_xapi\local\statement\item_verb;
|
||||
use stdClass;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Attempt 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 attempt_testcase extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Generate a scenario to run all tests.
|
||||
* @return array course_modules, user record, course record
|
||||
*/
|
||||
private function generate_testing_scenario(): array {
|
||||
$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);
|
||||
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||
|
||||
return [$cm, $student, $course];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for create_attempt method.
|
||||
*/
|
||||
public function test_create_attempt() {
|
||||
|
||||
list($cm, $student) = $this->generate_testing_scenario();
|
||||
|
||||
// Create first attempt.
|
||||
$attempt = attempt::new_attempt($student, $cm);
|
||||
$this->assertEquals($student->id, $attempt->get_userid());
|
||||
$this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
|
||||
$this->assertEquals(1, $attempt->get_attempt());
|
||||
|
||||
// Create a second attempt.
|
||||
$attempt = attempt::new_attempt($student, $cm);
|
||||
$this->assertEquals($student->id, $attempt->get_userid());
|
||||
$this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
|
||||
$this->assertEquals(2, $attempt->get_attempt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for last_attempt method
|
||||
*/
|
||||
public function test_last_attempt() {
|
||||
|
||||
list($cm, $student) = $this->generate_testing_scenario();
|
||||
|
||||
// Create first attempt.
|
||||
$attempt = attempt::last_attempt($student, $cm);
|
||||
$this->assertEquals($student->id, $attempt->get_userid());
|
||||
$this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
|
||||
$this->assertEquals(1, $attempt->get_attempt());
|
||||
$lastid = $attempt->get_id();
|
||||
|
||||
// Get last attempt.
|
||||
$attempt = attempt::last_attempt($student, $cm);
|
||||
$this->assertEquals($student->id, $attempt->get_userid());
|
||||
$this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
|
||||
$this->assertEquals(1, $attempt->get_attempt());
|
||||
$this->assertEquals($lastid, $attempt->get_id());
|
||||
|
||||
// Now force a new attempt.
|
||||
$attempt = attempt::new_attempt($student, $cm);
|
||||
$this->assertEquals($student->id, $attempt->get_userid());
|
||||
$this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
|
||||
$this->assertEquals(2, $attempt->get_attempt());
|
||||
$lastid = $attempt->get_id();
|
||||
|
||||
// Get last attempt.
|
||||
$attempt = attempt::last_attempt($student, $cm);
|
||||
$this->assertEquals($student->id, $attempt->get_userid());
|
||||
$this->assertEquals($cm->instance, $attempt->get_h5pactivityid());
|
||||
$this->assertEquals(2, $attempt->get_attempt());
|
||||
$this->assertEquals($lastid, $attempt->get_id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test saving statements.
|
||||
*
|
||||
* @dataProvider save_statement_data
|
||||
* @param string $subcontent subcontent identifier
|
||||
* @param bool $hasdefinition generate definition
|
||||
* @param bool $hasresult generate result
|
||||
* @param array $results 0 => insert ok, 1 => maxscore, 2 => rawscore, 3 => count
|
||||
*/
|
||||
public function test_save_statement(string $subcontent, bool $hasdefinition, bool $hasresult, array $results) {
|
||||
|
||||
list($cm, $student) = $this->generate_testing_scenario();
|
||||
|
||||
$attempt = attempt::new_attempt($student, $cm);
|
||||
$this->assertEquals(0, $attempt->get_maxscore());
|
||||
$this->assertEquals(0, $attempt->get_rawscore());
|
||||
$this->assertEquals(0, $attempt->count_results());
|
||||
|
||||
$statement = $this->generate_statement($hasdefinition, $hasresult);
|
||||
$result = $attempt->save_statement($statement, $subcontent);
|
||||
$this->assertEquals($results[0], $result);
|
||||
$this->assertEquals($results[1], $attempt->get_maxscore());
|
||||
$this->assertEquals($results[2], $attempt->get_rawscore());
|
||||
$this->assertEquals($results[3], $attempt->count_results());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for data request creation tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function save_statement_data(): array {
|
||||
return [
|
||||
'Statement without definition and result' => [
|
||||
'', false, false, [false, 0, 0, 0]
|
||||
],
|
||||
'Statement with definition but no result' => [
|
||||
'', true, false, [false, 0, 0, 0]
|
||||
],
|
||||
'Statement with result but no definition' => [
|
||||
'', true, false, [false, 0, 0, 0]
|
||||
],
|
||||
'Statement subcontent without definition and result' => [
|
||||
'111-222-333', false, false, [false, 0, 0, 0]
|
||||
],
|
||||
'Statement subcontent with definition but no result' => [
|
||||
'111-222-333', true, false, [false, 0, 0, 0]
|
||||
],
|
||||
'Statement subcontent with result but no definition' => [
|
||||
'111-222-333', true, false, [false, 0, 0, 0]
|
||||
],
|
||||
'Statement with definition, result but no subcontent' => [
|
||||
'', true, true, [true, 2, 2, 1]
|
||||
],
|
||||
'Statement with definition, result and subcontent' => [
|
||||
'111-222-333', true, true, [true, 0, 0, 1]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test delete results from attempt.
|
||||
*/
|
||||
public function test_delete_results() {
|
||||
|
||||
list($cm, $student) = $this->generate_testing_scenario();
|
||||
|
||||
$attempt = $this->generate_full_attempt($student, $cm);
|
||||
$attempt->delete_results();
|
||||
$this->assertEquals(0, $attempt->count_results());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test delete attempt.
|
||||
*/
|
||||
public function test_delete_attempt() {
|
||||
global $DB;
|
||||
|
||||
list($cm, $student) = $this->generate_testing_scenario();
|
||||
|
||||
// Check no previous attempts are created.
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$this->assertEquals(0, $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals(0, $count);
|
||||
|
||||
// Generate one attempt.
|
||||
$attempt1 = $this->generate_full_attempt($student, $cm);
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$this->assertEquals(1, $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals(2, $count);
|
||||
|
||||
// Generate a second attempt.
|
||||
$attempt2 = $this->generate_full_attempt($student, $cm);
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$this->assertEquals(2, $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals(4, $count);
|
||||
|
||||
// Delete the first attempt.
|
||||
attempt::delete_attempt($attempt1);
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$this->assertEquals(1, $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals(2, $count);
|
||||
$this->assertEquals(2, $attempt2->count_results());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test delete all attempts.
|
||||
*
|
||||
* @dataProvider delete_all_attempts_data
|
||||
* @param bool $hasstudent if user is specificed
|
||||
* @param int[] 0-3 => statements count results, 4-5 => totals
|
||||
*/
|
||||
public function test_delete_all_attempts(bool $hasstudent, array $results) {
|
||||
global $DB;
|
||||
|
||||
list($cm, $student, $course) = $this->generate_testing_scenario();
|
||||
|
||||
// For this test we need extra activity and student.
|
||||
$activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]);
|
||||
$cm2 = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST);
|
||||
$student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||
|
||||
// Check no previous attempts are created.
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$this->assertEquals(0, $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals(0, $count);
|
||||
|
||||
// Generate some attempts attempt on both activities and students.
|
||||
$attempts = [];
|
||||
$attempts[] = $this->generate_full_attempt($student, $cm);
|
||||
$attempts[] = $this->generate_full_attempt($student2, $cm);
|
||||
$attempts[] = $this->generate_full_attempt($student, $cm2);
|
||||
$attempts[] = $this->generate_full_attempt($student2, $cm2);
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$this->assertEquals(4, $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals(8, $count);
|
||||
|
||||
// Delete all specified attempts.
|
||||
$user = ($hasstudent) ? $student : null;
|
||||
attempt::delete_all_attempts($cm, $user);
|
||||
|
||||
// Check data.
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
$count = $attempts[$i]->count_results();
|
||||
$this->assertEquals($results[$i], $count);
|
||||
}
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$this->assertEquals($results[4], $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals($results[5], $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for data request creation tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function delete_all_attempts_data(): array {
|
||||
return [
|
||||
'Delete all attempts from activity' => [
|
||||
false, [0, 0, 2, 2, 2, 4]
|
||||
],
|
||||
'Delete all attempts from user' => [
|
||||
true, [0, 2, 2, 2, 3, 6]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a fake attempt with two results.
|
||||
*
|
||||
* @param stdClass $student a user record
|
||||
* @param stdClass $cm a course_module record
|
||||
* @return attempt
|
||||
*/
|
||||
private function generate_full_attempt($student, $cm): attempt {
|
||||
$attempt = attempt::new_attempt($student, $cm);
|
||||
$this->assertEquals(0, $attempt->get_maxscore());
|
||||
$this->assertEquals(0, $attempt->get_rawscore());
|
||||
$this->assertEquals(0, $attempt->count_results());
|
||||
|
||||
$statement = $this->generate_statement(true, true);
|
||||
$saveok = $attempt->save_statement($statement, '');
|
||||
$this->assertTrue($saveok);
|
||||
$saveok = $attempt->save_statement($statement, '111-222-333');
|
||||
$this->assertTrue($saveok);
|
||||
$this->assertEquals(2, $attempt->count_results());
|
||||
|
||||
return $attempt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a xAPI partial statement with object defined.
|
||||
* @param bool $hasdefinition if has to include definition
|
||||
* @param bool $hasresult if has to include results
|
||||
* @return statement
|
||||
*/
|
||||
private function generate_statement(bool $hasdefinition, bool $hasresult): statement {
|
||||
global $USER;
|
||||
|
||||
$statement = new statement();
|
||||
$statement->set_actor(item_agent::create_from_user($USER));
|
||||
$statement->set_verb(item_verb::create_from_id('http://adlnet.gov/expapi/verbs/completed'));
|
||||
$definition = null;
|
||||
if ($hasdefinition) {
|
||||
$definition = item_definition::create_from_data((object)[
|
||||
'interactionType' => 'compound',
|
||||
'correctResponsesPattern' => '1',
|
||||
]);
|
||||
}
|
||||
$statement->set_object(item_activity::create_from_id('something', $definition));
|
||||
if ($hasresult) {
|
||||
$statement->set_result(item::create_from_data((object)[
|
||||
'completion' => true,
|
||||
'success' => true,
|
||||
'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1],
|
||||
]));
|
||||
}
|
||||
return $statement;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
<?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 generator 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\xapi;
|
||||
|
||||
use \core_xapi\local\statement;
|
||||
use \core_xapi\local\statement\item;
|
||||
use \core_xapi\local\statement\item_agent;
|
||||
use \core_xapi\local\statement\item_activity;
|
||||
use \core_xapi\local\statement\item_definition;
|
||||
use \core_xapi\local\statement\item_verb;
|
||||
use context_module;
|
||||
use stdClass;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Attempt 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 handler_testcase extends \advanced_testcase {
|
||||
|
||||
/**
|
||||
* Generate a valid scenario for each tests.
|
||||
*
|
||||
* @return stdClass an object with all scenario data in it
|
||||
*/
|
||||
private function generate_testing_scenario(): stdClass {
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
$data = new stdClass();
|
||||
|
||||
$data->course = $this->getDataGenerator()->create_course();
|
||||
|
||||
// Generate 2 users, one enroled into course and one not.
|
||||
$data->student = $this->getDataGenerator()->create_and_enrol($data->course, 'student');
|
||||
$data->otheruser = $this->getDataGenerator()->create_user();
|
||||
|
||||
// H5P activity.
|
||||
$data->activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $data->course]);
|
||||
$data->context = context_module::instance($data->activity->cmid);
|
||||
|
||||
$data->xapihandler = handler::create('mod_h5pactivity');
|
||||
$this->assertNotEmpty($data->xapihandler);
|
||||
$this->assertInstanceOf('\mod_h5pactivity\xapi\handler', $data->xapihandler);
|
||||
|
||||
$this->setUser($data->student);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for xapi_handler with valid statements.
|
||||
*/
|
||||
public function test_xapi_handler() {
|
||||
global $DB;
|
||||
|
||||
$data = $this->generate_testing_scenario();
|
||||
$xapihandler = $data->xapihandler;
|
||||
$context = $data->context;
|
||||
$student = $data->student;
|
||||
$otheruser = $data->otheruser;
|
||||
|
||||
// Check we have 0 entries in the attempts tables.
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$this->assertEquals(0, $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals(0, $count);
|
||||
|
||||
$statements = $this->generate_statements($context, $student);
|
||||
|
||||
// Insert first statement.
|
||||
$event = $xapihandler->statement_to_event($statements[0]);
|
||||
$this->assertNotNull($event);
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$this->assertEquals(1, $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals(1, $count);
|
||||
|
||||
// Insert second statement.
|
||||
$event = $xapihandler->statement_to_event($statements[1]);
|
||||
$this->assertNotNull($event);
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$this->assertEquals(1, $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals(2, $count);
|
||||
|
||||
// Insert again first statement.
|
||||
$event = $xapihandler->statement_to_event($statements[0]);
|
||||
$this->assertNotNull($event);
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$this->assertEquals(2, $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals(3, $count);
|
||||
|
||||
// Insert again second statement.
|
||||
$event = $xapihandler->statement_to_event($statements[1]);
|
||||
$this->assertNotNull($event);
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$this->assertEquals(2, $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals(4, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Testing wrong statements scenarios.
|
||||
*
|
||||
* @dataProvider xapi_handler_errors_data
|
||||
* @param bool $hasverb valid verb
|
||||
* @param bool $hasdefinition generate definition
|
||||
* @param bool $hasresult generate result
|
||||
* @param bool $hascontext valid context
|
||||
* @param bool $hasuser valid user
|
||||
* @param bool $generateattempt if generates an empty attempt
|
||||
*/
|
||||
public function test_xapi_handler_errors(bool $hasverb, bool $hasdefinition, bool $hasresult,
|
||||
bool $hascontext, bool $hasuser, bool $generateattempt) {
|
||||
global $DB, $CFG;
|
||||
|
||||
$data = $this->generate_testing_scenario();
|
||||
$xapihandler = $data->xapihandler;
|
||||
$context = $data->context;
|
||||
$student = $data->student;
|
||||
$otheruser = $data->otheruser;
|
||||
|
||||
// Check we have 0 entries in the attempts tables.
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$this->assertEquals(0, $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals(0, $count);
|
||||
|
||||
$statement = new statement();
|
||||
if ($hasverb) {
|
||||
$statement->set_verb(item_verb::create_from_id('http://adlnet.gov/expapi/verbs/completed'));
|
||||
} else {
|
||||
$statement->set_verb(item_verb::create_from_id('cook'));
|
||||
}
|
||||
$definition = null;
|
||||
if ($hasdefinition) {
|
||||
$definition = item_definition::create_from_data((object)[
|
||||
'interactionType' => 'compound',
|
||||
'correctResponsesPattern' => '1',
|
||||
]);
|
||||
}
|
||||
if ($hascontext) {
|
||||
$statement->set_object(item_activity::create_from_id($context->id, $definition));
|
||||
} else {
|
||||
$statement->set_object(item_activity::create_from_id('paella', $definition));
|
||||
}
|
||||
if ($hasresult) {
|
||||
$statement->set_result(item::create_from_data((object)[
|
||||
'completion' => true,
|
||||
'success' => true,
|
||||
'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1],
|
||||
]));
|
||||
}
|
||||
if ($hasuser) {
|
||||
$statement->set_actor(item_agent::create_from_user($student));
|
||||
} else {
|
||||
$statement->set_actor(item_agent::create_from_user($otheruser));
|
||||
}
|
||||
|
||||
$event = $xapihandler->statement_to_event($statement);
|
||||
$this->assertNull($event);
|
||||
// No enties should be generated.
|
||||
$count = $DB->count_records('h5pactivity_attempts');
|
||||
$attempts = ($generateattempt) ? 1 : 0;
|
||||
$this->assertEquals($attempts, $count);
|
||||
$count = $DB->count_records('h5pactivity_attempts_results');
|
||||
$this->assertEquals(0, $count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for data request creation tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function xapi_handler_errors_data(): array {
|
||||
return [
|
||||
// Invalid Definitions and results possibilities.
|
||||
'Invalid definition and result' => [
|
||||
true, false, false, true, true, false
|
||||
],
|
||||
'Invalid result' => [
|
||||
true, true, false, true, true, false
|
||||
],
|
||||
'Invalid definition (generate empty attempt)' => [
|
||||
true, false, true, true, true, true
|
||||
],
|
||||
// Invalid verb possibilities.
|
||||
'Invalid verb, definition and result' => [
|
||||
false, false, false, true, true, false
|
||||
],
|
||||
'Invalid verb and result' => [
|
||||
false, true, false, true, true, false
|
||||
],
|
||||
'Invalid verb and result' => [
|
||||
false, false, true, true, true, false
|
||||
],
|
||||
// Invalid context possibilities.
|
||||
'Invalid definition, result and context' => [
|
||||
true, false, false, false, true, false
|
||||
],
|
||||
'Invalid result' => [
|
||||
true, true, false, false, true, false
|
||||
],
|
||||
'Invalid result and context' => [
|
||||
true, false, true, false, true, false
|
||||
],
|
||||
'Invalid verb, definition result and context' => [
|
||||
false, false, false, false, true, false
|
||||
],
|
||||
'Invalid verb, result and context' => [
|
||||
false, true, false, false, true, false
|
||||
],
|
||||
'Invalid verb, result and context' => [
|
||||
false, false, true, false, true, false
|
||||
],
|
||||
// Invalid user possibilities.
|
||||
'Invalid definition, result and user' => [
|
||||
true, false, false, true, false, false
|
||||
],
|
||||
'Invalid result and user' => [
|
||||
true, true, false, true, false, false
|
||||
],
|
||||
'Invalid definition and user' => [
|
||||
true, false, true, true, false, false
|
||||
],
|
||||
'Invalid verb, definition, result and user' => [
|
||||
false, false, false, true, false, false
|
||||
],
|
||||
'Invalid verb, result and user' => [
|
||||
false, true, false, true, false, false
|
||||
],
|
||||
'Invalid verb, result and user' => [
|
||||
false, false, true, true, false, false
|
||||
],
|
||||
'Invalid definition, result, context and user' => [
|
||||
true, false, false, false, false, false
|
||||
],
|
||||
'Invalid result, context and user' => [
|
||||
true, true, false, false, false, false
|
||||
],
|
||||
'Invalid definition, context and user' => [
|
||||
true, false, true, false, false, false
|
||||
],
|
||||
'Invalid verb, definition, result, context and user' => [
|
||||
false, false, false, false, false, false
|
||||
],
|
||||
'Invalid verb, result, context and user' => [
|
||||
false, true, false, false, false, false
|
||||
],
|
||||
'Invalid verb, result, context and user' => [
|
||||
false, false, true, false, false, false
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a basic xAPI statements simulating a H5P content.
|
||||
*
|
||||
* @param context_module $context activity context
|
||||
* @param stdClass $user user record
|
||||
* @return statement[] array of xAPI statements
|
||||
*/
|
||||
private function generate_statements(context_module $context, stdClass $user): array {
|
||||
$statements = [];
|
||||
|
||||
$statement = new statement();
|
||||
$statement->set_actor(item_agent::create_from_user($user));
|
||||
$statement->set_verb(item_verb::create_from_id('http://adlnet.gov/expapi/verbs/completed'));
|
||||
$definition = item_definition::create_from_data((object)[
|
||||
'interactionType' => 'compound',
|
||||
'correctResponsesPattern' => '1',
|
||||
]);
|
||||
$statement->set_object(item_activity::create_from_id($context->id, $definition));
|
||||
$statement->set_result(item::create_from_data((object)[
|
||||
'completion' => true,
|
||||
'success' => true,
|
||||
'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1],
|
||||
]));
|
||||
$statements[] = $statement;
|
||||
|
||||
$statement = new statement();
|
||||
$statement->set_actor(item_agent::create_from_user($user));
|
||||
$statement->set_verb(item_verb::create_from_id('http://adlnet.gov/expapi/verbs/completed'));
|
||||
$definition = item_definition::create_from_data((object)[
|
||||
'interactionType' => 'matching',
|
||||
'correctResponsesPattern' => '1',
|
||||
]);
|
||||
$statement->set_object(item_activity::create_from_id($context->id.'?subContentId=111-222-333', $definition));
|
||||
$statement->set_result(item::create_from_data((object)[
|
||||
'completion' => true,
|
||||
'success' => true,
|
||||
'score' => (object) ['min' => 0, 'max' => 1, 'raw' => 0, 'scaled' => 0],
|
||||
]));
|
||||
$statements[] = $statement;
|
||||
|
||||
return $statements;
|
||||
}
|
||||
}
|
||||
@@ -25,5 +25,5 @@
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'mod_h5pactivity';
|
||||
$plugin->version = 2020022501;
|
||||
$plugin->version = 2020032300;
|
||||
$plugin->requires = 2020013000;
|
||||
|
||||
@@ -62,13 +62,25 @@ $fileurl = moodle_url::make_pluginfile_url($file->get_contextid(), $file->get_co
|
||||
$file->get_filename(), false);
|
||||
|
||||
$PAGE->set_url('/mod/h5pactivity/view.php', ['id' => $cm->id]);
|
||||
$PAGE->set_title(format_string($moduleinstance->name));
|
||||
|
||||
$shortname = format_string($course->shortname, true, ['context' => $context]);
|
||||
$pagetitle = strip_tags($shortname.': '.format_string($moduleinstance->name));
|
||||
$PAGE->set_title(format_string($pagetitle));
|
||||
|
||||
$PAGE->set_heading(format_string($course->fullname));
|
||||
$PAGE->set_context($context);
|
||||
|
||||
echo $OUTPUT->header();
|
||||
echo $OUTPUT->heading(format_string($moduleinstance->name));
|
||||
|
||||
// TODO: add component to enable xAPI traking.
|
||||
echo \core_h5p\player::display($fileurl, $config, true);
|
||||
if (has_capability('mod/h5pactivity:submit', $context, null, false)) {
|
||||
$trackcomponent = 'mod_h5pactivity';
|
||||
} else {
|
||||
$trackcomponent = '';
|
||||
$message = get_string('previewmode', 'mod_h5pactivity');
|
||||
echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING);
|
||||
}
|
||||
|
||||
echo \core_h5p\player::display($fileurl, $config, true, $trackcomponent);
|
||||
|
||||
echo $OUTPUT->footer();
|
||||
|
||||
Reference in New Issue
Block a user