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
+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'));
}
}