first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,37 @@
<?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 list viewed event.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\event;
defined('MOODLE_INTERNAL') || die();
/**
* The course_module_instance_list_viewed event class.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed {
}
@@ -0,0 +1,59 @@
<?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 viewed.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\event;
defined('MOODLE_INTERNAL') || die();
/**
* The course_module_viewed event class.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class course_module_viewed extends \core\event\course_module_viewed {
/**
* Init method.
*
* @return void
*/
protected function init(): void {
$this->data['objecttable'] = 'h5pactivity';
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* 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,131 @@
<?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 report viewed.
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\event;
defined('MOODLE_INTERNAL') || die();
/**
* The report_viewed event class.
*
* @property-read array $other {
* Extra information about the event.
*
* - int instanceid: The instance ID
* - int userid: The optional user ID
* - int attemptid: The optional attempt ID
*
* @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 report_viewed extends \core\event\base {
/**
* Init method.
*
* @return void
*/
protected function init(): void {
$this->data['objecttable'] = 'h5pactivity';
$this->data['crud'] = 'r';
$this->data['edulevel'] = self::LEVEL_PARTICIPATING;
}
/**
* Returns localised general event name.
*
* @return string
*/
public static function get_name() {
return get_string('report_viewed', 'mod_h5pactivity');
}
/**
* Custom validation.
*
* @throws \coding_exception
* @return void
*/
protected function validate_data() {
parent::validate_data();
if (empty($this->other['instanceid'])) {
throw new \coding_exception('The \'instanceid\' value must be set in other.');
}
}
/**
* Returns non-localised description of what happened.
*
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' viewed the H5P report for the H5P with " .
"course module id '$this->contextinstanceid'.";
}
/**
* Get URL related to the action
*
* @return \moodle_url
*/
public function get_url() {
$params = ['a' => $this->other['instanceid']];
if (!empty($this->other['userid'])) {
$params['userid'] = $this->other['userid'];
}
if (!empty($this->other['attemptid'])) {
$params['attemptid'] = $this->other['attemptid'];
}
return new \moodle_url('/mod/h5pactivity/report.php', $params);
}
/**
* 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'];
}
/**
* Return the other field mapping.
*
* @return array
*/
public static function get_other_mapping() {
$othermapped = array();
$othermapped['attemptid'] = array('db' => 'h5pactivity_attempts', 'restore' => 'h5pactivity_attempts');
$othermapped['userid'] = array('db' => 'user', 'restore' => 'user');
return $othermapped;
}
}
@@ -0,0 +1,87 @@
<?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 <ferran@moodle.com>
* @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 <ferran@moodle.com>
* @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');
}
/**
* 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'];
}
}
+232
View File
@@ -0,0 +1,232 @@
<?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/>.
namespace mod_h5pactivity\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
use mod_h5pactivity\local\manager;
use mod_h5pactivity\local\attempt;
use mod_h5pactivity\local\report\attempts as report_attempts;
use context_module;
use stdClass;
/**
* This is the external method for getting the information needed to present an attempts report.
*
* @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 get_attempts extends external_api {
/**
* Webservice parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'h5pactivityid' => new external_value(PARAM_INT, 'h5p activity instance id'),
'userids' => new external_multiple_structure(
new external_value(PARAM_INT, 'The user ids to get attempts (null means only current user)', VALUE_DEFAULT),
'User ids', VALUE_DEFAULT, []
),
]
);
}
/**
* Return user attempts information in a h5p activity.
*
* @throws moodle_exception if the user cannot see the report
* @param int $h5pactivityid The h5p activity id
* @param int[]|null $userids The user ids (if no provided $USER will be used)
* @return stdClass report data
*/
public static function execute(int $h5pactivityid, ?array $userids = []): stdClass {
global $USER;
$params = external_api::validate_parameters(self::execute_parameters(), [
'h5pactivityid' => $h5pactivityid,
'userids' => $userids,
]);
$h5pactivityid = $params['h5pactivityid'];
$userids = $params['userids'];
if (empty($userids)) {
$userids = [$USER->id];
}
$warnings = [];
// Request and permission validation.
list ($course, $cm) = get_course_and_cm_from_instance($h5pactivityid, 'h5pactivity');
$context = context_module::instance($cm->id);
self::validate_context($context);
$manager = manager::create_from_coursemodule($cm);
$instance = $manager->get_instance();
$usersattempts = [];
foreach ($userids as $userid) {
$report = $manager->get_report($userid);
if ($report && $report instanceof report_attempts) {
$usersattempts[] = self::export_user_attempts($report, $userid);
} else {
$warnings[] = [
'item' => 'user',
'itemid' => $userid,
'warningcode' => '1',
'message' => "Cannot access user attempts",
];
}
}
$result = (object)[
'activityid' => $instance->id,
'usersattempts' => $usersattempts,
'warnings' => $warnings,
];
return $result;
}
/**
* Export attempts data for a specific user.
*
* @param report_attempts $report the report attempts object
* @param int $userid the user id
* @return stdClass
*/
public static function export_user_attempts(report_attempts $report, int $userid): stdClass {
$scored = $report->get_scored();
$attempts = $report->get_attempts();
$result = (object)[
'userid' => $userid,
'attempts' => [],
];
foreach ($attempts as $attempt) {
$result->attempts[] = self::export_attempt($attempt);
}
if (!empty($scored)) {
$result->scored = (object)[
'title' => $scored->title,
'grademethod' => $scored->grademethod,
'attempts' => [self::export_attempt($scored->attempt)],
];
}
return $result;
}
/**
* Return a data object from an attempt.
*
* @param attempt $attempt the attempt object
* @return stdClass a WS compatible version of the attempt
*/
private static function export_attempt(attempt $attempt): stdClass {
$result = (object)[
'id' => $attempt->get_id(),
'h5pactivityid' => $attempt->get_h5pactivityid(),
'userid' => $attempt->get_userid(),
'timecreated' => $attempt->get_timecreated(),
'timemodified' => $attempt->get_timemodified(),
'attempt' => $attempt->get_attempt(),
'rawscore' => $attempt->get_rawscore(),
'maxscore' => $attempt->get_maxscore(),
'duration' => $attempt->get_duration(),
'scaled' => $attempt->get_scaled(),
];
if ($attempt->get_completion() !== null) {
$result->completion = $attempt->get_completion();
}
if ($attempt->get_success() !== null) {
$result->success = $attempt->get_success();
}
return $result;
}
/**
* Describes the get_h5pactivity_access_information return value.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'activityid' => new external_value(PARAM_INT, 'Activity course module ID'),
'usersattempts' => new external_multiple_structure(
self::get_user_attempts_returns(), 'The complete users attempts list'
),
'warnings' => new external_warnings(),
], 'Activity attempts data');
}
/**
* Describes the get_h5pactivity_access_information return value.
*
* @return external_single_structure
*/
public static function get_user_attempts_returns(): external_single_structure {
$structure = [
'userid' => new external_value(PARAM_INT, 'The user id'),
'attempts' => new external_multiple_structure(self::get_attempt_returns(), 'The complete attempts list'),
'scored' => new external_single_structure([
'title' => new external_value(PARAM_NOTAGS, 'Scored attempts title'),
'grademethod' => new external_value(PARAM_NOTAGS, 'Scored attempts title'),
'attempts' => new external_multiple_structure(self::get_attempt_returns(), 'List of the grading attempts'),
], 'Attempts used to grade the activity', VALUE_OPTIONAL),
];
return new external_single_structure($structure);
}
/**
* Return the external structure of an attempt.
*
* @return external_single_structure
*/
private static function get_attempt_returns(): external_single_structure {
$result = new external_single_structure([
'id' => new external_value(PARAM_INT, 'ID of the context'),
'h5pactivityid' => new external_value(PARAM_INT, 'ID of the H5P activity'),
'userid' => new external_value(PARAM_INT, 'ID of the user'),
'timecreated' => new external_value(PARAM_INT, 'Attempt creation'),
'timemodified' => new external_value(PARAM_INT, 'Attempt modified'),
'attempt' => new external_value(PARAM_INT, 'Attempt number'),
'rawscore' => new external_value(PARAM_INT, 'Attempt score value'),
'maxscore' => new external_value(PARAM_INT, 'Attempt max score'),
'duration' => new external_value(PARAM_INT, 'Attempt duration in seconds'),
'completion' => new external_value(PARAM_INT, 'Attempt completion', VALUE_OPTIONAL),
'success' => new external_value(PARAM_INT, 'Attempt success', VALUE_OPTIONAL),
'scaled' => new external_value(PARAM_FLOAT, 'Attempt scaled'),
]);
return $result;
}
}
@@ -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/>.
namespace mod_h5pactivity\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
use context_module;
use core_h5p\factory;
/**
* This is the external method for returning a list of h5p activities.
*
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_h5pactivities_by_courses extends external_api {
/**
* Parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters (
[
'courseids' => new external_multiple_structure(
new external_value(PARAM_INT, 'Course id'), 'Array of course ids', VALUE_DEFAULT, []
),
]
);
}
/**
* Returns a list of h5p activities in a provided list of courses.
* If no list is provided all h5p activities that the user can view will be returned.
*
* @param array $courseids course ids
* @return array of h5p activities and warnings
* @since Moodle 3.9
*/
public static function execute(array $courseids): array {
global $PAGE;
$warnings = [];
$returnedh5pactivities = [];
$params = external_api::validate_parameters(self::execute_parameters(), [
'courseids' => $courseids
]);
$mycourses = [];
if (empty($params['courseids'])) {
$mycourses = enrol_get_my_courses();
$params['courseids'] = array_keys($mycourses);
}
// Ensure there are courseids to loop through.
if (!empty($params['courseids'])) {
$factory = new factory();
list($courses, $warnings) = \core_external\util::validate_courses($params['courseids'], $mycourses);
$output = $PAGE->get_renderer('core');
// Get the h5p activities in this course, this function checks users visibility permissions.
// We can avoid then additional validate_context calls.
$h5pactivities = get_all_instances_in_courses('h5pactivity', $courses);
foreach ($h5pactivities as $h5pactivity) {
$context = context_module::instance($h5pactivity->coursemodule);
// Remove fields that are not from the h5p activity (added by get_all_instances_in_courses).
unset($h5pactivity->coursemodule, $h5pactivity->context,
$h5pactivity->visible, $h5pactivity->section,
$h5pactivity->groupmode, $h5pactivity->groupingid);
$exporter = new h5pactivity_summary_exporter($h5pactivity,
['context' => $context, 'factory' => $factory]);
$summary = $exporter->export($output);
$returnedh5pactivities[] = $summary;
}
}
$h5pglobalsettings = [
'enablesavestate' => get_config('mod_h5pactivity', 'enablesavestate'),
];
if (!empty($h5pglobalsettings['enablesavestate'])) {
$h5pglobalsettings['savestatefreq'] = get_config('mod_h5pactivity', 'savestatefreq');
}
$result = [
'h5pactivities' => $returnedh5pactivities,
'h5pglobalsettings' => $h5pglobalsettings,
'warnings' => $warnings
];
return $result;
}
/**
* Describes the get_h5pactivities_by_courses return value.
*
* @return external_single_structure
* @since Moodle 3.9
*/
public static function execute_returns() {
return new external_single_structure(
[
'h5pactivities' => new external_multiple_structure(
h5pactivity_summary_exporter::get_read_structure()
),
'h5pglobalsettings' => new external_single_structure(
[
'enablesavestate' => new external_value(PARAM_BOOL, 'Whether saving state is enabled.'),
'savestatefreq' => new external_value(PARAM_INT, 'How often (in seconds) state is saved.', VALUE_OPTIONAL),
],
'H5P global settings',
VALUE_OPTIONAL,
),
'warnings' => new external_warnings(),
]
);
}
}
@@ -0,0 +1,109 @@
<?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/>.
namespace mod_h5pactivity\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
use context_module;
use mod_h5pactivity\local\manager;
/**
* This is the external method for getting access information for a h5p activity.
*
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_h5pactivity_access_information extends external_api {
/**
* Parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'h5pactivityid' => new external_value(PARAM_INT, 'h5p activity instance id')
]
);
}
/**
* Return access information for a given h5p activity.
*
* @param int $h5pactivityid The h5p activity id.
* @return array of warnings and the access information
* @since Moodle 3.9
* @throws moodle_exception
*/
public static function execute(int $h5pactivityid): array {
global $DB;
$params = external_api::validate_parameters(self::execute_parameters(), [
'h5pactivityid' => $h5pactivityid
]);
// Request and permission validation.
$h5pactivity = $DB->get_record('h5pactivity', ['id' => $params['h5pactivityid']], '*', MUST_EXIST);
list($course, $cm) = get_course_and_cm_from_instance($h5pactivity, 'h5pactivity');
$context = context_module::instance($cm->id);
self::validate_context($context);
$result = [];
// Return all the available capabilities.
$manager = manager::create_from_coursemodule($cm);
$capabilities = load_capability_def('mod_h5pactivity');
foreach ($capabilities as $capname => $capdata) {
$field = 'can' . str_replace('mod/h5pactivity:', '', $capname);
// For mod/h5pactivity:submit we need to check if tracking is enabled in the h5pactivity for the current user.
if ($field == 'cansubmit') {
$result[$field] = $manager->is_tracking_enabled() && $manager->can_submit();
} else {
$result[$field] = has_capability($capname, $context);
}
}
$result['warnings'] = [];
return $result;
}
/**
* Describes the get_h5pactivity_access_information return value.
*
* @return external_single_structure
* @since Moodle 3.9
*/
public static function execute_returns() {
$structure = [
'warnings' => new external_warnings()
];
$capabilities = load_capability_def('mod_h5pactivity');
foreach ($capabilities as $capname => $capdata) {
$field = 'can' . str_replace('mod/h5pactivity:', '', $capname);
$structure[$field] = new external_value(PARAM_BOOL, 'Whether the user has the capability ' . $capname . ' allowed.',
VALUE_OPTIONAL);
}
return new external_single_structure($structure);
}
}
+293
View File
@@ -0,0 +1,293 @@
<?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/>.
namespace mod_h5pactivity\external;
use mod_h5pactivity\local\manager;
use mod_h5pactivity\local\report\results as report_results;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
use context_module;
use stdClass;
/**
* This is the external method for getting the information needed to present a results report.
*
* @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 get_results extends external_api {
/**
* Webservice parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'h5pactivityid' => new external_value(PARAM_INT, 'h5p activity instance id'),
'attemptids' => new external_multiple_structure(
new external_value(PARAM_INT, 'The attempt id'),
'Attempt ids', VALUE_DEFAULT, []
),
]
);
}
/**
* Return user attempts results information in a h5p activity.
*
* In case an empty array of attempt ids is passed, the method will load all
* activity attempts from the current user.
*
* @throws moodle_exception if the user cannot see the report
* @param int $h5pactivityid The h5p activity id
* @param int[] $attemptids The attempt ids
* @return stdClass report data
*/
public static function execute(int $h5pactivityid, array $attemptids = []): stdClass {
global $USER;
$params = external_api::validate_parameters(self::execute_parameters(), [
'h5pactivityid' => $h5pactivityid,
'attemptids' => $attemptids,
]);
$h5pactivityid = $params['h5pactivityid'];
$attemptids = $params['attemptids'];
$warnings = [];
// Request and permission validation.
list ($course, $cm) = get_course_and_cm_from_instance($h5pactivityid, 'h5pactivity');
$context = context_module::instance($cm->id);
self::validate_context($context);
$manager = manager::create_from_coursemodule($cm);
if (empty($attemptids)) {
$attemptids = [];
foreach ($manager->get_user_attempts($USER->id) as $attempt) {
$attemptids[] = $attempt->get_id();
}
}
$attempts = [];
foreach ($attemptids as $attemptid) {
$report = $manager->get_report(null, $attemptid);
if ($report && $report instanceof report_results) {
$attempts[] = self::export_attempt($report);
} else {
$warnings[] = [
'item' => 'h5pactivity_attempts',
'itemid' => $attemptid,
'warningcode' => '1',
'message' => "Cannot access attempt",
];
}
}
$result = (object)[
'activityid' => $h5pactivityid,
'attempts' => $attempts,
'warnings' => $warnings,
];
return $result;
}
/**
* Return a data object from an attempt.
*
* @param report_results $report the attempt data
* @return stdClass a WS compatible version of the attempt
*/
private static function export_attempt(report_results $report): stdClass {
$data = $report->export_data_for_external();
$attemptdata = $data->attempt;
$attempt = (object)[
'id' => $attemptdata->id,
'h5pactivityid' => $attemptdata->h5pactivityid,
'userid' => $attemptdata->userid,
'timecreated' => $attemptdata->timecreated,
'timemodified' => $attemptdata->timemodified,
'attempt' => $attemptdata->attempt,
'rawscore' => $attemptdata->rawscore,
'maxscore' => $attemptdata->maxscore,
'duration' => (empty($attemptdata->durationvalue)) ? 0 : $attemptdata->durationvalue,
'scaled' => (empty($attemptdata->scaled)) ? 0 : $attemptdata->scaled,
'results' => [],
];
if (isset($attemptdata->completion) && $attemptdata->completion !== null) {
$attempt->completion = $attemptdata->completion;
}
if (isset($attemptdata->success) && $attemptdata->success !== null) {
$attempt->success = $attemptdata->success;
}
foreach ($data->results as $result) {
$attempt->results[] = self::export_result($result);
}
return $attempt;
}
/**
* Return a data object from a result.
*
* @param stdClass $data the result data
* @return stdClass a WS compatible version of the result
*/
private static function export_result(stdClass $data): stdClass {
$result = (object)[
'id' => $data->id,
'attemptid' => $data->attemptid,
'subcontent' => $data->subcontent,
'timecreated' => $data->timecreated,
'interactiontype' => $data->interactiontype,
'description' => $data->description,
'rawscore' => $data->rawscore,
'maxscore' => $data->maxscore,
'duration' => $data->duration,
'optionslabel' => $data->optionslabel ?? get_string('choice', 'mod_h5pactivity'),
'correctlabel' => $data->correctlabel ?? get_string('correct_answer', 'mod_h5pactivity'),
'answerlabel' => $data->answerlabel ?? get_string('attempt_answer', 'mod_h5pactivity'),
'track' => $data->track ?? false,
];
if (isset($data->completion) && $data->completion !== null) {
$result->completion = $data->completion;
}
if (isset($data->success) && $data->success !== null) {
$result->success = $data->success;
}
if (isset($data->options)) {
$result->options = $data->options;
}
if (isset($data->content)) {
$result->content = $data->content;
}
return $result;
}
/**
* Describes the get_h5pactivity_access_information return value.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'activityid' => new external_value(PARAM_INT, 'Activity course module ID'),
'attempts' => new external_multiple_structure(
self::get_attempt_returns(), 'The complete attempts list'
),
'warnings' => new external_warnings(),
], 'Activity attempts results data');
}
/**
* Return the external structure of an attempt
* @return external_single_structure
*/
private static function get_attempt_returns(): external_single_structure {
$result = new external_single_structure([
'id' => new external_value(PARAM_INT, 'ID of the context'),
'h5pactivityid' => new external_value(PARAM_INT, 'ID of the H5P activity'),
'userid' => new external_value(PARAM_INT, 'ID of the user'),
'timecreated' => new external_value(PARAM_INT, 'Attempt creation'),
'timemodified' => new external_value(PARAM_INT, 'Attempt modified'),
'attempt' => new external_value(PARAM_INT, 'Attempt number'),
'rawscore' => new external_value(PARAM_INT, 'Attempt score value'),
'maxscore' => new external_value(PARAM_INT, 'Attempt max score'),
'duration' => new external_value(PARAM_INT, 'Attempt duration in seconds'),
'completion' => new external_value(PARAM_INT, 'Attempt completion', VALUE_OPTIONAL),
'success' => new external_value(PARAM_INT, 'Attempt success', VALUE_OPTIONAL),
'scaled' => new external_value(PARAM_FLOAT, 'Attempt scaled'),
'results' => new external_multiple_structure(
self::get_result_returns(),
'The results of the attempt', VALUE_OPTIONAL
),
], 'The attempt general information');
return $result;
}
/**
* Return the external structure of a result
* @return external_single_structure
*/
private static function get_result_returns(): external_single_structure {
$result = new external_single_structure([
'id' => new external_value(PARAM_INT, 'ID of the context'),
'attemptid' => new external_value(PARAM_INT, 'ID of the H5P attempt'),
'subcontent' => new external_value(PARAM_NOTAGS, 'Subcontent identifier'),
'timecreated' => new external_value(PARAM_INT, 'Result creation'),
'interactiontype' => new external_value(PARAM_NOTAGS, 'Interaction type'),
'description' => new external_value(PARAM_RAW, 'Result description'),
'content' => new external_value(PARAM_RAW, 'Result extra content', VALUE_OPTIONAL),
'rawscore' => new external_value(PARAM_INT, 'Result score value'),
'maxscore' => new external_value(PARAM_INT, 'Result max score'),
'duration' => new external_value(PARAM_INT, 'Result duration in seconds', VALUE_OPTIONAL, 0),
'completion' => new external_value(PARAM_INT, 'Result completion', VALUE_OPTIONAL),
'success' => new external_value(PARAM_INT, 'Result success', VALUE_OPTIONAL),
'optionslabel' => new external_value(PARAM_NOTAGS, 'Label used for result options', VALUE_OPTIONAL),
'correctlabel' => new external_value(PARAM_NOTAGS, 'Label used for correct answers', VALUE_OPTIONAL),
'answerlabel' => new external_value(PARAM_NOTAGS, 'Label used for user answers', VALUE_OPTIONAL),
'track' => new external_value(PARAM_BOOL, 'If the result has valid track information', VALUE_OPTIONAL),
'options' => new external_multiple_structure(
new external_single_structure([
'description' => new external_value(PARAM_RAW, 'Option description', VALUE_OPTIONAL),
'id' => new external_value(PARAM_TEXT, 'Option string identifier', VALUE_OPTIONAL),
'correctanswer' => self::get_answer_returns('The option correct answer', VALUE_OPTIONAL),
'useranswer' => self::get_answer_returns('The option user answer', VALUE_OPTIONAL),
]),
'The statement options', VALUE_OPTIONAL
),
], 'A single result statement tracking information');
return $result;
}
/**
* Return the external structure of an answer or correctanswer
*
* @param string $description the return description
* @param int $required the return required value
* @return external_single_structure
*/
private static function get_answer_returns(string $description, int $required = VALUE_REQUIRED): external_single_structure {
$result = new external_single_structure([
'answer' => new external_value(PARAM_NOTAGS, 'Option text value', VALUE_OPTIONAL),
'correct' => new external_value(PARAM_BOOL, 'If has to be displayed as correct', VALUE_OPTIONAL),
'incorrect' => new external_value(PARAM_BOOL, 'If has to be displayed as incorrect', VALUE_OPTIONAL),
'text' => new external_value(PARAM_BOOL, 'If has to be displayed as simple text', VALUE_OPTIONAL),
'checked' => new external_value(PARAM_BOOL, 'If has to be displayed as a checked option', VALUE_OPTIONAL),
'unchecked' => new external_value(PARAM_BOOL, 'If has to be displayed as a unchecked option', VALUE_OPTIONAL),
'pass' => new external_value(PARAM_BOOL, 'If has to be displayed as passed', VALUE_OPTIONAL),
'fail' => new external_value(PARAM_BOOL, 'If has to be displayed as failed', VALUE_OPTIONAL),
], $description, $required);
return $result;
}
}
+302
View File
@@ -0,0 +1,302 @@
<?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/>.
namespace mod_h5pactivity\external;
use mod_h5pactivity\local\manager;
use mod_h5pactivity\local\attempt;
use mod_h5pactivity\local\report;
use mod_h5pactivity\local\report\attempts as report_attempts;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_multiple_structure;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
use moodle_exception;
use context_module;
use stdClass;
/**
* This is the external method to return the information needed to list all enrolled user attempts.
*
* @package mod_h5pactivity
* @since Moodle 3.11
* @copyright 2020 Ilya Tregubov <ilya@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class get_user_attempts extends external_api {
/**
* Webservice parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'h5pactivityid' => new external_value(PARAM_INT, 'h5p activity instance id'),
'sortorder' => new external_value(PARAM_TEXT,
'sort by either user id, firstname or lastname (with optional asc/desc)', VALUE_DEFAULT, 'id ASC'),
'page' => new external_value(PARAM_INT, 'current page', VALUE_DEFAULT, -1),
'perpage' => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0),
'firstinitial' => new external_value(PARAM_TEXT, 'Users whose first name ' .
'starts with $firstinitial', VALUE_DEFAULT, ''),
'lastinitial' => new external_value(PARAM_TEXT, 'Users whose last name ' .
'starts with $lastinitial', VALUE_DEFAULT, ''),
]
);
}
/**
* Return user attempts information in a h5p activity.
*
* @throws moodle_exception if the user cannot see the report
* @param int $h5pactivityid The h5p activity id
* @param int $sortorder The sort order
* @param int $page page number
* @param int $perpage items per page
* @param int $firstinitial Users whose first name starts with $firstinitial
* @param int $lastinitial Users whose last name starts with $lastinitial
* @return stdClass report data
*/
public static function execute(int $h5pactivityid, $sortorder = 'id ASC', ?int $page = 0,
?int $perpage = 0, $firstinitial = '', $lastinitial = ''): stdClass {
[
'h5pactivityid' => $h5pactivityid,
'sortorder' => $sortorder,
'page' => $page,
'perpage' => $perpage,
'firstinitial' => $firstinitial,
'lastinitial' => $lastinitial,
] = external_api::validate_parameters(self::execute_parameters(), [
'h5pactivityid' => $h5pactivityid,
'sortorder' => $sortorder,
'page' => $page,
'perpage' => $perpage,
'firstinitial' => $firstinitial,
'lastinitial' => $lastinitial,
]);
$warnings = [];
[$course, $cm] = get_course_and_cm_from_instance($h5pactivityid, 'h5pactivity');
$context = context_module::instance($cm->id);
self::validate_context($context);
$manager = manager::create_from_coursemodule($cm);
$instance = $manager->get_instance();
if (!$manager->can_view_all_attempts()) {
throw new moodle_exception('nopermissiontoviewattempts', 'error', '', null,
'h5pactivity:reviewattempts required view attempts of all enrolled users.');
}
// Ensure sortorder parameter is safe to use. Fallback to default value of the parameter itself.
$sortorderparts = explode(' ', $sortorder, 2);
$sortorder = get_safe_orderby([
'id' => 'u.id',
'firstname' => 'u.firstname',
'lastname' => 'u.lastname',
'default' => 'u.id',
], $sortorderparts[0], $sortorderparts[1] ?? '');
$users = self::get_active_users($manager, 'u.id, u.firstname, u.lastname',
$sortorder, $page * $perpage, $perpage);
$usersattempts = [];
foreach ($users as $user) {
if ($firstinitial) {
if (strpos($user->firstname, $firstinitial) === false) {
continue;
}
}
if ($lastinitial) {
if (strpos($user->lastname, $lastinitial) === false) {
continue;
}
}
$report = $manager->get_report($user->id);
if ($report && $report instanceof report_attempts) {
$usersattempts[] = self::export_user_attempts($report, $user->id);
} else {
$warnings[] = [
'item' => 'user',
'itemid' => $user->id,
'warningcode' => '1',
'message' => "Cannot access user attempts",
];
}
}
$result = (object)[
'activityid' => $instance->id,
'usersattempts' => $usersattempts,
'warnings' => $warnings,
];
return $result;
}
/**
* Generate the active users list
*
* @param manager $manager the h5pactivity manager
* @param string $userfields the user fields to get
* @param string $sortorder the SQL sortorder
* @param int $limitfrom SQL limit from
* @param int $limitnum SQL limit num
*/
private static function get_active_users(
manager $manager,
string $userfields = 'u.*',
string $sortorder = '',
int $limitfrom = 0,
int $limitnum = 0
): array {
global $DB;
$capjoin = $manager->get_active_users_join(true);
// Final SQL.
$sql = "SELECT DISTINCT {$userfields}
FROM {user} u {$capjoin->joins}
WHERE {$capjoin->wheres}
{$sortorder}";
return $DB->get_records_sql($sql, $capjoin->params, $limitfrom, $limitnum);
}
/**
* Export attempts data for a specific user.
*
* @param report $report the report attempts object
* @param int $userid the user id
* @return stdClass
*/
private static function export_user_attempts(report $report, int $userid): stdClass {
$scored = $report->get_scored();
$attempts = $report->get_attempts();
$result = (object)[
'userid' => $userid,
'attempts' => [],
];
foreach ($attempts as $attempt) {
$result->attempts[] = self::export_attempt($attempt);
}
if (!empty($scored)) {
$result->scored = (object)[
'title' => $scored->title,
'grademethod' => $scored->grademethod,
'attempts' => [self::export_attempt($scored->attempt)],
];
}
return $result;
}
/**
* Return a data object from an attempt.
*
* @param attempt $attempt the attempt object
* @return stdClass a WS compatible version of the attempt
*/
private static function export_attempt(attempt $attempt): stdClass {
$result = (object)[
'id' => $attempt->get_id(),
'h5pactivityid' => $attempt->get_h5pactivityid(),
'userid' => $attempt->get_userid(),
'timecreated' => $attempt->get_timecreated(),
'timemodified' => $attempt->get_timemodified(),
'attempt' => $attempt->get_attempt(),
'rawscore' => $attempt->get_rawscore(),
'maxscore' => $attempt->get_maxscore(),
'duration' => $attempt->get_duration(),
'scaled' => $attempt->get_scaled(),
];
if ($attempt->get_completion() !== null) {
$result->completion = $attempt->get_completion();
}
if ($attempt->get_success() !== null) {
$result->success = $attempt->get_success();
}
return $result;
}
/**
* Describes the get_h5pactivity_access_information return value.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'activityid' => new external_value(PARAM_INT, 'Activity course module ID'),
'usersattempts' => new external_multiple_structure(
self::get_user_attempts_returns(), 'The complete users attempts list'
),
'warnings' => new external_warnings(),
], 'Activity attempts data');
}
/**
* Describes the get_h5pactivity_access_information return value.
*
* @return external_single_structure
*/
private static function get_user_attempts_returns(): external_single_structure {
$structure = [
'userid' => new external_value(PARAM_INT, 'The user id'),
'attempts' => new external_multiple_structure(self::get_user_attempt_returns(), 'The complete attempts list'),
'scored' => new external_single_structure([
'title' => new external_value(PARAM_NOTAGS, 'Scored attempts title'),
'grademethod' => new external_value(PARAM_NOTAGS, 'Grading method'),
'attempts' => new external_multiple_structure(self::get_user_attempt_returns(), 'List of the grading attempts'),
], 'Attempts used to grade the activity', VALUE_OPTIONAL),
];
return new external_single_structure($structure);
}
/**
* Return the external structure of an attempt.
*
* @return external_single_structure
*/
private static function get_user_attempt_returns(): external_single_structure {
$result = new external_single_structure([
'id' => new external_value(PARAM_INT, 'ID of the context'),
'h5pactivityid' => new external_value(PARAM_INT, 'ID of the H5P activity'),
'userid' => new external_value(PARAM_INT, 'ID of the user'),
'timecreated' => new external_value(PARAM_INT, 'Attempt creation'),
'timemodified' => new external_value(PARAM_INT, 'Attempt modified'),
'attempt' => new external_value(PARAM_INT, 'Attempt number'),
'rawscore' => new external_value(PARAM_INT, 'Attempt score value'),
'maxscore' => new external_value(PARAM_INT, 'Attempt max score'),
'duration' => new external_value(PARAM_INT, 'Attempt duration in seconds'),
'completion' => new external_value(PARAM_INT, 'Attempt completion', VALUE_OPTIONAL),
'success' => new external_value(PARAM_INT, 'Attempt success', VALUE_OPTIONAL),
'scaled' => new external_value(PARAM_FLOAT, 'Attempt scaled'),
]);
return $result;
}
}
@@ -0,0 +1,244 @@
<?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/>.
/**
* Class for exporting h5p activity data.
*
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\external;
use core\external\exporter;
use renderer_base;
use core_external\util as external_util;
use core_external\external_files;
use core_h5p\api;
/**
* Class for exporting h5p activity data.
*
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class h5pactivity_summary_exporter extends exporter {
/**
* Properties definition.
*
* @return array
*/
protected static function define_properties() {
return [
'id' => [
'type' => PARAM_INT,
'description' => 'The primary key of the record.',
],
'course' => [
'type' => PARAM_INT,
'description' => 'Course id this h5p activity is part of.',
],
'name' => [
'type' => PARAM_TEXT,
'description' => 'The name of the activity module instance.',
],
'timecreated' => [
'type' => PARAM_INT,
'description' => 'Timestamp of when the instance was added to the course.',
'optional' => true,
],
'timemodified' => [
'type' => PARAM_INT,
'description' => 'Timestamp of when the instance was last modified.',
'optional' => true,
],
'intro' => [
'default' => '',
'type' => PARAM_RAW,
'description' => 'H5P activity description.',
'null' => NULL_ALLOWED,
],
'introformat' => [
'choices' => [FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN],
'type' => PARAM_INT,
'default' => FORMAT_MOODLE,
'description' => 'The format of the intro field.',
],
'grade' => [
'type' => PARAM_INT,
'default' => 0,
'description' => 'The maximum grade for submission.',
'optional' => true,
],
'displayoptions' => [
'type' => PARAM_INT,
'default' => 0,
'description' => 'H5P Button display options.',
],
'enabletracking' => [
'type' => PARAM_INT,
'default' => 1,
'description' => 'Enable xAPI tracking.',
],
'grademethod' => [
'type' => PARAM_INT,
'default' => 1,
'description' => 'Which H5P attempt is used for grading.',
],
'contenthash' => [
'type' => PARAM_ALPHANUM,
'description' => 'Sha1 hash of file content.',
'optional' => true,
],
];
}
/**
* Related objects definition.
*
* @return array
*/
protected static function define_related() {
return [
'context' => 'context',
'factory' => 'core_h5p\\factory'
];
}
/**
* Other properties definition.
*
* @return array
*/
protected static function define_other_properties() {
return [
'coursemodule' => [
'type' => PARAM_INT
],
'context' => [
'type' => PARAM_INT
],
'introfiles' => [
'type' => external_files::get_properties_for_exporter(),
'multiple' => true
],
'package' => [
'type' => external_files::get_properties_for_exporter(),
'multiple' => true
],
'deployedfile' => [
'optional' => true,
'description' => 'H5P file deployed.',
'type' => [
'filename' => array(
'type' => PARAM_FILE,
'description' => 'File name.',
'optional' => true,
'null' => NULL_NOT_ALLOWED,
),
'filepath' => array(
'type' => PARAM_PATH,
'description' => 'File path.',
'optional' => true,
'null' => NULL_NOT_ALLOWED,
),
'filesize' => array(
'type' => PARAM_INT,
'description' => 'File size.',
'optional' => true,
'null' => NULL_NOT_ALLOWED,
),
'fileurl' => array(
'type' => PARAM_URL,
'description' => 'Downloadable file url.',
'optional' => true,
'null' => NULL_NOT_ALLOWED,
),
'timemodified' => array(
'type' => PARAM_INT,
'description' => 'Time modified.',
'optional' => true,
'null' => NULL_NOT_ALLOWED,
),
'mimetype' => array(
'type' => PARAM_RAW,
'description' => 'File mime type.',
'optional' => true,
'null' => NULL_NOT_ALLOWED,
)
]
],
];
}
/**
* Assign values to the defined other properties.
*
* @param renderer_base $output The output renderer object.
* @return array
*/
protected function get_other_values(renderer_base $output) {
$context = $this->related['context'];
$factory = $this->related['factory'];
$values = [
'coursemodule' => $context->instanceid,
'context' => $context->id,
];
$values['introfiles'] = external_util::get_area_files($context->id, 'mod_h5pactivity', 'intro', false, false);
$values['package'] = external_util::get_area_files($context->id, 'mod_h5pactivity', 'package', false, true);
// Only if this H5P activity has been deployed, return the exported file.
$fileh5p = api::get_export_info_from_context_id($context->id, $factory, 'mod_h5pactivity', 'package');
if ($fileh5p) {
$values['deployedfile'] = $fileh5p;
}
return $values;
}
/**
* Get the formatting parameters for the intro.
*
* @return array with the formatting parameters
*/
protected function get_format_parameters_for_intro() {
return [
'component' => 'mod_h5pactivity',
'filearea' => 'intro',
'options' => ['noclean' => true],
];
}
/**
* Get the formatting parameters for the package.
*
* @return array with the formatting parameters
*/
protected function get_format_parameters_for_package() {
return [
'component' => 'mod_h5pactivity',
'filearea' => 'package',
'itemid' => 0,
'options' => ['noclean' => true],
];
}
}
+122
View File
@@ -0,0 +1,122 @@
<?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/>.
namespace mod_h5pactivity\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
use mod_h5pactivity\local\manager;
use mod_h5pactivity\event\report_viewed;
use context_module;
use stdClass;
/**
* This is the external method for logging that the h5pactivity was viewed.
*
* @package mod_h5pactivity
* @copyright 2021 Ilya Tregubov <ilya@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.11
*/
class log_report_viewed extends external_api {
/**
* Webservice parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'h5pactivityid' => new external_value(PARAM_INT, 'h5p activity instance id'),
'userid' => new external_value(
PARAM_INT,
'The user id to log attempt (null means only current user)',
VALUE_DEFAULT
),
'attemptid' => new external_value(PARAM_INT, 'The attempt id', VALUE_DEFAULT),
]
);
}
/**
* Logs that the h5pactivity was viewed.
*
* @throws moodle_exception if the user cannot see the report
* @param int $h5pactivityid The h5p activity id
* @param int|null $userid The user id
* @param int|null $attemptid The attempt id
* @return array of warnings and status result
*/
public static function execute(int $h5pactivityid, int $userid = null, int $attemptid = null): stdClass {
$params = external_api::validate_parameters(self::execute_parameters(), [
'h5pactivityid' => $h5pactivityid,
'userid' => $userid,
'attemptid' => $attemptid,
]);
$h5pactivityid = $params['h5pactivityid'];
$userid = $params['userid'];
$attemptid = $params['attemptid'];
// Request and permission validation.
list ($course, $cm) = get_course_and_cm_from_instance($h5pactivityid, 'h5pactivity');
$context = context_module::instance($cm->id);
self::validate_context($context);
$manager = manager::create_from_coursemodule($cm);
$instance = $manager->get_instance();
// Trigger event.
$other = [
'instanceid' => $instance->id,
'userid' => $userid,
'attemptid' => $attemptid,
];
$event = report_viewed::create([
'objectid' => $instance->id,
'context' => $context,
'other' => $other,
]);
$event->trigger();
$result = (object)[
'status' => true,
'warnings' => [],
];
return $result;
}
/**
* Describes the report_viewed return value.
*
* @return external_single_structure
* @since Moodle 3.11
*/
public static function execute_returns() {
return new external_single_structure(
[
'status' => new external_value(PARAM_BOOL, 'status: true if success'),
'warnings' => new external_warnings()
]
);
}
}
+94
View File
@@ -0,0 +1,94 @@
<?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/>.
namespace mod_h5pactivity\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use core_external\external_warnings;
use context_module;
use mod_h5pactivity\local\manager;
/**
* This is the external method for triggering the course module viewed event.
*
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class view_h5pactivity extends external_api {
/**
* Parameters.
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters(
[
'h5pactivityid' => new external_value(PARAM_INT, 'H5P activity instance id')
]
);
}
/**
* Trigger the course module viewed event and update the module completion status.
*
* @param int $h5pactivityid The h5p activity id.
* @return array of warnings and the access information
* @since Moodle 3.9
* @throws moodle_exception
*/
public static function execute(int $h5pactivityid): array {
$params = external_api::validate_parameters(self::execute_parameters(), [
'h5pactivityid' => $h5pactivityid
]);
$warnings = [];
// Request and permission validation.
list($course, $cm) = get_course_and_cm_from_instance($params['h5pactivityid'], 'h5pactivity');
$context = context_module::instance($cm->id);
self::validate_context($context);
$manager = manager::create_from_coursemodule($cm);
$manager->set_module_viewed($course);
$result = array(
'status' => true,
'warnings' => $warnings,
);
return $result;
}
/**
* Describes the view_h5pactivity return value.
*
* @return external_single_structure
* @since Moodle 3.9
*/
public static function execute_returns() {
return new external_single_structure(
[
'status' => new external_value(PARAM_BOOL, 'status: true if success'),
'warnings' => new external_warnings()
]
);
}
}
+515
View File
@@ -0,0 +1,515 @@
<?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 <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\local;
use core_xapi\handler;
use stdClass;
use core_xapi\local\statement;
/**
* Class attempt 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 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
*/
public function __construct(stdClass $record) {
$this->record = $record;
}
/**
* 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;
$record->duration = 0;
$record->completion = null;
$record->success = null;
// 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;
}
// Remove any xAPI State associated to this attempt.
$context = \context_module::instance($cm->id);
$xapihandler = handler::create('mod_h5pactivity');
$xapihandler->wipe_states($context->id, $user->id);
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();
$duration = $xapiresult->get_duration();
// 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;
}
$record->duration = $duration;
if (isset($result->completion)) {
$record->completion = ($result->completion) ? 1 : 0;
}
if (isset($result->success)) {
$record->success = ($result->success) ? 1 : 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)) {
$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();
}
/**
* Update the current attempt record into DB.
*
* @return bool true if update is succesful
*/
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.
*/
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);
}
/**
* Return all results stored in this attempt.
*
* @return stdClass[] results records.
*/
public function get_results(): array {
global $DB;
$conditions = ['attemptid' => $this->record->id];
return $DB->get_records('h5pactivity_attempts_results', $conditions, 'id ASC');
}
/**
* 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 timecreated.
*
* @return int the attempt timecreated
*/
public function get_timecreated(): int {
return $this->record->timecreated;
}
/**
* Return the attempt H5P timemodified.
*
* @return int the attempt timemodified
*/
public function get_timemodified(): int {
return $this->record->timemodified;
}
/**
* 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->rawscore;
}
/**
* Return the attempt duration.
*
* @return int|null the duration value
*/
public function get_duration(): ?int {
return $this->record->duration;
}
/**
* Return the attempt completion.
*
* @return int|null the completion value
*/
public function get_completion(): ?int {
return $this->record->completion;
}
/**
* Return the attempt success.
*
* @return int|null the success value
*/
public function get_success(): ?int {
return $this->record->success;
}
/**
* Return the attempt scaled.
*
* @return int|null the scaled value
*/
public function get_scaled(): ?int {
return is_null($this->record->scaled) ? $this->record->scaled : (int)$this->record->scaled;
}
/**
* Return if the attempt has been modified.
*
* Note: adding a result only add track information unless the statement does
* not specify subcontent. In this case this will update also the statement.
*
* @return bool if the attempt score have been modified
*/
public function get_scoreupdated(): bool {
return $this->scoreupdated;
}
}
+214
View File
@@ -0,0 +1,214 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* H5P activity grader class.
*
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <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;
}
}
+581
View File
@@ -0,0 +1,581 @@
<?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 mod_h5pactivity\local\report\participants;
use mod_h5pactivity\local\report\attempts;
use mod_h5pactivity\local\report\results;
use context_module;
use cm_info;
use moodle_recordset;
use core_user;
use stdClass;
use core\dml\sql_join;
use mod_h5pactivity\event\course_module_viewed;
/**
* 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;
/** Participants cannot review their own attempts. */
const REVIEWNONE = 0;
/** Participants can review their own attempts when have one attempt completed. */
const REVIEWCOMPLETION = 1;
/** @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'),
];
}
/**
* Return the selected attempt criteria.
* @return string[] an array "grademethod value", "attempt description"
*/
public function get_selected_attempt(): array {
$types = [
self::GRADEHIGHESTATTEMPT => get_string('attempt_highest', 'mod_h5pactivity'),
self::GRADEAVERAGEATTEMPT => get_string('attempt_average', 'mod_h5pactivity'),
self::GRADELASTATTEMPT => get_string('attempt_last', 'mod_h5pactivity'),
self::GRADEFIRSTATTEMPT => get_string('attempt_first', 'mod_h5pactivity'),
self::GRADEMANUAL => get_string('attempt_none', 'mod_h5pactivity'),
];
if ($this->instance->enabletracking) {
$key = $this->instance->grademethod;
} else {
$key = self::GRADEMANUAL;
}
return [$key, $types[$key]];
}
/**
* Return the available review modes.
*
* @return string[] an array "option value" => "option description"
*/
public static function get_review_modes(): array {
return [
self::REVIEWCOMPLETION => get_string('review_on_completion', 'mod_h5pactivity'),
self::REVIEWNONE => get_string('review_none', 'mod_h5pactivity'),
];
}
/**
* Check if tracking is enabled in a particular h5pactivity for a specific user.
*
* @return bool if tracking is enabled in this activity
*/
public function is_tracking_enabled(): bool {
return $this->instance->enabletracking;
}
/**
* Check if the user has permission to submit a particular h5pactivity for a specific user.
*
* @param stdClass|null $user user record (default $USER)
* @return bool if the user has permission to submit in this activity
*/
public function can_submit(stdClass $user = null): bool {
global $USER;
if (empty($user)) {
$user = $USER;
}
return has_capability('mod/h5pactivity:submit', $this->context, $user, false);
}
/**
* Check if a user can see the activity attempts list.
*
* @param stdClass|null $user user record (default $USER)
* @return bool if the user can see the attempts link
*/
public function can_view_all_attempts(stdClass $user = null): bool {
global $USER;
if (!$this->instance->enabletracking) {
return false;
}
if (empty($user)) {
$user = $USER;
}
return has_capability('mod/h5pactivity:reviewattempts', $this->context, $user);
}
/**
* Check if a user can see own attempts.
*
* @param stdClass|null $user user record (default $USER)
* @return bool if the user can see the own attempts link
*/
public function can_view_own_attempts(stdClass $user = null): bool {
global $USER;
if (!$this->instance->enabletracking) {
return false;
}
if (empty($user)) {
$user = $USER;
}
if (has_capability('mod/h5pactivity:reviewattempts', $this->context, $user, false)) {
return true;
}
if ($this->instance->reviewmode == self::REVIEWNONE) {
return false;
}
if ($this->instance->reviewmode == self::REVIEWCOMPLETION) {
return true;
}
return 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);
}
/**
* Count the activity completed attempts.
*
* If no user is provided the method will count all active users attempts.
* Check get_active_users_join PHPdoc to a more detailed description of "active users".
*
* @param int|null $userid optional user id (default null)
* @return int the total amount of attempts
*/
public function count_attempts(int $userid = null): int {
global $DB;
// Counting records is enough for one user.
if ($userid) {
$params['userid'] = $userid;
$params = [
'h5pactivityid' => $this->instance->id,
'userid' => $userid,
'completion' => 1,
];
return $DB->count_records('h5pactivity_attempts', $params);
}
$usersjoin = $this->get_active_users_join();
// Final SQL.
return $DB->count_records_sql(
"SELECT COUNT(*)
FROM {user} u $usersjoin->joins
WHERE $usersjoin->wheres",
array_merge($usersjoin->params)
);
}
/**
* Return the join to collect all activity active users.
*
* The concept of active user is relative to the activity permissions. All users with
* "mod/h5pactivity:view" are potential users but those with "mod/h5pactivity:reviewattempts"
* are evaluators and they don't count as valid submitters.
*
* Note that, in general, the active list has the same effect as checking for "mod/h5pactivity:submit"
* but submit capability cannot be used because is a write capability and does not apply to frozen contexts.
*
* @since Moodle 3.11
* @param bool $allpotentialusers if true, the join will return all active users, not only the ones with attempts.
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
* @return sql_join the active users attempts join
*/
public function get_active_users_join(bool $allpotentialusers = false, $currentgroup = false): sql_join {
// Only valid users counts. By default, all users with submit capability are considered potential ones.
$context = $this->get_context();
$coursemodule = $this->get_coursemodule();
// Ensure user can view users from all groups.
if ($currentgroup === 0 && $coursemodule->effectivegroupmode == SEPARATEGROUPS
&& !has_capability('moodle/site:accessallgroups', $context)) {
return new sql_join('', '1=2', [], true);
}
// We want to present all potential users.
$capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view', $currentgroup);
if ($capjoin->cannotmatchanyrows) {
return $capjoin;
}
// But excluding all reviewattempts users converting a capabilities join into left join.
$reviewersjoin = get_with_capability_join($context, 'mod/h5pactivity:reviewattempts', 'u.id');
if ($reviewersjoin->cannotmatchanyrows) {
return $capjoin;
}
$capjoin = new sql_join(
$capjoin->joins . "\n LEFT " . str_replace('ra', 'reviewer', $reviewersjoin->joins),
$capjoin->wheres . " AND reviewer.userid IS NULL",
$capjoin->params
);
if ($allpotentialusers) {
return $capjoin;
}
// Add attempts join.
$where = "ha.h5pactivityid = :h5pactivityid AND ha.completion = :completion";
$params = [
'h5pactivityid' => $this->instance->id,
'completion' => 1,
];
return new sql_join(
$capjoin->joins . "\n JOIN {h5pactivity_attempts} ha ON ha.userid = u.id",
$capjoin->wheres . " AND $where",
array_merge($capjoin->params, $params)
);
}
/**
* Return an array of all users and it's total attempts.
*
* Note: this funciton only returns the list of users with attempts,
* it does not check all participants.
*
* @return array indexed count userid => total number of attempts
*/
public function count_users_attempts(): array {
global $DB;
$params = [
'h5pactivityid' => $this->instance->id,
];
$sql = "SELECT userid, count(*)
FROM {h5pactivity_attempts}
WHERE h5pactivityid = :h5pactivityid
GROUP BY userid";
return $DB->get_records_sql_menu($sql, $params);
}
/**
* Return the current context.
*
* @return context_module
*/
public function get_context(): context_module {
return $this->context;
}
/**
* Return the current instance.
*
* @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);
}
/**
* Return the suitable report to show the attempts.
*
* This method controls the access to the different reports
* the activity have.
*
* @param int $userid an opional userid to show
* @param int $attemptid an optional $attemptid to show
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
* @return report|null available report (or null if no report available)
*/
public function get_report(int $userid = null, int $attemptid = null, $currentgroup = false): ?report {
global $USER, $CFG;
require_once("{$CFG->dirroot}/user/lib.php");
// If tracking is disabled, no reports are available.
if (!$this->instance->enabletracking) {
return null;
}
$attempt = null;
if ($attemptid) {
$attempt = $this->get_attempt($attemptid);
if (!$attempt) {
return null;
}
// If we have and attempt we can ignore the provided $userid.
$userid = $attempt->get_userid();
}
if ($this->can_view_all_attempts()) {
$user = core_user::get_user($userid);
// Ensure user can view the attempt of specific userid, respecting access checks.
if ($user && $user->id != $USER->id) {
$course = get_course($this->coursemodule->course);
if (!groups_user_groups_visible($course, $user->id, $this->coursemodule)) {
return null;
}
}
} else if ($this->can_view_own_attempts()) {
$user = core_user::get_user($USER->id);
if ($userid && $user->id != $userid) {
return null;
}
} else {
return null;
}
// Only enrolled users has reports.
if ($user && !is_enrolled($this->context, $user, 'mod/h5pactivity:view')) {
return null;
}
// Create the proper report.
if ($user && $attempt) {
return new results($this, $user, $attempt);
} else if ($user) {
return new attempts($this, $user);
}
return new participants($this, $currentgroup);
}
/**
* Return a single attempt.
*
* @param int $attemptid the attempt id
* @return attempt
*/
public function get_attempt(int $attemptid): ?attempt {
global $DB;
$record = $DB->get_record('h5pactivity_attempts', [
'id' => $attemptid,
'h5pactivityid' => $this->instance->id,
]);
if (!$record) {
return null;
}
return new attempt($record);
}
/**
* Return an array of all user attempts (including incompleted)
*
* @param int $userid the user id
* @return attempt[]
*/
public function get_user_attempts(int $userid): array {
global $DB;
$records = $DB->get_records(
'h5pactivity_attempts',
['userid' => $userid, 'h5pactivityid' => $this->instance->id],
'id ASC'
);
if (!$records) {
return [];
}
$result = [];
foreach ($records as $record) {
$result[] = new attempt($record);
}
return $result;
}
/**
* Trigger module viewed event and set the module viewed for completion.
*
* @param stdClass $course course object
* @return void
*/
public function set_module_viewed(stdClass $course): void {
global $CFG;
require_once($CFG->libdir . '/completionlib.php');
// Trigger module viewed event.
$event = course_module_viewed::create([
'objectid' => $this->instance->id,
'context' => $this->context
]);
$event->add_record_snapshot('course', $course);
$event->add_record_snapshot('course_modules', $this->coursemodule);
$event->add_record_snapshot('h5pactivity', $this->instance);
$event->trigger();
// Completion.
$completion = new \completion_info($course);
$completion->set_module_viewed($this->coursemodule);
}
}
+58
View File
@@ -0,0 +1,58 @@
<?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 report interface
*
* @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 templatable;
use stdClass;
/**
* Interface for any mod_h5pactivity report.
*
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <ferran@moodle.com>
*/
interface report {
/**
* Return the report user record.
*
* @return stdClass|null a user or null
*/
public function get_user(): ?stdClass;
/**
* Return the report attempt object.
*
* @return attempt|null the attempt object or null
*/
public function get_attempt(): ?attempt;
/**
* Print the report visualization.
*/
public function print(): void;
}
@@ -0,0 +1,139 @@
<?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 attempts report
*
* @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\report;
use mod_h5pactivity\local\report;
use mod_h5pactivity\local\manager;
use mod_h5pactivity\local\attempt;
use mod_h5pactivity\output\reportattempts;
use stdClass;
/**
* Class H5P activity attempts report.
*
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <ferran@moodle.com>
*/
class attempts implements report {
/** @var manager the H5P activity manager instance. */
private $manager;
/** @var stdClass the user record. */
private $user;
/**
* Create a new participants report.
*
* @param manager $manager h5pactivity manager object
* @param stdClass $user user record
*/
public function __construct(manager $manager, stdClass $user) {
$this->manager = $manager;
$this->user = $user;
}
/**
* Return the report user record.
*
* @return stdClass|null a user or null
*/
public function get_user(): ?stdClass {
return $this->user;
}
/**
* Return the report attempt object.
*
* Attempts report has no specific attempt.
*
* @return attempt|null the attempt object or null
*/
public function get_attempt(): ?attempt {
return null;
}
/**
* Print the report.
*/
public function print(): void {
global $OUTPUT;
$manager = $this->manager;
$cm = $manager->get_coursemodule();
$scored = $this->get_scored();
$title = $scored->title ?? null;
$scoredattempt = $scored->attempt ?? null;
$attempts = $this->get_attempts();
$widget = new reportattempts($attempts, $this->user, $cm->course, $title, $scoredattempt);
echo $OUTPUT->render($widget);
}
/**
* Return the current report attempts.
*
* This method is used to render the report in both browser and mobile.
*
* @return attempts[]
*/
public function get_attempts(): array {
return $this->manager->get_user_attempts($this->user->id);
}
/**
* Return the current report attempts.
*
* This method is used to render the report in both browser and mobile.
*
* @return stdClass|null a structure with
* - title => name of the selected attempt (or null)
* - attempt => the selected attempt object (or null)
* - gradingmethos => the activity grading method (or null)
*/
public function get_scored(): ?stdClass {
$manager = $this->manager;
$scores = $manager->get_users_scaled_score($this->user->id);
$score = $scores[$this->user->id] ?? null;
if (empty($score->attemptid)) {
return null;
}
list($grademethod, $title) = $manager->get_selected_attempt();
$scoredattempt = $manager->get_attempt($score->attemptid);
$result = (object)[
'title' => $title,
'attempt' => $scoredattempt,
'grademethod' => $grademethod,
];
return $result;
}
}
@@ -0,0 +1,218 @@
<?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 participants report
*
* @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\report;
use mod_h5pactivity\local\report;
use mod_h5pactivity\local\manager;
use mod_h5pactivity\local\attempt;
use core\dml\sql_join;
use table_sql;
use moodle_url;
use html_writer;
use stdClass;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->libdir.'/tablelib.php');
/**
* Class H5P activity participants report.
*
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <ferran@moodle.com>
*/
class participants extends table_sql implements report {
/** @var manager the H5P activity manager instance. */
private $manager;
/** @var array the users scored attempts. */
private $scores;
/** @var array the user attempts count. */
private $count;
/**
* Create a new participants report.
*
* @param manager $manager h5pactivitymanager object
* @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
*/
public function __construct(manager $manager, $currentgroup = false) {
parent::__construct('mod_h5pactivity-participants');
$this->manager = $manager;
$this->scores = $manager->get_users_scaled_score();
$this->count = $manager->count_users_attempts();
// Setup table_sql.
$columns = ['fullname', 'timemodified', 'score', 'attempts'];
$headers = [
get_string('fullname'), get_string('date'),
get_string('score', 'mod_h5pactivity'), get_string('attempts', 'mod_h5pactivity'),
];
$this->define_columns($columns);
$this->define_headers($headers);
$this->set_attribute('class', 'generaltable generalbox boxaligncenter boxwidthwide');
$this->sortable(true);
$this->no_sorting('score');
$this->no_sorting('timemodified');
$this->no_sorting('attempts');
$this->pageable(true);
$capjoin = $this->manager->get_active_users_join(true, $currentgroup);
// Final SQL.
$this->set_sql(
'DISTINCT u.id, u.picture, u.firstname, u.lastname, u.firstnamephonetic, u.lastnamephonetic,
u.middlename, u.alternatename, u.imagealt, u.email',
"{user} u $capjoin->joins",
$capjoin->wheres,
$capjoin->params);
}
/**
* Return the report user record.
*
* Participants report has no specific user.
*
* @return stdClass|null a user or null
*/
public function get_user(): ?stdClass {
return null;
}
/**
* Return the report attempt object.
*
* Participants report has no specific attempt.
*
* @return attempt|null the attempt object or null
*/
public function get_attempt(): ?attempt {
return null;
}
/**
* Print the report.
*/
public function print(): void {
global $PAGE, $OUTPUT;
$this->define_baseurl($PAGE->url);
$this->out($this->get_page_size(), true);
}
/**
* Warning in case no user has the selected initials letters.
*
*/
public function print_nothing_to_display() {
global $OUTPUT;
echo $this->render_reset_button();
$this->print_initials_bar();
echo $OUTPUT->notification(get_string('noparticipants', 'mod_h5pactivity'), 'warning');
}
/**
* Generate the fullname column.
*
* @param stdClass $user
* @return string
*/
public function col_fullname($user): string {
global $OUTPUT;
$cm = $this->manager->get_coursemodule();
return $OUTPUT->user_picture($user, ['size' => 35, 'courseid' => $cm->course, 'includefullname' => true]);
}
/**
* Generate score column.
*
* @param stdClass $user the user record
* @return string
*/
public function col_score(stdClass $user): string {
$cm = $this->manager->get_coursemodule();
if (isset($this->scores[$user->id])) {
$score = $this->scores[$user->id];
$maxgrade = floatval(100);
$scaled = round($maxgrade * $score->scaled).'%';
if (empty($score->attemptid)) {
return $scaled;
} else {
$url = new moodle_url('/mod/h5pactivity/report.php', ['a' => $cm->instance, 'attemptid' => $score->attemptid]);
return html_writer::link($url, $scaled);
}
}
return '';
}
/**
* Generate attempts count column, if any.
*
* @param stdClass $user the user record
* @return string
*/
public function col_attempts(stdClass $user): string {
$cm = $this->manager->get_coursemodule();
if (isset($this->count[$user->id])) {
$msg = get_string('review_user_attempts', 'mod_h5pactivity', $this->count[$user->id]);
$url = new moodle_url('/mod/h5pactivity/report.php', ['a' => $cm->instance, 'userid' => $user->id]);
return html_writer::link($url, $msg);
}
return '';
}
/**
* Generate attempt timemodified column, if any.
*
* @param stdClass $user the user record
* @return string
*/
public function col_timemodified(stdClass $user): string {
if (isset($this->scores[$user->id])) {
$score = $this->scores[$user->id];
return userdate($score->timemodified);
}
return '';
}
/**
* Print headers
*
* Note: as per MDL-80754, we have to modify the header dynamically to display the total
* attempts in the column header.
*/
public function print_headers(): void {
$totalcount = array_sum($this->count);
$this->headers[$this->columns['attempts']] = get_string('attempts_report_header_label', 'mod_h5pactivity', $totalcount);
parent::print_headers();
}
}
@@ -0,0 +1,114 @@
<?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 results report.
*
* @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\report;
use mod_h5pactivity\local\report;
use mod_h5pactivity\local\manager;
use mod_h5pactivity\local\attempt;
use mod_h5pactivity\output\reportresults;
use stdClass;
/**
* Class H5P activity results report.
*
* @package mod_h5pactivity
* @since Moodle 3.9
* @copyright 2020 Ferran Recio <ferran@moodle.com>
*/
class results implements report {
/** @var manager the H5P activity manager instance. */
private $manager;
/** @var stdClass the user record. */
private $user;
/** @var attempt the h5pactivity attempt to show. */
private $attempt;
/**
* Create a new participants report.
*
* @param manager $manager h5pactivity manager object
* @param stdClass $user user record
* @param attempt $attempt attempt object
*/
public function __construct(manager $manager, stdClass $user, attempt $attempt) {
$this->manager = $manager;
$this->user = $user;
$this->attempt = $attempt;
}
/**
* Return the report user record.
*
* @return stdClass|null a user or null
*/
public function get_user(): ?stdClass {
return $this->user;
}
/**
* Return the report attempt object.
*
* Attempts report has no specific attempt.
*
* @return attempt|null the attempt object or null
*/
public function get_attempt(): ?attempt {
return $this->attempt;
}
/**
* Print the report.
*/
public function print(): void {
global $OUTPUT;
$manager = $this->manager;
$attempt = $this->attempt;
$cm = $manager->get_coursemodule();
$widget = new reportresults($attempt, $this->user, $cm->course);
echo $OUTPUT->render($widget);
}
/**
* Get the export data form this report.
*
* This method is used to render the report in mobile.
*/
public function export_data_for_external(): stdClass {
global $PAGE;
$manager = $this->manager;
$attempt = $this->attempt;
$cm = $manager->get_coursemodule();
$widget = new reportresults($attempt, $this->user, $cm->course);
return $widget->export_for_template($PAGE->get_renderer('core'));
}
}
+228
View File
@@ -0,0 +1,228 @@
<?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/>.
/**
* Contains class mod_h5pactivity\output\reportlink
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\output;
defined('MOODLE_INTERNAL') || die();
use mod_h5pactivity\local\attempt as activity_attempt;
use renderable;
use templatable;
use renderer_base;
use moodle_url;
use user_picture;
use stdClass;
/**
* Class to help display report link in mod_h5pactivity.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class attempt implements renderable, templatable {
/** @var activity_attempt attempt */
public $attempt;
/** @var stdClass user record */
public $user;
/** @var int courseid necesary to present user picture */
public $courseid;
/**
* Constructor.
*
* @param activity_attempt $attempt the attempt object
* @param stdClass $user a user record (default null).
* @param int $courseid optional course id (default null).
*/
public function __construct(activity_attempt $attempt, stdClass $user = null, int $courseid = null) {
$this->attempt = $attempt;
$this->user = $user;
$this->courseid = $courseid;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output
* @return stdClass
*/
public function export_for_template(renderer_base $output) {
$attempt = $this->attempt;
$data = (object)[
'id' => $attempt->get_id(),
'h5pactivityid' => $attempt->get_h5pactivityid(),
'userid' => $attempt->get_userid(),
'timecreated' => $attempt->get_timecreated(),
'timemodified' => $attempt->get_timemodified(),
'attempt' => $attempt->get_attempt(),
'rawscore' => $attempt->get_rawscore(),
'maxscore' => $attempt->get_maxscore(),
'duration' => '-',
'durationcompact' => '-',
'completion' => $attempt->get_completion(),
'completionicon' => $this->completion_icon($output, $attempt->get_completion()),
'completiontext' => $this->completion_icon($output, $attempt->get_completion(), true),
'success' => $attempt->get_success(),
'successicon' => $this->success_icon($output, $attempt->get_success()),
'successtext' => $this->success_icon($output, $attempt->get_success(), true),
'scaled' => $attempt->get_scaled(),
'reporturl' => new moodle_url('/mod/h5pactivity/report.php', [
'a' => $attempt->get_h5pactivityid(), 'attemptid' => $attempt->get_id()
]),
];
if ($attempt->get_duration() !== null) {
$data->durationvalue = $attempt->get_duration();
$duration = $this->extract_duration($data->durationvalue);
$data->duration = $this->format_duration($duration);
$data->durationcompact = $this->format_duration_short($duration);
}
if (!empty($data->maxscore)) {
$data->score = get_string('score_out_of', 'mod_h5pactivity', $data);
}
if ($this->user) {
$data->user = $this->user;
$userpicture = new user_picture($this->user);
$userpicture->courseid = $this->courseid;
$data->user->picture = $output->render($userpicture);
$data->user->fullname = fullname($this->user);
}
return $data;
}
/**
* Return a completion icon HTML.
*
* @param renderer_base $output the renderer base object
* @param int|null $completion the current completion value
* @param bool $showtext if the icon must have a text or only icon
* @return string icon HTML
*/
private function completion_icon(renderer_base $output, int $completion = null, bool $showtext = false): string {
if ($completion === null) {
return '';
}
if ($completion) {
$alt = get_string('attempt_completion_yes', 'mod_h5pactivity');
$icon = 'i/completion-auto-y';
} else {
$alt = get_string('attempt_completion_no', 'mod_h5pactivity');
$icon = 'i/completion-auto-n';
}
$text = '';
if ($showtext) {
$text = $alt;
$alt = '';
}
return $output->pix_icon($icon, $alt).$text;
}
/**
* Return a success icon
* @param renderer_base $output the renderer base object
* @param int|null $success the current success value
* @param bool $showtext if the icon must have a text or only icon
* @return string icon HTML
*/
private function success_icon(renderer_base $output, int $success = null, bool $showtext = false): string {
if ($success === null) {
$alt = get_string('attempt_success_unknown', 'mod_h5pactivity');
if ($showtext) {
return $alt;
}
$icon = 'i/empty';
} else if ($success) {
$alt = get_string('attempt_success_pass', 'mod_h5pactivity');
$icon = 'i/checkedcircle';
} else {
$alt = get_string('attempt_success_fail', 'mod_h5pactivity');
$icon = 'i/uncheckedcircle';
}
$text = '';
if ($showtext) {
$text = $alt;
$alt = '';
}
return $output->pix_icon($icon, $alt).$text;
}
/**
* Return the duration in long format (localized)
*
* @param stdClass $duration object with (h)hours, (m)minutes and (s)seconds
* @return string the long format duration
*/
private function format_duration(stdClass $duration): string {
$result = [];
if ($duration->h) {
$result[] = get_string('numhours', 'moodle', $duration->h);
}
if ($duration->m) {
$result[] = get_string('numminutes', 'moodle', $duration->m);
}
if ($duration->s) {
$result[] = get_string('numseconds', 'moodle', $duration->s);
}
return implode(' ', $result);
}
/**
* Return the duration en short format (for example: 145' 43'')
*
* Note: this method is used to make duration responsive.
*
* @param stdClass $duration object with (h)hours, (m)minutes and (s)seconds
* @return string the short format duration
*/
private function format_duration_short(stdClass $duration): string {
$result = [];
if ($duration->h || $duration->m) {
$result[] = ($duration->h * 60 + $duration->m)."'";
}
if ($duration->s) {
$result[] = $duration->s."''";
}
return implode(' ', $result);
}
/**
* Extract hours and minutes from second duration.
*
* Note: this function is used to generate the param for format_duration
* and format_duration_short
*
* @param int $seconds number of second
* @return stdClass with (h)hours, (m)minutes and (s)seconds
*/
private function extract_duration(int $seconds): stdClass {
$h = floor($seconds / 3600);
$m = floor(($seconds - $h * 3600) / 60);
$s = $seconds - ($h * 3600 + $m * 60);
return (object)['h' => $h, 'm' => $m, 's' => $s];
}
}
@@ -0,0 +1,118 @@
<?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/>.
/**
* Contains class mod_h5pactivity\output\report\attempts
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\output;
defined('MOODLE_INTERNAL') || die();
use mod_h5pactivity\local\attempt;
use mod_h5pactivity\output\attempt as output_attempt;
use renderable;
use templatable;
use renderer_base;
use user_picture;
use stdClass;
/**
* Class to output an attempts report on mod_h5pactivity.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class reportattempts implements renderable, templatable {
/** @var attempt[] attempts */
public $attempts;
/** @var stdClass user record */
public $user;
/** @var int courseid necesary to present user picture */
public $courseid;
/** @var attempt scored attempt */
public $scored;
/** @var string scored attempt title */
public $title;
/**
* Constructor.
*
* The "scored attempt" is the attempt used for grading. By default it is the max score attempt
* but this could be defined in the activity settings. In some cases this scored attempts does not
* exists at all, this is the reason why it's an optional param.
*
* @param array $attempts an array of attempts
* @param stdClass $user a user record
* @param int $courseid course id
* @param string|null $title title to display on the scored attempt (null if none attempt is the scored one)
* @param attempt|null $scored the scored attempt (null if none)
*/
public function __construct(array $attempts, stdClass $user, int $courseid, string $title = null, attempt $scored = null) {
$this->attempts = $attempts;
$this->user = $user;
$this->courseid = $courseid;
$this->title = $title;
$this->scored = $scored;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output
* @return stdClass
*/
public function export_for_template(renderer_base $output) {
global $USER;
$data = (object)['attempts' => [], 'user' => $this->user];
foreach ($this->attempts as $attempt) {
$outputattempt = new output_attempt($attempt);
$data->attempts[] = $outputattempt->export_for_template($output);
}
$data->attemptscount = count($data->attempts);
$userpicture = new user_picture($this->user);
$userpicture->courseid = $this->courseid;
$data->user->fullname = fullname($this->user);
$data->user->picture = $output->render($userpicture);
if ($USER->id == $this->user->id) {
$data->title = get_string('myattempts', 'mod_h5pactivity');
}
if (!empty($this->title)) {
$scored = (object)[
'title' => $this->title,
'attempts' => [],
];
$outputattempt = new output_attempt($this->scored);
$scored->attempts[] = $outputattempt->export_for_template($output);
$data->scored = $scored;
}
return $data;
}
}
@@ -0,0 +1,68 @@
<?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/>.
/**
* Contains class mod_h5pactivity\output\reportlink
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\output;
defined('MOODLE_INTERNAL') || die();
use renderable;
use templatable;
use renderer_base;
use moodle_url;
/**
* Class to help display report link in mod_h5pactivity.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class reportlink implements renderable, templatable {
/** @var H5P factory */
public $url;
/** @var H5P library list */
public $message;
/**
* Constructor.
*
* @param moodle_url $url the destination url
* @param string $message the link message
*/
public function __construct(moodle_url $url, string $message) {
$this->url = $url;
$this->message = $message;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output
* @return stdClass
*/
public function export_for_template(renderer_base $output) {
return $this;
}
}
@@ -0,0 +1,92 @@
<?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/>.
/**
* Contains class mod_h5pactivity\output\reportresults
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\output;
defined('MOODLE_INTERNAL') || die();
use mod_h5pactivity\local\attempt;
use mod_h5pactivity\output\attempt as output_attempt;
use mod_h5pactivity\output\result as output_result;
use renderable;
use templatable;
use renderer_base;
use stdClass;
/**
* Class to display the result report in mod_h5pactivity.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class reportresults implements renderable, templatable {
/** @var attempt the header attempt */
public $attempt;
/** @var stdClass user record */
public $user;
/** @var int courseid necesary to present user picture */
public $courseid;
/**
* Constructor.
*
* @param attempt $attempt the current attempt
* @param stdClass $user a user record
* @param int $courseid course id
*/
public function __construct(attempt $attempt, stdClass $user, int $courseid) {
$this->attempt = $attempt;
$this->user = $user;
$this->courseid = $courseid;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output
* @return stdClass
*/
public function export_for_template(renderer_base $output) {
$outputattempt = new output_attempt($this->attempt, $this->user, $this->courseid);
$data = (object)[
'attempt' => $outputattempt->export_for_template($output),
];
$results = $this->attempt->get_results();
$data->results = [];
foreach ($results as $key => $result) {
$outputresult = output_result::create_from_record($result);
if ($outputresult) {
$data->results[] = $outputresult->export_for_template($output);
}
}
return $data;
}
}
+300
View File
@@ -0,0 +1,300 @@
<?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/>.
/**
* Contains class mod_h5pactivity\output\result
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\output;
defined('MOODLE_INTERNAL') || die();
use renderable;
use templatable;
use renderer_base;
use stdClass;
/**
* Class to display an attempt tesult in mod_h5pactivity.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class result implements renderable, templatable {
/** Correct answer state. */
const CORRECT = 1;
/** Incorrect answer state. */
const INCORRECT = 2;
/** Checked answer state. */
const CHECKED = 3;
/** Unchecked answer state. */
const UNCHECKED = 4;
/** Pass answer state. */
const PASS = 5;
/** Pass answer state. */
const FAIL = 6;
/** Unknown answer state. */
const UNKNOWN = 7;
/** Text answer state. */
const TEXT = 8;
/** @var stdClass result record */
protected $result;
/** @var mixed additional decoded data */
protected $additionals;
/** @var mixed response decoded data */
protected $response;
/** @var mixed correctpattern decoded data */
protected $correctpattern = [];
/**
* Constructor.
*
* @param stdClass $result a h5pactivity_attempts_results record
*/
protected function __construct(stdClass $result) {
$this->result = $result;
if (empty($result->additionals)) {
$this->additionals = new stdClass();
} else {
$this->additionals = json_decode($result->additionals);
}
$this->response = $this->decode_response($result->response);
if (!empty($result->correctpattern)) {
$correctpattern = json_decode($result->correctpattern);
foreach ($correctpattern as $pattern) {
$this->correctpattern[] = $this->decode_response($pattern);
}
}
}
/**
* return the correct result output depending on the interactiontype
*
* @param stdClass $result h5pactivity_attempts_results record
* @return result|null the result output class if any
*/
public static function create_from_record(stdClass $result): ?self {
// Compound result track is omitted from the report.
if ($result->interactiontype == 'compound') {
return null;
}
$classname = "mod_h5pactivity\\output\\result\\{$result->interactiontype}";
$classname = str_replace('-', '', $classname);
if (class_exists($classname)) {
return new $classname($result);
}
return new self($result);
}
/**
* Return a decoded response structure.
*
* @param string $value the current response structure
* @return array an array of reponses
*/
private function decode_response(string $value): array {
// If [,] means a list of elements.
$list = explode('[,]', $value);
// Inside a list element [.] means sublist (pair) and [:] a range.
foreach ($list as $key => $item) {
if (strpos($item, '[.]') !== false) {
$list[$key] = explode('[.]', $item);
} else if (strpos($item, '[:]') !== false) {
$list[$key] = explode('[:]', $item);
}
}
return $list;
}
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
$result = $this->result;
$data = (object)[
'id' => $result->id,
'attemptid' => $result->attemptid,
'subcontent' => $result->subcontent,
'timecreated' => $result->timecreated,
'interactiontype' => $result->interactiontype,
'description' => strip_tags($result->description),
'rawscore' => $result->rawscore,
'maxscore' => $result->maxscore,
'duration' => $result->duration,
'completion' => $result->completion,
'success' => $result->success,
];
$result;
$options = $this->export_options();
if (!empty($options)) {
$data->hasoptions = true;
$data->optionslabel = $this->get_optionslabel();
$data->correctlabel = $this->get_correctlabel();
$data->answerlabel = $this->get_answerlabel();
$data->options = array_values($options);
$data->track = true;
}
if (!empty($result->maxscore)) {
$data->score = get_string('score_out_of', 'mod_h5pactivity', $result);
}
return $data;
}
/**
* Return the options data structure.
*
* Result types have to override this method generate a specific options report.
*
* An option is an object with:
* - id: the option ID
* - description: option description text
* - useranswer (optional): what the user answer (see get_answer method)
* - correctanswer (optional): the correct answer (see get_answer method)
*
* @return array of options
*/
protected function export_options(): ?array {
return [];
}
/**
* Return a label for result user options/choices.
*
* Specific result types can override this method to customize
* the result options table header.
*
* @return string to use in options table
*/
protected function get_optionslabel(): string {
return get_string('choice', 'mod_h5pactivity');
}
/**
* Return a label for result user correct answer.
*
* Specific result types can override this method to customize
* the result options table header.
*
* @return string to use in options table
*/
protected function get_correctlabel(): string {
return get_string('correct_answer', 'mod_h5pactivity');
}
/**
* Return a label for result user attempt answer.
*
* Specific result types can override this method to customize
* the result options table header.
*
* @return string to use in options table
*/
protected function get_answerlabel(): string {
return get_string('attempt_answer', 'mod_h5pactivity');
}
/**
* Extract descriptions from array.
*
* @param array $data additional attribute to parse
* @return string[] the resulting strings
*/
protected function get_descriptions(array $data): array {
$result = [];
foreach ($data as $key => $value) {
$description = $this->get_description($value);
$index = $value->id ?? $key;
$index = trim($index);
if (is_numeric($index)) {
$index = intval($index);
}
$result[$index] = (object)['description' => $description, 'id' => $index];
}
ksort($result);
return $result;
}
/**
* Extract description from data element.
*
* @param stdClass $data additional attribute to parse
* @return string the resulting string
*/
protected function get_description(stdClass $data): string {
if (!isset($data->description)) {
return '';
}
$translations = (array) $data->description;
if (empty($translations)) {
return '';
}
// By default, H5P packages only send "en-US" descriptions.
$result = $translations['en-US'] ?? array_shift($translations);
return trim($result);
}
/**
* Return an answer data to show results.
*
* @param int $state the answer state
* @param string $answer the extra text to display (default null)
* @return stdClass with "answer" text and the state attribute to be displayed
*/
protected function get_answer(int $state, string $answer = null): stdClass {
$states = [
self::CORRECT => 'correct',
self::INCORRECT => 'incorrect',
self::CHECKED => 'checked',
self::UNCHECKED => 'unchecked',
self::PASS => 'pass',
self::FAIL => 'fail',
self::UNKNOWN => 'unknown',
self::TEXT => 'text',
];
$state = $states[$state] ?? self::UNKNOWN;
if ($answer === null) {
$answer = get_string('answer_'.$state, 'mod_h5pactivity');
}
$result = (object)[
'answer' => $answer,
$state => true,
];
return $result;
}
}
@@ -0,0 +1,101 @@
<?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/>.
/**
* Contains class mod_h5pactivity\output\result\choice
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\output\result;
defined('MOODLE_INTERNAL') || die();
use mod_h5pactivity\output\result;
use renderer_base;
/**
* Class to display H5P choice result.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class choice extends result {
/**
* Return the options data structure.
*
* @return array of options
*/
protected function export_options(): ?array {
// Suppose H5P choices have only a single list of valid answers.
$correctpattern = reset($this->correctpattern);
if (empty($correctpattern)) {
$correctpattern = [];
}
$additionals = $this->additionals;
// H5P has a special extension for long choices.
$extensions = (array) $additionals->extensions ?? [];
$filter = isset($extensions['https://h5p.org/x-api/line-breaks']) ? true : false;
if (isset($additionals->choices)) {
$options = $this->get_descriptions($additionals->choices);
} else {
$options = [];
}
// Some H5P activities like Find the Words don't user the standard CMI format delimiter
// and don't use propper choice additionals. In those cases the report needs to fix this
// using the correct pattern as choices and using a non standard delimiter.
if (empty($options)) {
if (count($correctpattern) == 1) {
$correctpattern = explode(',', reset($correctpattern));
}
foreach ($correctpattern as $value) {
$option = (object)[
'id' => $value,
'description' => $value,
];
$options[$value] = $option;
}
}
foreach ($options as $key => $value) {
$correctstate = (in_array($key, $correctpattern)) ? parent::CHECKED : parent::UNCHECKED;
if (in_array($key, $this->response)) {
$answerstate = ($correctstate == parent::CHECKED) ? parent::PASS : parent::FAIL;
// In some cases, like Branching scenario H5P activity, no correct Pattern is provided
// so any answer is just a check.
if (empty($correctpattern)) {
$answerstate = parent::CHECKED;
}
$value->useranswer = $this->get_answer($answerstate);
}
$value->correctanswer = $this->get_answer($correctstate);
if ($filter && $correctstate == parent::UNCHECKED && !isset($value->useranswer)) {
unset($options[$key]);
}
}
return $options;
}
}
@@ -0,0 +1,127 @@
<?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/>.
/**
* Contains class mod_h5pactivity\output\result\fillin
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\output\result;
defined('MOODLE_INTERNAL') || die();
use mod_h5pactivity\output\result;
use renderer_base;
use stdClass;
/**
* Class to display H5P fill-in result.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class fillin extends result {
/**
* Return the options data structure.
*
* @return array of options
*/
protected function export_options(): ?array {
$correctpatterns = $this->correctpattern;
$additionals = $this->additionals;
$extensions = (array) $additionals->extensions ?? [];
// There are two way in which H5P could force case sensitivity, with extensions
// or using options in the correctpatterns. By default it is case sensible.
$casesensitive = $extensions['https://h5p.org/x-api/case-sensitivity'] ?? true;
if ((!empty($this->result->correctpattern)
&& strpos($this->result->correctpattern, '{case_matters=false}') !== false)) {
$casesensitive = false;
}
$values = [];
// Add all possibilities from $additionals.
if (isset($extensions['https://h5p.org/x-api/alternatives'])) {
foreach ($extensions['https://h5p.org/x-api/alternatives'] as $key => $value) {
if (!is_array($value)) {
$value = [$value];
}
$values[$key] = ($casesensitive) ? $value : array_change_key_case($value);
}
}
// Add possibilities from correctpattern.
foreach ($correctpatterns as $correctpattern) {
foreach ($correctpattern as $key => $pattern) {
// The xAPI admits more params a part form values.
// For now this extra information is not used in reporting
// but it is posible future H5P types need them.
$value = preg_replace('/\{.+=.*\}/', '', $pattern);
$value = ($casesensitive) ? $value : strtolower($value);
if (!isset($values[$key])) {
$values[$key] = [];
}
if (!in_array($value, $values[$key])) {
array_unshift($values[$key], $value);
}
}
}
// Generate options.
$options = [];
$num = 1;
foreach ($values as $key => $value) {
$option = (object)[
'id' => $key,
'description' => get_string('result_fill-in_gap', 'mod_h5pactivity', $num),
];
$gapresponse = $this->response[$key] ?? null;
$gapresponse = ($casesensitive) ? $gapresponse : strtolower($gapresponse);
if ($gapresponse !== null && in_array($gapresponse, $value)) {
$state = parent::CORRECT;
} else {
$state = parent::INCORRECT;
}
$option->useranswer = $this->get_answer($state, $this->response[$key]);
$option->correctanswer = $this->get_answer(parent::TEXT, implode(' / ', $value));
$options[] = $option;
$num++;
}
return $options;
}
/**
* Return a label for result user options/choices
*
* Specific result types can override this method to customize
* the result options table header.
*
* @return string to use in options table
*/
protected function get_optionslabel(): string {
return get_string('result_matching', 'mod_h5pactivity');
}
}
@@ -0,0 +1,58 @@
<?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/>.
/**
* Contains class mod_h5pactivity\output\result\longfillin
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\output\result;
defined('MOODLE_INTERNAL') || die();
use mod_h5pactivity\output\result;
use renderer_base;
use stdClass;
/**
* Class to display H5P long fill in result.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class longfillin extends result {
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
$data = parent::export_for_template($output);
$userresponse = reset($this->response);
$data->content = format_text($userresponse, FORMAT_PLAIN);
$data->track = true;
// Long fill-in is used for Essay type exercices. H5P adds
// extra characters to the description in all fill-in interactions
// but in the essay questions is unnecesary.
$data->description = preg_replace('/__________$/', '', $data->description);
return $data;
}
}
@@ -0,0 +1,139 @@
<?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/>.
/**
* Contains class mod_h5pactivity\output\result\matching
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\output\result;
defined('MOODLE_INTERNAL') || die();
use mod_h5pactivity\output\result;
/**
* Class to display H5P matching result.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class matching extends result {
/**
* Return the options data structure.
*
* @return array|null of options
*/
protected function export_options(): ?array {
// Suppose H5P choices have only list of valid answers.
$correctpattern = reset($this->correctpattern);
$additionals = $this->additionals;
// Get sources (options).
if (isset($additionals->source)) {
$sources = $this->get_descriptions($additionals->source);
} else {
$sources = [];
}
// Get targets.
if (isset($additionals->target)) {
$targets = $this->get_descriptions($additionals->target);
} else {
$targets = [];
}
// Create original options array.
$options = array_map(function ($source) {
$cloneddraggable = clone $source;
$cloneddraggable->correctanswers = [];
return $cloneddraggable;
}, $sources);
// Fill options with correct answers flags if they exist.
foreach ($correctpattern as $pattern) {
if (!is_array($pattern) || count($pattern) != 2) {
continue;
}
// We assume here that the activity is following the convention sets in:
// https://github.com/h5p/h5p-php-report/blob/master/type-processors/matching-processor.class.php
// i.e. source is index 1 and dropzone is index 0.
if (isset($sources[$pattern[1]]) && isset($targets[$pattern[0]])) {
$target = $targets[$pattern[0]];
$source = $sources[$pattern[1]];
$currentoption = $options[$source->id];
$currentoption->correctanswers[$target->id] = $target->description;
}
}
// Fill in user responses.
foreach ($this->response as $response) {
if (!is_array($response) || count($response) != 2) {
continue;
}
if (isset($sources[$response[1]]) && isset($targets[$response[0]])) {
$source = $sources[$response[1]];
$target = $targets[$response[0]];
$answer = $response[0];
$option = $options[$source->id] ?? null;
if ($option) {
if (isset($option->correctanswers[$answer])) {
$state = parent::CORRECT;
} else {
$state = parent::INCORRECT;
}
$option->useranswer = $this->get_answer($state, $target->description);
}
}
}
// Fill in unchecked options.
foreach ($options as $option) {
if (!isset($option->useranswer)) {
if (!empty($option->correctanswers)) {
$option->useranswer = $this->get_answer(parent::INCORRECT,
get_string('answer_noanswer', 'mod_h5pactivity'));
} else {
$option->useranswer = $this->get_answer(parent::CORRECT,
get_string('answer_noanswer', 'mod_h5pactivity'));
}
}
}
// Now flattern correct answers.
foreach ($options as $option) {
$option->correctanswer = $this->get_answer( parent::TEXT, join(', ', $option->correctanswers));
unset($option->correctanswers);
}
return array_values($options);
}
/**
* Return a label for result user options/choices
*
* Specific result types can override this method to customize
* the result options table header.
*
* @return string to use in options table
*/
protected function get_optionslabel(): string {
return get_string('result_matching', 'mod_h5pactivity');
}
}
@@ -0,0 +1,54 @@
<?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/>.
/**
* Contains class mod_h5pactivity\output\result\other
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\output\result;
defined('MOODLE_INTERNAL') || die();
use mod_h5pactivity\output\result;
use renderer_base;
use stdClass;
/**
* Class to display H5P other result.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class other extends result {
/**
* Export this data so it can be used as the context for a mustache template.
*
* @param renderer_base $output
* @return stdClass
*/
public function export_for_template(renderer_base $output): stdClass {
$data = parent::export_for_template($output);
if (empty($data->description)) {
$data->description = get_string('result_other', 'mod_h5pactivity');
}
return $data;
}
}
@@ -0,0 +1,102 @@
<?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/>.
/**
* Contains class mod_h5pactivity\output\result\sequencing
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\output\result;
defined('MOODLE_INTERNAL') || die();
use mod_h5pactivity\output\result;
use renderer_base;
/**
* Class to display H5P sequencing result.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class sequencing extends result {
/**
* Return the options data structure.
*
* @return array of options
*/
protected function export_options(): ?array {
$correctpattern = reset($this->correctpattern);
$additionals = $this->additionals;
$response = $this->response;
if (isset($additionals->choices)) {
$choices = $this->get_descriptions($additionals->choices);
} else {
$choices = [];
}
$options = [];
$num = 1;
foreach ($correctpattern as $key => $pattern) {
if (!isset($choices[$pattern])) {
continue;
}
$option = (object)[
'id' => 'true',
'description' => get_string('result_sequencing_position', 'mod_h5pactivity', $num),
'correctanswer' => $this->get_answer(parent::TEXT, $choices[$pattern]->description),
'correctanswerid' => $key,
];
if (isset($response[$key])) {
$responseid = str_replace('item_', '', $response[$key]);
$answerstate = ($responseid == $option->correctanswerid) ? parent::PASS : parent::FAIL;
} else {
$answerstate = parent::FAIL;
}
$option->useranswer = $this->get_answer($answerstate);
$options[$key] = $option;
$num ++;
}
return $options;
}
/**
* Return a label for result user options/choices.
*
* @return string to use in options table
*/
protected function get_optionslabel(): string {
return get_string('result_sequencing_choice', 'mod_h5pactivity');
}
/**
* Return a label for result user correct answer.
*
* @return string to use in options table
*/
protected function get_correctlabel(): string {
return get_string('result_sequencing_answer', 'mod_h5pactivity');
}
}
@@ -0,0 +1,78 @@
<?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/>.
/**
* Contains class mod_h5pactivity\output\result\truefalse
*
* @package mod_h5pactivity
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\output\result;
defined('MOODLE_INTERNAL') || die();
use mod_h5pactivity\output\result;
use renderer_base;
/**
* Class to display H5P choice result.
*
* @copyright 2020 Ferran Recio
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class truefalse extends result {
/**
* Return the options data structure.
*
* @return array of options
*/
protected function export_options(): ?array {
// This interaction type have only one entry which is the correct option.
$correctpattern = reset($this->correctpattern);
$correctpattern = filter_var(reset($correctpattern), FILTER_VALIDATE_BOOLEAN);
$correctpattern = $correctpattern ? 'true' : 'false';
$response = filter_var(reset($this->response), FILTER_VALIDATE_BOOLEAN);
$response = $response ? 'true' : 'false';
$options = [
(object)[
'id' => 'true',
'description' => get_string('true', 'mod_h5pactivity'),
],
(object)[
'id' => 'false',
'description' => get_string('false', 'mod_h5pactivity'),
],
];
foreach ($options as $value) {
$correctstate = ($value->id == $correctpattern) ? parent::CHECKED : parent::UNCHECKED;
if ($value->id == $response) {
$answerstate = ($correctstate == parent::CHECKED) ? parent::PASS : parent::FAIL;
$value->useranswer = $this->get_answer($answerstate);
}
$value->correctanswer = $this->get_answer($correctstate);
}
return $options;
}
}
@@ -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/>.
namespace mod_h5pactivity\privacy;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\contextlist;
use core_privacy\local\request\helper;
use core_privacy\local\request\transform;
use core_privacy\local\request\userlist;
use core_privacy\local\request\writer;
use stdClass;
/**
* Privacy API implementation for the H5P activity plugin.
*
* @package mod_h5pactivity
* @category privacy
* @copyright 2020 Ferran Recio <ferran@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\plugin\provider {
/**
* Return the fields which contain personal data.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table('h5pactivity_attempts', [
'userid' => 'privacy:metadata:userid',
'attempt' => 'privacy:metadata:attempt',
'timecreated' => 'privacy:metadata:timecreated',
'timemodified' => 'privacy:metadata:timemodified',
'rawscore' => 'privacy:metadata:rawscore',
], 'privacy:metadata:xapi_track');
$collection->add_database_table('h5pactivity_attempts_results', [
'attempt' => 'privacy:metadata:attempt',
'timecreated' => 'privacy:metadata:timecreated',
'rawscore' => 'privacy:metadata:rawscore',
], 'privacy:metadata:xapi_track_results');
$collection->add_subsystem_link('core_xapi', [], 'privacy:metadata:xapisummary');
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): contextlist {
$sql = "SELECT ctx.id
FROM {h5pactivity_attempts} ss
JOIN {modules} m
ON m.name = :activityname
JOIN {course_modules} cm
ON cm.instance = ss.h5pactivityid
AND cm.module = m.id
JOIN {context} ctx
ON ctx.instanceid = cm.id
AND ctx.contextlevel = :modlevel
WHERE ss.userid = :userid";
$params = ['activityname' => 'h5pactivity', 'modlevel' => CONTEXT_MODULE, 'userid' => $userid];
$contextlist = new contextlist();
$contextlist->add_from_sql($sql, $params);
\core_xapi\privacy\provider::add_contexts_for_userid($contextlist, $userid, 'mod_h5pactivity');
return $contextlist;
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
if (!is_a($context, \context_module::class)) {
return;
}
$sql = "SELECT ss.userid
FROM {h5pactivity_attempts} ss
JOIN {modules} m
ON m.name = 'h5pactivity'
JOIN {course_modules} cm
ON cm.instance = ss.h5pactivityid
AND cm.module = m.id
JOIN {context} ctx
ON ctx.instanceid = cm.id
AND ctx.contextlevel = :modlevel
WHERE ctx.id = :contextid";
$params = ['modlevel' => CONTEXT_MODULE, 'contextid' => $context->id];
$userlist->add_from_sql('userid', $sql, $params);
\core_xapi\privacy\provider::add_userids_for_context($userlist);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
global $DB;
// Remove contexts different from CONTEXT_MODULE.
$contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) {
if ($context->contextlevel == CONTEXT_MODULE) {
$carry[] = $context->id;
}
return $carry;
}, []);
if (empty($contexts)) {
return;
}
$user = $contextlist->get_user();
$userid = $user->id;
// Get H5P attempts data.
foreach ($contexts as $contextid) {
$context = \context::instance_by_id($contextid);
$data = helper::get_context_data($context, $user);
writer::with_context($context)->export_data([], $data);
helper::export_context_files($context, $user);
// Get user's xAPI state data for the particular context.
$state = \core_xapi\privacy\provider::get_xapi_states_for_user($contextlist->get_user()->id,
'mod_h5pactivity', $context->instanceid);
if ($state) {
// If the activity has xAPI state data by the user, include it in the export.
writer::with_context($context)->export_data(
[get_string('privacy:xapistate', 'core_xapi')], (object) $state);
}
}
// Get attempts track data.
list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
$sql = "SELECT har.id,
ha.attempt,
har.description,
har.interactiontype,
har.response,
har.additionals,
har.rawscore,
har.maxscore,
har.duration,
har.timecreated,
ctx.id as contextid
FROM {h5pactivity_attempts_results} har
JOIN {h5pactivity_attempts} ha
ON har.attemptid = ha.id
JOIN {course_modules} cm
ON cm.instance = ha.h5pactivityid
JOIN {context} ctx
ON ctx.instanceid = cm.id
WHERE ctx.id $insql
AND ha.userid = :userid";
$params = array_merge($inparams, ['userid' => $userid]);
$alldata = [];
$attemptsdata = $DB->get_recordset_sql($sql, $params);
foreach ($attemptsdata as $track) {
$alldata[$track->contextid][$track->attempt][] = (object)[
'description' => $track->description,
'response' => $track->response,
'interactiontype' => $track->interactiontype,
'additionals' => $track->additionals,
'rawscore' => $track->rawscore,
'maxscore' => $track->maxscore,
'duration' => $track->duration,
'timecreated' => transform::datetime($track->timecreated),
];
}
$attemptsdata->close();
// The result data is organised in:
// {Course name}/{H5P activity name}/{My attempts}/{Attempt X}/data.json
// where X is the attempt number.
array_walk($alldata, function($attemptsdata, $contextid) {
$context = \context::instance_by_id($contextid);
array_walk($attemptsdata, function($data, $attempt) use ($context) {
$subcontext = [
get_string('myattempts', 'mod_h5pactivity'),
get_string('attempt', 'mod_h5pactivity'). " $attempt"
];
writer::with_context($context)->export_data(
$subcontext,
(object)['results' => $data]
);
});
});
}
/**
* Delete all user data which matches the specified context.
*
* @param \context $context A user context.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
// This should not happen, but just in case.
if ($context->contextlevel != CONTEXT_MODULE) {
return;
}
$cm = get_coursemodule_from_id('h5pactivity', $context->instanceid);
if (!$cm) {
// Only h5pactivity module will be handled.
return;
}
self::delete_all_attempts($cm);
// Delete xAPI state data.
\core_xapi\privacy\provider::delete_states_for_all_users($context, 'mod_h5pactivity');
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
foreach ($contextlist as $context) {
if ($context->contextlevel != CONTEXT_MODULE) {
continue;
}
$cm = get_coursemodule_from_id('h5pactivity', $context->instanceid);
if (!$cm) {
// Only h5pactivity module will be handled.
continue;
}
$user = $contextlist->get_user();
self::delete_all_attempts($cm, $user);
// Delete xAPI state data.
\core_xapi\privacy\provider::delete_states_for_user($contextlist, 'mod_h5pactivity');
}
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
$context = $userlist->get_context();
if (!is_a($context, \context_module::class)) {
return;
}
$cm = get_coursemodule_from_id('h5pactivity', $context->instanceid);
if (!$cm) {
// Only h5pactivity module will be handled.
return;
}
$userids = $userlist->get_userids();
foreach ($userids as $userid) {
self::delete_all_attempts ($cm, (object)['id' => $userid]);
}
// Delete xAPI states data.
\core_xapi\privacy\provider::delete_states_for_userlist($userlist);
}
/**
* 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
*/
private 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);
}
}
@@ -0,0 +1,44 @@
<?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/>.
/**
* Search area for mod_h5pactivity activities.
*
* @package mod_h5pactivity
* @copyright 2022 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_h5pactivity\search;
/**
* Search area for mod_h5pactivity activities.
*
* @package mod_h5pactivity
* @copyright 2022 Carlos Escobedo <carlos@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class activity extends \core_search\base_activity {
/**
* Returns true if this area uses file indexing.
*
* @return bool
*/
public function uses_file_indexing() {
return true;
}
}
+186
View File
@@ -0,0 +1,186 @@
<?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/>.
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;
use core\event\base as event_base;
use core_xapi\local\state;
use moodle_exception;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot.'/mod/h5pactivity/lib.php');
/**
* Class xapi_handler for H5P statements and states.
*
* @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 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;
}
$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() && $manager->can_submit($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
// 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;
}
// 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();
$params = [
'other' => $minstatement,
'context' => $context,
'objectid' => $cm->instance,
'userid' => $user->id,
];
return statement_received::create($params);
}
/**
* Validate a xAPI state.
*
* Check if the state is valid for this handler.
*
* This method is used also for the state get requests so the validation
* cannot rely on having state data.
*
* @param state $state
* @return bool if the state is valid or not
*/
protected function validate_state(state $state): bool {
$xapiobject = $state->get_activity_id();
// H5P add some extra params to ID to define subcontents.
$parts = explode('?', $xapiobject, 2);
$contextid = array_shift($parts);
if (empty($contextid) || !is_numeric($contextid)) {
return false;
}
try {
$context = \context::instance_by_id($contextid);
if (!$context instanceof \context_module) {
return false;
}
} catch (moodle_exception $exception) {
return false;
}
$cm = get_coursemodule_from_id('h5pactivity', $context->instanceid, 0, false);
if (!$cm) {
return false;
}
// If tracking is not enabled or the user can't submit, the state won't be considered valid.
$manager = manager::create_from_coursemodule($cm);
$user = $state->get_user();
if (!($manager->is_tracking_enabled() && $manager->can_submit($user))) {
return false;
}
return true;
}
}