diff --git a/mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php b/mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php index 76a45227253..339bb2ce42a 100644 --- a/mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php +++ b/mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php @@ -41,14 +41,14 @@ class backup_h5pactivity_activity_structure_step extends backup_activity_structu // Replace with the attributes and final elements that the element will handle. $attributes = ['id']; $finalelements = ['name', 'timecreated', 'timemodified', 'intro', - 'introformat', 'grade', 'displayoptions', 'enabletracking', 'grademethod']; + 'introformat', 'grade', 'displayoptions', 'enabletracking', 'grademethod', 'reviewmode']; $root = new backup_nested_element('h5pactivity', $attributes, $finalelements); $attempts = new backup_nested_element('attempts'); $attempt = new backup_nested_element('attempt', ['id'], ['h5pactivityid', 'userid', 'timecreated', 'timemodified', 'attempt', 'rawscore', 'maxscore', - 'duration', 'completion', 'success'] + 'duration', 'completion', 'success', 'scaled'] ); $results = new backup_nested_element('results'); diff --git a/mod/h5pactivity/classes/event/report_viewed.php b/mod/h5pactivity/classes/event/report_viewed.php new file mode 100644 index 00000000000..bf3ed09f151 --- /dev/null +++ b/mod/h5pactivity/classes/event/report_viewed.php @@ -0,0 +1,131 @@ +. + +/** + * H5P activity report viewed. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @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 + * @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; + } + +} diff --git a/mod/h5pactivity/classes/local/attempt.php b/mod/h5pactivity/classes/local/attempt.php index e037ebbcc01..0333f766f55 100644 --- a/mod/h5pactivity/classes/local/attempt.php +++ b/mod/h5pactivity/classes/local/attempt.php @@ -293,6 +293,17 @@ class attempt { 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. * @@ -404,6 +415,24 @@ class attempt { 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. * @@ -458,6 +487,15 @@ class attempt { return $this->record->success; } + /** + * Return the attempt scaled. + * + * @return int|null the scaled value + */ + public function get_scaled(): ?int { + return $this->record->scaled; + } + /** * Return if the attempt has been modified. * diff --git a/mod/h5pactivity/classes/local/manager.php b/mod/h5pactivity/classes/local/manager.php index 466995a9925..4f446c67086 100644 --- a/mod/h5pactivity/classes/local/manager.php +++ b/mod/h5pactivity/classes/local/manager.php @@ -25,9 +25,13 @@ 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; /** @@ -55,6 +59,12 @@ class manager { /** 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; @@ -118,6 +128,38 @@ class manager { ]; } + /** + * 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. * @@ -135,6 +177,50 @@ class manager { 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. * @@ -198,6 +284,46 @@ class manager { return $DB->get_records_sql($sql, $params); } + /** + * Count the activity completed attempts. + * + * If no user is provided will count all activity attempts. + * + * @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; + $params = [ + 'h5pactivityid' => $this->instance->id, + 'completion' => 1 + ]; + if ($userid) { + $params['userid'] = $userid; + } + return $DB->count_records('h5pactivity_attempts', $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. * @@ -208,7 +334,7 @@ class manager { } /** - * Return the current context. + * Return the current instance. * * @return stdClass the instance record */ @@ -234,4 +360,89 @@ class manager { $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 + * @return report|null available report (or null if no report available) + */ + public function get_report(int $userid = null, int $attemptid = null): ?report { + global $USER; + $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); + } else if ($this->can_view_own_attempts()) { + $user = $USER; + if ($userid && $user->id != $userid) { + return null; + } + } else { + return null; + } + + // Check if that user can be tracked. + if ($user && !$this->is_tracking_enabled($user)) { + 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); + } + + /** + * 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]); + 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; + } } diff --git a/mod/h5pactivity/classes/local/report.php b/mod/h5pactivity/classes/local/report.php new file mode 100644 index 00000000000..4267e49e298 --- /dev/null +++ b/mod/h5pactivity/classes/local/report.php @@ -0,0 +1,58 @@ +. + +/** + * H5P activity report interface + * + * @package mod_h5pactivity + * @since Moodle 3.9 + * @copyright 2020 Ferran Recio + * @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 + */ +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; +} \ No newline at end of file diff --git a/mod/h5pactivity/classes/local/report/attempts.php b/mod/h5pactivity/classes/local/report/attempts.php new file mode 100644 index 00000000000..eac8460a608 --- /dev/null +++ b/mod/h5pactivity/classes/local/report/attempts.php @@ -0,0 +1,139 @@ +. + +/** + * H5P activity attempts report + * + * @package mod_h5pactivity + * @since Moodle 3.9 + * @copyright 2020 Ferran Recio + * @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 + */ +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; + } +} diff --git a/mod/h5pactivity/classes/local/report/participants.php b/mod/h5pactivity/classes/local/report/participants.php new file mode 100644 index 00000000000..973b422e868 --- /dev/null +++ b/mod/h5pactivity/classes/local/report/participants.php @@ -0,0 +1,204 @@ +. + +/** + * H5P activity participants report + * + * @package mod_h5pactivity + * @since Moodle 3.9 + * @copyright 2020 Ferran Recio + * @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 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 + */ +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 + */ + public function __construct(manager $manager) { + 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); + + // Set query SQL. + $capjoin = get_enrolled_with_capabilities_join($this->manager->get_context(), '', 'mod/h5pactivity:submit'); + $this->set_sql( + 'u.*', + "{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); + + echo $OUTPUT->heading(get_string('attempts_report', 'mod_h5pactivity')); + + $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 ''; + } +} diff --git a/mod/h5pactivity/classes/local/report/results.php b/mod/h5pactivity/classes/local/report/results.php new file mode 100644 index 00000000000..c5690f9c715 --- /dev/null +++ b/mod/h5pactivity/classes/local/report/results.php @@ -0,0 +1,98 @@ +. + +/** + * H5P activity results report. + * + * @package mod_h5pactivity + * @since Moodle 3.9 + * @copyright 2020 Ferran Recio + * @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 + */ +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); + } +} diff --git a/mod/h5pactivity/classes/output/attempt.php b/mod/h5pactivity/classes/output/attempt.php new file mode 100644 index 00000000000..ac433b44886 --- /dev/null +++ b/mod/h5pactivity/classes/output/attempt.php @@ -0,0 +1,227 @@ +. + +/** + * 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) { + $duration = $this->extract_duration($attempt->get_duration()); + $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]; + } +} diff --git a/mod/h5pactivity/classes/output/reportattempts.php b/mod/h5pactivity/classes/output/reportattempts.php new file mode 100644 index 00000000000..84b15e50f83 --- /dev/null +++ b/mod/h5pactivity/classes/output/reportattempts.php @@ -0,0 +1,118 @@ +. + +/** + * 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; + } +} diff --git a/mod/h5pactivity/classes/output/reportlink.php b/mod/h5pactivity/classes/output/reportlink.php new file mode 100644 index 00000000000..771aa0386eb --- /dev/null +++ b/mod/h5pactivity/classes/output/reportlink.php @@ -0,0 +1,68 @@ +. + +/** + * 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; + } +} diff --git a/mod/h5pactivity/classes/output/reportresults.php b/mod/h5pactivity/classes/output/reportresults.php new file mode 100644 index 00000000000..7627a30cea9 --- /dev/null +++ b/mod/h5pactivity/classes/output/reportresults.php @@ -0,0 +1,92 @@ +. + +/** + * 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; + } +} diff --git a/mod/h5pactivity/classes/output/result.php b/mod/h5pactivity/classes/output/result.php new file mode 100644 index 00000000000..fc8b53af5ca --- /dev/null +++ b/mod/h5pactivity/classes/output/result.php @@ -0,0 +1,300 @@ +. + +/** + * 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; + + /** Unkown 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' => format_string($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 => 'unkown', + 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; + } +} diff --git a/mod/h5pactivity/classes/output/result/choice.php b/mod/h5pactivity/classes/output/result/choice.php new file mode 100644 index 00000000000..da4011e70a9 --- /dev/null +++ b/mod/h5pactivity/classes/output/result/choice.php @@ -0,0 +1,101 @@ +. + +/** + * 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; + } +} diff --git a/mod/h5pactivity/classes/output/result/fillin.php b/mod/h5pactivity/classes/output/result/fillin.php new file mode 100644 index 00000000000..0439845ce36 --- /dev/null +++ b/mod/h5pactivity/classes/output/result/fillin.php @@ -0,0 +1,139 @@ +. + +/** + * 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 { + + /** + * 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); + $data->content = $this->result->description; + $data->description = get_string('result_fill-in', 'mod_h5pactivity'); + return $data; + } + + /** + * 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}') !== null) { + $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, $gapresponse); + + $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'); + } +} diff --git a/mod/h5pactivity/classes/output/result/longfillin.php b/mod/h5pactivity/classes/output/result/longfillin.php new file mode 100644 index 00000000000..7297132cd5b --- /dev/null +++ b/mod/h5pactivity/classes/output/result/longfillin.php @@ -0,0 +1,53 @@ +. + +/** + * 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); + $data->content = reset($this->response); + $data->track = true; + return $data; + } +} diff --git a/mod/h5pactivity/classes/output/result/matching.php b/mod/h5pactivity/classes/output/result/matching.php new file mode 100644 index 00000000000..645afdfdcc6 --- /dev/null +++ b/mod/h5pactivity/classes/output/result/matching.php @@ -0,0 +1,126 @@ +. + +/** + * 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; +use renderer_base; + +/** + * 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 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)) { + $options = $this->get_descriptions($additionals->source); + } else { + $options = []; + } + + // Get targets. + if (isset($additionals->target)) { + $targets = $this->get_descriptions($additionals->target); + } else { + $targets = []; + } + + // Correct answers. + foreach ($correctpattern as $pattern) { + if (!is_array($pattern) || count($pattern) != 2) { + continue; + } + // One pattern must be from options and the other from targets. + if (isset($options[$pattern[0]]) && isset($targets[$pattern[1]])) { + $option = $options[$pattern[0]]; + $target = $targets[$pattern[1]]; + } else if (isset($targets[$pattern[0]]) && isset($options[$pattern[1]])) { + $option = $options[$pattern[1]]; + $target = $targets[$pattern[0]]; + } else { + $option = null; + } + if ($option) { + $option->correctanswer = $this->get_answer(parent::TEXT, $target->description); + $option->correctanswerid = $target->id; + } + } + + // User responses. + foreach ($this->response as $response) { + if (!is_array($response) || count($response) != 2) { + continue; + } + // One repsonse must be from options and the other from targets. + if (isset($options[$response[0]]) && isset($targets[$response[1]])) { + $option = $options[$response[0]]; + $target = $targets[$response[1]]; + $answer = $response[1]; + } else if (isset($targets[$response[0]]) && isset($options[$response[1]])) { + $option = $options[$response[1]]; + $target = $targets[$response[0]]; + $answer = $response[0]; + } else { + $option = null; + } + if ($option) { + if (isset($option->correctanswerid) && $option->correctanswerid == $answer) { + $state = parent::CORRECT; + } else { + $state = parent::INCORRECT; + } + $option->useranswer = $this->get_answer($state, $target->description); + } + } + 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'); + } +} diff --git a/mod/h5pactivity/classes/output/result/other.php b/mod/h5pactivity/classes/output/result/other.php new file mode 100644 index 00000000000..7d0f66a886b --- /dev/null +++ b/mod/h5pactivity/classes/output/result/other.php @@ -0,0 +1,54 @@ +. + +/** + * 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; + } +} diff --git a/mod/h5pactivity/classes/output/result/sequencing.php b/mod/h5pactivity/classes/output/result/sequencing.php new file mode 100644 index 00000000000..286d1ed10f2 --- /dev/null +++ b/mod/h5pactivity/classes/output/result/sequencing.php @@ -0,0 +1,101 @@ +. + +/** + * 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' => 'item_'.$key, + ]; + if (isset($response[$key])) { + $answerstate = ($response[$key] == $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'); + } +} diff --git a/mod/h5pactivity/classes/output/result/truefalse.php b/mod/h5pactivity/classes/output/result/truefalse.php new file mode 100644 index 00000000000..a16cf1a5a35 --- /dev/null +++ b/mod/h5pactivity/classes/output/result/truefalse.php @@ -0,0 +1,76 @@ +. + +/** + * 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); + + $response = filter_var(reset($this->response), FILTER_VALIDATE_BOOLEAN); + + $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; + } +} diff --git a/mod/h5pactivity/db/access.php b/mod/h5pactivity/db/access.php index cef7abaea51..459b8485c34 100644 --- a/mod/h5pactivity/db/access.php +++ b/mod/h5pactivity/db/access.php @@ -56,4 +56,15 @@ $capabilities = [ 'student' => CAP_ALLOW ], ], + + 'mod/h5pactivity:reviewattempts' => array( + 'captype' => 'read', + 'contextlevel' => CONTEXT_MODULE, + 'archetypes' => array( + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ), + 'clonepermissionsfrom' => 'moodle/grade:manage' + ), ]; diff --git a/mod/h5pactivity/db/install.xml b/mod/h5pactivity/db/install.xml index 0484154eeb0..57cd1e0a085 100644 --- a/mod/h5pactivity/db/install.xml +++ b/mod/h5pactivity/db/install.xml @@ -17,6 +17,7 @@ + diff --git a/mod/h5pactivity/db/upgrade.php b/mod/h5pactivity/db/upgrade.php index d0fb851f427..1f19284b745 100644 --- a/mod/h5pactivity/db/upgrade.php +++ b/mod/h5pactivity/db/upgrade.php @@ -225,5 +225,20 @@ function xmldb_h5pactivity_upgrade($oldversion) { upgrade_mod_savepoint(true, 2020041401, 'h5pactivity'); } + if ($oldversion < 2020042202) { + + // Define field reviewmode to be added to h5pactivity. + $table = new xmldb_table('h5pactivity'); + $field = new xmldb_field('reviewmode', XMLDB_TYPE_INTEGER, '4', null, null, null, '1', 'grademethod'); + + // Conditionally launch add field reviewmode. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // H5pactivity savepoint reached. + upgrade_mod_savepoint(true, 2020042202, 'h5pactivity'); + } + return true; } diff --git a/mod/h5pactivity/grade.php b/mod/h5pactivity/grade.php index ccf6c4af1ec..f4b30e4c955 100644 --- a/mod/h5pactivity/grade.php +++ b/mod/h5pactivity/grade.php @@ -23,6 +23,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +use mod_h5pactivity\local\manager; + require(__DIR__.'/../../config.php'); // Course module ID. @@ -34,9 +36,27 @@ $itemnumber = optional_param('itemnumber', 0, PARAM_INT); // Graded user ID (optional). $userid = optional_param('userid', 0, PARAM_INT); -require_login(); +list ($course, $cm) = get_course_and_cm_from_cmid($id, 'h5pactivity'); -// TODO: in the near future this file will redirect to a specific user H5P attempts page. +require_login($course, true, $cm); -// In the simplest case just redirect to the view page. -redirect('view.php?id='.$id); +$manager = manager::create_from_coursemodule($cm); + +if (!$manager->can_view_all_attempts() && !$manager->can_view_own_attempts()) { + redirect(new moodle_url('/mod/h5pactivity/view.php', ['id' => $id])); +} + +$moduleinstance = $manager->get_instance(); + +$params = [ + 'a' => $moduleinstance->id, + 'userid' => $userid, +]; + +$scores = $manager->get_users_scaled_score($userid); +$score = $scores[$userid] ?? null; +if (!empty($score->attemptid)) { + $params['attemptid'] = $score->attemptid; +} + +redirect(new moodle_url('/mod/h5pactivity/report.php', $params)); diff --git a/mod/h5pactivity/lang/en/h5pactivity.php b/mod/h5pactivity/lang/en/h5pactivity.php index c855830763d..35fa58e810f 100644 --- a/mod/h5pactivity/lang/en/h5pactivity.php +++ b/mod/h5pactivity/lang/en/h5pactivity.php @@ -25,14 +25,42 @@ defined('MOODLE_INTERNAL') || die(); +$string['all_attempts'] = 'All user attempts'; +$string['answer_checked'] = 'Answer checked'; +$string['answer_correct'] = 'Your answer is correct'; +$string['answer_fail'] = 'Incorrect answer'; +$string['answer_incorrect'] = 'Your answer is incorrect'; +$string['answer_pass'] = 'Correct answer'; +$string['answer_unchecked'] = 'Answer unchecked'; +$string['answer_unknown'] = 'Unkown answer'; +$string['answer_text'] = 'Answer text'; $string['areapackage'] = 'Package file'; $string['attempt'] = 'Attempt'; +$string['attempt_average'] = 'Attempts average scored'; +$string['attempt_answer'] = 'Attempt answer'; +$string['attempt_completion_no'] = 'This attempt is not marked as completed'; +$string['attempt_completion_yes'] = 'This attempt is completed'; +$string['attempt_first'] = 'First attempt'; +$string['attempt_highest'] = 'Highest score attempt'; +$string['attempt_last'] = 'Last attempt'; +$string['attempt_none'] = 'No attempts are used for grading'; +$string['attempt_number'] = 'Attempt #{$a}'; +$string['attempt_success_fail'] = 'Fail'; +$string['attempt_success_pass'] = 'Pass'; +$string['attempt_success_unknown'] = 'Not reported'; $string['attempts'] = 'Attempts'; +$string['attempts_report'] = 'Attempts report'; +$string['attempts_none'] = 'This user has no attempts to display.'; +$string['choice'] = 'Choice'; +$string['completion'] = 'Completion'; +$string['correct_answer'] = 'Correct answer'; $string['deleteallattempts'] = 'Delete all H5P attempts'; $string['displayexport'] = 'Allow download'; $string['displayembed'] = 'Embed button'; $string['displaycopyright'] = 'Copyright button'; +$string['duration'] = 'Duration'; $string['enabletracking'] = 'Enable attempt tracking'; +$string['false'] = 'False'; $string['grade_grademethod'] = 'Grading method'; $string['grade_grademethod_help'] = 'When using point grading, the following methods are available for calculating the final grade: @@ -47,6 +75,7 @@ $string['grade_average_attempt'] = 'Average grade'; $string['grade_last_attempt'] = 'Last attempt'; $string['grade_first_attempt'] = 'First attempt'; $string['h5pactivity:addinstance'] = 'Add a new H5P'; +$string['h5pactivity:reviewattempts'] = 'Review H5P attempts'; $string['h5pactivity:submit'] = 'Submit H5P attempts'; $string['h5pactivity:view'] = 'View H5P'; $string['h5pactivityfieldset'] = 'H5P settings'; @@ -54,6 +83,7 @@ $string['h5pactivityname'] = 'H5P'; $string['h5pactivitysettings'] = 'Settings'; $string['h5pattempts'] = 'Attempt options'; $string['h5pdisplay'] = 'H5P options'; +$string['maxscore'] = 'Max score'; $string['modulename'] = 'H5P'; $string['modulename_help'] = 'H5P is an abbreviation for HTML5 Package - interactive content such as presentations, videos and other multimedia, questions, quizzes, games and more. The H5P activity enables H5P to be uploaded and added to a course. @@ -61,6 +91,10 @@ Any question attempts are marked automatically, and the grade is recorded in the $string['modulename_link'] = 'mod/h5pactivity/view'; $string['modulenameplural'] = 'H5P'; $string['myattempts'] = 'My attempts'; +$string['no_compatible_track'] = 'This interaction ({$a}) does not provide tracking information or the tracking + provided is not compatible with the current activity version.'; +$string['noparticipants'] = 'No participants to display'; +$string['outcome'] = 'Outcome'; $string['package'] = 'Package file'; $string['package_help'] = 'The package file is a h5p file containing H5P interactive content.'; $string['page-mod-h5pactivity-x'] = 'Any H5P module page'; @@ -74,6 +108,27 @@ $string['privacy:metadata:timemodified'] = 'The last time element was tracked'; $string['privacy:metadata:userid'] = 'The ID of the user who accessed the H5P activity'; $string['privacy:metadata:xapi_track'] = 'Attempt tracking information'; $string['privacy:metadata:xapi_track_results'] = 'Attempt results tracking information'; +$string['report_viewed'] = 'Report viewed'; +$string['result_compound'] = 'Combined partial score'; +$string['result_fill-in'] = 'Fill-in text'; +$string['result_fill-in_gap'] = 'Gap #{$a}'; +$string['result_matching'] = 'Matching choice'; +$string['result_other'] = 'Unkown interaction type'; +$string['result_sequencing_choice'] = 'Positions'; +$string['result_sequencing_answer'] = 'Position value'; +$string['result_sequencing_position'] = '#{$a}'; +$string['review_all_attempts'] = 'View all attempts ({$a} submitted)'; +$string['review_mode'] = 'Review attempts'; +$string['review_my_attempts'] = 'View my attempts'; +$string['review_user_attempts'] = 'View user attempts ({$a})'; +$string['review_none'] = 'Participants cannot review their own attempts'; +$string['review_on_completion'] = 'Participants can review their own attempts'; +$string['score'] = 'Score'; +$string['score_out_of'] = '{$a->rawscore} out of {$a->maxscore}'; +$string['startdate'] = 'Start date'; $string['statement_received'] = 'xAPI statement received'; +$string['totalscore'] = 'Total score'; $string['tracking_messages'] = 'Some H5P provide attempt tracking data for advanced reporting such as number of attempts, responses and grades. Note: Some H5P don\'t provide attempt tracking data. In such cases, the following settings will have no effect.'; +$string['true'] = 'True'; $string['view'] = 'View'; +$string['view_report'] = 'View report'; diff --git a/mod/h5pactivity/mod_form.php b/mod/h5pactivity/mod_form.php index d3c20d0dc4b..090a1a33fb2 100644 --- a/mod/h5pactivity/mod_form.php +++ b/mod/h5pactivity/mod_form.php @@ -104,6 +104,11 @@ class mod_h5pactivity_mod_form extends moodleform_mod { $mform->disabledIf('grademethod', 'grade[modgrade_type]', 'neq', 'point'); $mform->addHelpButton('grademethod', 'grade_grademethod', 'mod_h5pactivity'); + $options = manager::get_review_modes(); + $mform->addElement('select', 'reviewmode', get_string('review_mode', 'mod_h5pactivity'), $options); + $mform->setType('reviewmode', PARAM_INT); + $mform->hideIf('reviewmode', 'enabletracking', 'notchecked'); + // Add standard elements. $this->standard_coursemodule_elements(); diff --git a/mod/h5pactivity/report.php b/mod/h5pactivity/report.php new file mode 100644 index 00000000000..e1ed52a8b69 --- /dev/null +++ b/mod/h5pactivity/report.php @@ -0,0 +1,130 @@ +. + +/** + * Prints an instance of mod_h5pactivity. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use mod_h5pactivity\local\manager; +use mod_h5pactivity\event\report_viewed; + +require(__DIR__.'/../../config.php'); +require_once(__DIR__.'/lib.php'); + +$userid = optional_param('userid', null, PARAM_INT); +$attemptid = optional_param('attemptid', null, PARAM_INT); + +// Attempts have only the instance id information but system events +// have only cmid. To prevent unnecesary db queries, this page accept both. +$id = optional_param('id', null, PARAM_INT); +if (empty($id)) { + $a = required_param('a', PARAM_INT); + list ($course, $cm) = get_course_and_cm_from_instance($a, 'h5pactivity'); +} else { + list ($course, $cm) = get_course_and_cm_from_cmid($id, 'h5pactivity'); +} + +require_login($course, true, $cm); + +$manager = manager::create_from_coursemodule($cm); + +$report = $manager->get_report($userid, $attemptid); +if (!$report) { + print_error('permissiondenied'); +} + +$user = $report->get_user(); +$attempt = $report->get_attempt(); + +$moduleinstance = $manager->get_instance(); + +$context = $manager->get_context(); + +$params = ['a' => $cm->instance]; +if ($user) { + $params['userid'] = $user->id; +} +if ($attempt) { + $params['attemptid'] = $attempt->get_id(); +} +$PAGE->set_url('/mod/h5pactivity/report.php', $params); + +// Trigger event. +$other = [ + 'instanceid' => $params['a'], + 'userid' => $params['userid'] ?? null, + 'attemptid' => $params['attemptid'] ?? null, +]; +$event = report_viewed::create([ + 'objectid' => $moduleinstance->id, + 'context' => $context, + 'other' => $other, +]); +$event->add_record_snapshot('course', $course); +$event->add_record_snapshot('h5pactivity', $moduleinstance); +$event->trigger(); + +$shortname = format_string($course->shortname, true, ['context' => $context]); +$pagetitle = strip_tags($shortname.': '.format_string($moduleinstance->name)); +$PAGE->set_title(format_string($pagetitle)); + +$navbar = []; +if ($manager->can_view_all_attempts()) { + // Report navbar have 3 levels for teachers: + // - Participants list + // - Participant attempts list + // - Individual attempt details. + $nav = [get_string('attempts', 'mod_h5pactivity'), null]; + if ($user) { + $nav[1] = new moodle_url('/mod/h5pactivity/report.php', ['a' => $cm->instance]); + $navbar[] = $nav; + + $nav = [fullname($user), null]; + if ($attempt) { + $nav[1] = new moodle_url('/mod/h5pactivity/report.php', ['a' => $cm->instance, 'userid' => $user->id]); + } + } + $navbar[] = $nav; +} else { + // Report navbar have 2 levels for a regular participant: + // - My attempts + // - Individual attempt details. + $nav = [get_string('myattempts', 'mod_h5pactivity'), null]; + if ($attempt) { + $nav[1] = new moodle_url('/mod/h5pactivity/report.php', ['a' => $cm->instance]); + } + $navbar[] = $nav; + +} +if ($attempt) { + $navbar[] = [get_string('attempt_number', 'mod_h5pactivity', $attempt->get_attempt()), null]; +} +foreach ($navbar as $nav) { + $PAGE->navbar->add($nav[0], $nav[1]); +} + +$PAGE->set_heading(format_string($course->fullname)); +$PAGE->set_context($context); + +echo $OUTPUT->header(); + +echo $report->print(); + +echo $OUTPUT->footer(); diff --git a/mod/h5pactivity/templates/attempt.mustache b/mod/h5pactivity/templates/attempt.mustache new file mode 100644 index 00000000000..faa97b39a05 --- /dev/null +++ b/mod/h5pactivity/templates/attempt.mustache @@ -0,0 +1,96 @@ +{{! + 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 . +}} +{{! + @template mod_h5pactivity/attempt + + This template will render an attempt basic information inside a H5P activity results report. + + Variables required for this template: + * timemodified - Full attempts list + * attempt - Full attempts list + * rawscore - Full attempts list + * maxscore - Full attempts list + * duration - Full attempts list + * completionicon - Full attempts list + * successicon - Full attempts list + * reporturl - Full attempts list + + Variables optional for this template: + * user - optional user record + * scored - The scored attempt + + Example context (json): + { + "id": 11, + "h5pactivityid": 1, + "userid": 3, + "timecreated": 1587655101, + "timemodified": 1587655101, + "attempt": 2, + "rawscore": 6, + "maxscore": 6, + "duration": "2 minutes 10 seconds", + "completion": 1, + "completionicon": "<\/i>", + "success": 1, + "successicon": "<\/i>", + "scaled": 1, + "reporturl": {}, + "score": "6 out of 6", + "user": { + "id": "3", + "idnumber": "", + "firstname": "John", + "lastname": "Doe", + "email": "s1@example.com", + "institution": "Moodle HQ", + "department": "Business", + "address": "", + "city": "Barcelona", + "country": "Spain", + "lang": "en", + "picture": "[userpic]", + "fullname": "User Fullname" + } + } + +}} +

+ {{#user}} + {{{picture}}} + {{#str}}attempt, mod_h5pactivity{{/str}} #{{attempt}}: {{fullname}} + {{/user}} + {{^user}} + {{#str}}attempt, mod_h5pactivity{{/str}} #{{attempt}} + {{/user}} +

+
+
+
{{#str}} startdate, mod_h5pactivity {{/str}}
+
{{#userdate}} {{timemodified}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}}
+
{{#str}} completion, mod_h5pactivity {{/str}}
+
{{{completiontext}}}
+
{{#str}} duration, mod_h5pactivity {{/str}}
+
{{duration}}
+
{{#str}} outcome, mod_h5pactivity {{/str}}
+
{{{successtext}}}
+ {{#score}} +
{{#str}} totalscore, mod_h5pactivity {{/str}}
+
{{score}}
+ {{/score}} +
+
diff --git a/mod/h5pactivity/templates/attempts.mustache b/mod/h5pactivity/templates/attempts.mustache new file mode 100644 index 00000000000..2f1f549bd62 --- /dev/null +++ b/mod/h5pactivity/templates/attempts.mustache @@ -0,0 +1,145 @@ +{{! + 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 . +}} +{{! + @template mod_h5pactivity/attempts + + This template will render the report link inside a H5P activity. + + Variables required for this template: + * attempts - Full attempts list with: + * timemodified - Attempt timemodified + * attempt - Attempt number + * rawscore - Attempt rawscore + * maxscore - Attempt maxscore + * duration - Attempt duration + * completionicon - Completion icon HTML + * successicon - Success icon HTML + * reporturl - Attempt report URL + + Variables optional for this template: + * title - optional selected attempt name + * scored - The scored attempt + + Example context (json): + { + "attempts": [ + { + "id": 7, + "h5pactivityid": 1, + "userid": 5, + "timecreated": 1587654916, + "timemodified": 1587654916, + "attempt": 1, + "rawscore": 6, + "maxscore": 6, + "duration": "14 seconds", + "durationcompact": "14''", + "completion": 1, + "completionicon": "<\/i>", + "success": 1, + "successicon": "<\/i>", + "scaled": 1, + "reporturl": {}, + "score": "6 out of 6" + }, + { + "id": 8, + "h5pactivityid": 1, + "userid": 5, + "timecreated": 1587654927, + "timemodified": 1587654927, + "attempt": 2, + "rawscore": 1, + "maxscore": 6, + "duration": "25 seconds", + "durationcompact": "25''", + "completion": 1, + "completionicon": "<\/i>", + "success": 0, + "successicon": "<\/i>", + "scaled": 0, + "reporturl": {}, + "score": "1 out of 6" + }, + { + "id": 9, + "h5pactivityid": 1, + "userid": 5, + "timecreated": 1587654942, + "timemodified": 1587654942, + "attempt": 3, + "rawscore": 3, + "maxscore": 6, + "duration": "40 seconds", + "durationcompact": "40''", + "completion": 1, + "completionicon": "<\/i>", + "success": 0, + "successicon": "<\/i>", + "scaled": 0, + "reporturl": {}, + "score": "3 out of 6" + } + ] + } + +}} + + + + + + + + + + + + + + + {{#attempts}} + + + + + + + + + + + {{/attempts}} + +
#{{#str}} date {{/str}}{{#str}} score, mod_h5pactivity {{/str}}{{#str}} maxscore, mod_h5pactivity {{/str}}{{#str}} duration, mod_h5pactivity {{/str}}{{#str}} completion, mod_h5pactivity {{/str}}{{#str}} success {{/str}}{{#str}} report {{/str}}
{{attempt}} + + {{#userdate}} {{timemodified}}, {{#str}} strftimedatetime, core_langconfig {{/str}} {{/userdate}} + + + {{#userdate}} {{timemodified}}, {{#str}} strftimedatetimeshort, core_langconfig {{/str}} {{/userdate}} + + {{rawscore}} / {{maxscore}}{{maxscore}} + {{duration}} + {{durationcompact}} + {{{completionicon}}}{{{successicon}}} + + {{#str}} view_report, mod_h5pactivity {{/str}} + + + {{#str}} view {{/str}} + +
diff --git a/mod/h5pactivity/templates/reportattempts.mustache b/mod/h5pactivity/templates/reportattempts.mustache new file mode 100644 index 00000000000..7dd697b95dd --- /dev/null +++ b/mod/h5pactivity/templates/reportattempts.mustache @@ -0,0 +1,156 @@ +{{! + 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 . +}} +{{! + @template mod_h5pactivity/reportattempts + + This template will render the report link inside a H5P activity. + + Variables required for this template: + * attempts - Full attempts list + + Variables optional for this template: + * title - optional selected attempt name + * scored - The scored attempt + * attemptscount - The current number of attempts + + Example context (json): + { + "attempts": [ + { + "id": 7, + "h5pactivityid": 1, + "userid": 5, + "timecreated": 1587654916, + "timemodified": 1587654916, + "attempt": 1, + "rawscore": 6, + "maxscore": 6, + "duration": "14 seconds", + "durationcompact": "14''", + "completion": 1, + "completionicon": "<\/i>", + "success": 1, + "successicon": "<\/i>", + "scaled": 1, + "reporturl": {}, + "score": "6 out of 6" + }, + { + "id": 8, + "h5pactivityid": 1, + "userid": 5, + "timecreated": 1587654927, + "timemodified": 1587654927, + "attempt": 2, + "rawscore": 1, + "maxscore": 6, + "duration": "25 seconds", + "durationcompact": "25''", + "completion": 1, + "completionicon": "<\/i>", + "success": 0, + "successicon": "<\/i>", + "scaled": 0, + "reporturl": {}, + "score": "1 out of 6" + }, + { + "id": 9, + "h5pactivityid": 1, + "userid": 5, + "timecreated": 1587654942, + "timemodified": 1587654942, + "attempt": 3, + "rawscore": 3, + "maxscore": 6, + "duration": "40 seconds", + "durationcompact": "40''", + "completion": 1, + "completionicon": "<\/i>", + "success": 0, + "successicon": "<\/i>", + "scaled": 0, + "reporturl": {}, + "score": "3 out of 6" + } + ], + "user": { + "id": "5", + "auth": "manual", + "idnumber": "", + "firstname": "Miguel", + "lastname": "Alonso", + "email": "s3@example.com", + "institution": "Moodle HQ", + "department": "Development", + "address": "", + "city": "Barcelona", + "country": "ES", + "lang": "en", + "picture": "[USERPIC]", + "fullname": "Miguel Alonso" + }, + "scored": { + "title": "Highest score attempt", + "attempts": [ + { + "id": 7, + "h5pactivityid": 1, + "userid": 5, + "timecreated": 1587654916, + "timemodified": 1587654916, + "attempt": 1, + "rawscore": 6, + "maxscore": 6, + "duration": "14 seconds", + "durationcompact": "14''", + "completion": 1, + "completionicon": "<\/i>", + "success": 1, + "successicon": "<\/i>", + "scaled": 1, + "reporturl": {}, + "score": "6 out of 6" + } + ] + } + } + +}} +{{#user}} +

+ {{{picture}}} + {{#title}}{{title}}{{/title}} + {{^title}}{{#str}} attempts, mod_h5pactivity {{/str}}: {{fullname}}{{/title}} +

+{{/user}} + +{{#scored}} +

{{title}}

+ {{>mod_h5pactivity/attempts}} +{{/scored}} + +

{{#str}}all_attempts, mod_h5pactivity{{/str}}

+ +{{^attemptscount}} +
+ {{#str}} attempts_none, mod_h5pactivity {{/str}} +
+{{/attemptscount}} +{{#attemptscount}} + {{>mod_h5pactivity/attempts}} +{{/attemptscount}} diff --git a/mod/h5pactivity/templates/reportlink.mustache b/mod/h5pactivity/templates/reportlink.mustache new file mode 100644 index 00000000000..14fd2ea0560 --- /dev/null +++ b/mod/h5pactivity/templates/reportlink.mustache @@ -0,0 +1,35 @@ +{{! + 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 . +}} +{{! + @template mod_h5pactivity/reportlink + + This template will render the report link inside a H5P activity. + + Variables required for this template: + * url - The URL to the report page + * message - The link message + + Example context (json): + { + "url": "#", + "message": "View attempts list (3 submitted)" + } + +}} + diff --git a/mod/h5pactivity/templates/reportresults.mustache b/mod/h5pactivity/templates/reportresults.mustache new file mode 100644 index 00000000000..167a7af1962 --- /dev/null +++ b/mod/h5pactivity/templates/reportresults.mustache @@ -0,0 +1,85 @@ +{{! + 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 . +}} +{{! + @template mod_h5pactivity/reportresults + + This template will render the report results inside a H5P activity. + + Variables required for this template: + * attempts - Full attempts list + + Variables optional for this template: + * title - optional selected attempt name + * scored - The scored attempt + + Example context (json): + { + "attempt": { + "id": 11, + "h5pactivityid": 1, + "userid": 3, + "timecreated": 1587655101, + "timemodified": 1587655101, + "attempt": 2, + "rawscore": 6, + "maxscore": 6, + "duration": "2 minutes 10 seconds", + "completion": 1, + "completionicon": "<\/i>", + "success": 1, + "successicon": "<\/i>", + "scaled": 1, + "reporturl": {}, + "score": "6 out of 6", + "user": { + "id": "3", + "username": "s1", + "idnumber": "", + "firstname": "John", + "lastname": "Doe", + "email": "s1@example.com", + "phone2": "", + "institution": "Moodle HQ", + "department": "Business", + "picture": "[USERPIC]", + "fullname": "John Doe" + } + }, + "results": [ + { + "timecreated": "1587655101", + "interactiontype": "other", + "description": "Example of some results", + "rawscore": "6", + "maxscore": "6", + "duration": "130", + "completion": "1", + "success": "1", + "optionslabel": "Example result", + "score": "6 out of 6", + "content": "

See mod_h5pactivity result template for more examples.

" + } + ] + } + +}} +{{#attempt}} + {{>mod_h5pactivity/attempt}} +{{/attempt}} +{{#results}} + {{>mod_h5pactivity/result}} +{{/results}} diff --git a/mod/h5pactivity/templates/result.mustache b/mod/h5pactivity/templates/result.mustache new file mode 100644 index 00000000000..4309275433c --- /dev/null +++ b/mod/h5pactivity/templates/result.mustache @@ -0,0 +1,119 @@ +{{! + 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 . +}} +{{! + @template mod_h5pactivity/result + + This template will render a result choices inside a H5P activity results report. + + Variables required for this template: + * description - Result description text + * score - Score string (for example: 2 out of 4) + + Variables optional for this template: + * hasoptions - If an option table must be present + * optionslabel - The right label for available options on this result type + * options - An array of mod_h5pactivity/result/options compatible array + * content - Extra content in HTML + * track - Indicate if the result has displayable tracking + + Example context (json): + { + "timecreated": "1587655101", + "interactiontype": "choice", + "description": "Example of some results", + "rawscore": "4", + "maxscore": "4", + "duration": "130", + "completion": "1", + "success": "1", + "hasoptions": true, + "optionslabel": "Choice", + "options": [ + { + "description": "Choice 1 text", + "id": 0, + "useranswer": { + "answer": "Correct answer", + "pass": true + }, + "correctanswer": { + "answer": "Answer checked", + "checked": true + } + }, + { + "description": "Choice 2 text", + "id": 1, + "useranswer": { + "answer": "Wrong answer", + "fail": true + }, + "correctanswer": { + "answer": "Answer checked", + "unchecked": true + } + }, + { + "description": "Choice 3 text", + "id": 2, + "useranswer": { + "answer": "This was the correct text", + "correct": true + }, + "correctanswer": { + "answer": "This was the correct text", + "text": true + } + }, + { + "description": "Choice 4 text", + "id": 3, + "correctanswer": { + "answer": "Some text", + "text": true + } + }, + { + "description": "Choice 4 text", + "id": 3, + "useranswer": { + "answer": "Some wrong text", + "incorrect": true + } + } + ], + "score": "4 out of 4", + "content": "

This is an optional extra content in HTML.

", + "track": true + } + +}} + +
+
+ {{>mod_h5pactivity/result/header}} + {{{content}}} + {{#hasoptions}} + {{>mod_h5pactivity/result/options}} + {{/hasoptions}} + {{^track}} + + {{/track}} +
+
diff --git a/mod/h5pactivity/templates/result/answer.mustache b/mod/h5pactivity/templates/result/answer.mustache new file mode 100644 index 00000000000..c6ba3e12891 --- /dev/null +++ b/mod/h5pactivity/templates/result/answer.mustache @@ -0,0 +1,66 @@ +{{! + 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 . +}} +{{! + @template mod_h5pactivity/result/answer + + This template render all kind of answers/choice in a results table. + + Variables required for this template: + * answer - The answer/choice text + + Variables optional for this template: + * pass - The answer pass + * fail - The answer failed + * correct - The answer is correct + * incorrect - The answer is not correct + * text - The choice is just text + * checked - The choice must be checked + * unchecked - The choice must be unchecked + + Example context (json): + { + "answer": "This was the ansewer,", + "correct": true, + "incorrect": true, + "text": true, + "checked": true, + "unchecked": true, + "pass": true, + "fail": true + } + +}} +{{#correct}} + {{#pix}}i/valid, moodle, {{#str}}answer_correct, mod_h5pactivity{{/str}}{{/pix}}{{answer}} +{{/correct}} +{{#incorrect}} + {{#pix}}i/invalid, moodle, {{#str}}answer_incorrect, mod_h5pactivity{{/str}}{{/pix}}{{answer}} +{{/incorrect}} +{{#text}} + {{answer}} +{{/text}} +{{#checked}} + {{#pix}}i/checkedcircle, moodle, {{#str}}answer_checked, mod_h5pactivity{{/str}}{{/pix}} +{{/checked}} +{{#unchecked}} +{{/unchecked}} +{{#pass}} + {{#pix}}i/valid, moodle, {{#str}}answer_pass, mod_h5pactivity{{/str}}{{/pix}} +{{/pass}} +{{#fail}} + {{#pix}}i/invalid, moodle, {{#str}}answer_fail, mod_h5pactivity{{/str}}{{/pix}} +{{/fail}} diff --git a/mod/h5pactivity/templates/result/header.mustache b/mod/h5pactivity/templates/result/header.mustache new file mode 100644 index 00000000000..a5592daa471 --- /dev/null +++ b/mod/h5pactivity/templates/result/header.mustache @@ -0,0 +1,44 @@ +{{! + 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 . +}} +{{! + @template mod_h5pactivity/result/header + + This template will render a results header inside mod_h5pactivity results report. + + Variables required for this template: + * description - Result description + + Variables optional for this template: + * success - If the result is marked as success + * score - The result score string + + Example context (json): + { + "timecreated": "1587655101", + "interactiontype": "choice", + "description": "Example of some results\n", + "rawscore": "4", + "maxscore": "4", + "duration": "130", + "success": "1", + "score": "4 out of 4" + } + +}} +

+ {{description}} +

diff --git a/mod/h5pactivity/templates/result/options.mustache b/mod/h5pactivity/templates/result/options.mustache new file mode 100644 index 00000000000..4be6d11dd4b --- /dev/null +++ b/mod/h5pactivity/templates/result/options.mustache @@ -0,0 +1,118 @@ +{{! + 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 . +}} +{{! + @template mod_h5pactivity/result/options + + This template will render a choices table inside a H5P activity results report. + + Variables required for this template: + * options - An array of options + * optionslabel - The right label for options column + * correctlabel - The right label for correct answer column + * answerlabel - The right label for the attempt answer column + + Example context (json): + { + "optionslabel": "Choice", + "correctlabel": "Correct answer", + "answerlabel": "Attempt answer", + "options": [ + { + "description": "Choice 1 text", + "id": 0, + "useranswer": { + "answer": "Correct answer", + "pass": true + }, + "correctanswer": { + "answer": "Answer checked", + "checked": true + } + }, + { + "description": "Choice 2 text", + "id": 1, + "useranswer": { + "answer": "Wrong answer", + "fail": true + }, + "correctanswer": { + "answer": "Answer checked", + "unchecked": true + } + }, + { + "description": "Choice 3 text", + "id": 2, + "useranswer": { + "answer": "This was the correct text", + "correct": true + }, + "correctanswer": { + "answer": "This was the correct text", + "text": true + } + }, + { + "description": "Choice 4 text", + "id": 3, + "correctanswer": { + "answer": "Some text", + "text": true + } + }, + { + "description": "Choice 4 text", + "id": 3, + "useranswer": { + "answer": "Some wrong text", + "incorrect": true + } + } + ] + } + +}} + + + + + + + + + + {{#options}} + + + + + + {{/options}} + {{#score}} + + + + + + {{/score}} + +
{{optionslabel}}{{correctlabel}}{{answerlabel}}
{{description}}{{#correctanswer}}{{>mod_h5pactivity/result/answer}}{{/correctanswer}}{{#useranswer}}{{>mod_h5pactivity/result/answer}}{{/useranswer}}
+ {{#str}}score, mod_h5pactivity{{/str}}: {{score}} + + {{#str}}score, mod_h5pactivity{{/str}}: {{score}} +
diff --git a/mod/h5pactivity/tests/event/report_viewed_test.php b/mod/h5pactivity/tests/event/report_viewed_test.php new file mode 100644 index 00000000000..342580bc424 --- /dev/null +++ b/mod/h5pactivity/tests/event/report_viewed_test.php @@ -0,0 +1,147 @@ +. + +/** + * Events test. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_h5pactivity\event; + +use advanced_testcase; +use moodle_url; +use coding_exception; +use context_module; + +defined('MOODLE_INTERNAL') || die(); + +/** + * H5P activity events test cases. + * + * @package mod_h5pactivity + * @copyright 2020 Ferran Recio + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class report_viewed_testcase extends advanced_testcase { + + /** + * Test report_viewed event. + * + * @dataProvider report_viewed_data + * @param bool $usea if a (instanceid) will be used in the event + * @param bool $useattemptid if attemptid will be used in the event + * @param bool $useuserid if user id will be used in the event + * @param bool $exception if exception is expected + */ + public function test_report_viewed(bool $usea, bool $useattemptid, bool $useuserid, bool $exception) { + + $this->resetAfterTest(); + + // Must be a non-guest user to create h5pactivities. + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course->id]); + + $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); + + // Create a user with 1 attempt. + $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $params = ['cmid' => $activity->cmid, 'userid' => $user->id]; + $attempt = $generator->create_content($activity, $params); + + $other = []; + $urlparams = []; + if ($usea) { + $other['instanceid'] = $activity->id; + $urlparams['a'] = $activity->id; + } + if ($useuserid) { + $other['userid'] = $user->id; + $urlparams['userid'] = $user->id; + } + if ($useattemptid) { + $other['attemptid'] = $attempt->id; + $urlparams['attemptid'] = $attempt->id; + } + $params = [ + 'context' => context_module::instance($activity->cmid), + 'objectid' => $activity->id, + 'other' => $other, + ]; + + if ($exception) { + $this->expectException(coding_exception::class); + } + + $event = report_viewed::create($params); + + // Triggering and capturing the event. + $sink = $this->redirectEvents(); + $event->trigger(); + $events = $sink->get_events(); + $this->assertCount(1, $events); + $event = reset($events); + + // Checking that the event contains the expected values. + $this->assertInstanceOf('\mod_h5pactivity\event\report_viewed', $event); + $this->assertEquals(context_module::instance($activity->cmid), $event->get_context()); + $this->assertEquals($activity->id, $event->objectid); + + $eventurl = $event->get_url(); + $url = new moodle_url('/mod/h5pactivity/report.php', $urlparams); + $this->assertTrue($eventurl->compare($url)); + } + + /** + * Data provider for data request creation tests. + * + * @return array + */ + public function report_viewed_data(): array { + return [ + // Exception cases. + 'Event withour other data (exception)' => [ + false, false, false, true + ], + 'Event with only userid (exception)' => [ + false, false, true, true + ], + 'Event with only attemptid (exception)' => [ + false, true, false, true + ], + 'Event with attemptid and userid (exception)' => [ + false, true, true, true + ], + // Correct cases. + 'Event with instance id' => [ + true, false, false, false + ], + 'Event with instance id and attempt id' => [ + true, false, true, false + ], + 'Event with instance id and userid' => [ + true, true, false, false + ], + 'Event with instance id, user id and attemptid' => [ + true, true, true, false + ], + ]; + } +} diff --git a/mod/h5pactivity/tests/event/statement_received_test.php b/mod/h5pactivity/tests/event/statement_received_test.php index 021043d6127..b4eaa7034fd 100644 --- a/mod/h5pactivity/tests/event/statement_received_test.php +++ b/mod/h5pactivity/tests/event/statement_received_test.php @@ -25,7 +25,6 @@ namespace mod_h5pactivity\event; use advanced_testcase; -use context_course; use context_module; /** @@ -38,7 +37,7 @@ use context_module; class statement_received_testcase extends advanced_testcase { /** - * Test course_module_viewed event. + * Test statement_recieved event. */ public function test_statement_received() { global $USER; diff --git a/mod/h5pactivity/tests/generator/lib.php b/mod/h5pactivity/tests/generator/lib.php index b12ffad212d..a9721632dc7 100644 --- a/mod/h5pactivity/tests/generator/lib.php +++ b/mod/h5pactivity/tests/generator/lib.php @@ -71,6 +71,9 @@ class mod_h5pactivity_generator extends testing_module_generator { if (!isset($record->grademethod)) { $record->grademethod = manager::GRADEHIGHESTATTEMPT; } + if (!isset($record->reviewmode)) { + $record->reviewmode = manager::REVIEWCOMPLETION; + } // The 'packagefile' value corresponds to the draft file area ID. If not specified, create from packagefilepath. if (empty($record->packagefile)) { diff --git a/mod/h5pactivity/tests/generator_test.php b/mod/h5pactivity/tests/generator_test.php index dbb7cfa3dfc..0fd5cab3e5e 100644 --- a/mod/h5pactivity/tests/generator_test.php +++ b/mod/h5pactivity/tests/generator_test.php @@ -63,6 +63,7 @@ class mod_h5pactivity_generator_testcase extends advanced_testcase { $this->assertEquals(6, $activity->displayoptions); $this->assertEquals(0, $activity->enabletracking); $this->assertEquals(manager::GRADELASTATTEMPT, $activity->grademethod); + $this->assertEquals(manager::REVIEWCOMPLETION, $activity->reviewmode); $this->assertEquals(2, count($records)); $this->assertEquals('Another h5pactivity', $records[$activity->id]->name); diff --git a/mod/h5pactivity/tests/local/attempt_test.php b/mod/h5pactivity/tests/local/attempt_test.php index 6cec3c2b067..2ba93798458 100644 --- a/mod/h5pactivity/tests/local/attempt_test.php +++ b/mod/h5pactivity/tests/local/attempt_test.php @@ -276,9 +276,9 @@ class attempt_testcase extends \advanced_testcase { attempt::delete_all_attempts($cm, $user); // Check data. - for ($i = 0; $i < 4; $i++) { - $count = $attempts[$i]->count_results(); - $this->assertEquals($results[$i], $count); + for ($assert = 0; $assert < 4; $assert++) { + $count = $attempts[$assert]->count_results(); + $this->assertEquals($results[$assert], $count); } $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals($results[4], $count); diff --git a/mod/h5pactivity/tests/local/manager_test.php b/mod/h5pactivity/tests/local/manager_test.php index e1b63f84740..ff2e8b6149f 100644 --- a/mod/h5pactivity/tests/local/manager_test.php +++ b/mod/h5pactivity/tests/local/manager_test.php @@ -264,6 +264,80 @@ class manager_testcase extends \advanced_testcase { $this->assertNotEmpty($methods[manager::GRADEMANUAL]); } + /** + * Test static get_selected_attempt. + * + * @dataProvider get_selected_attempt_data + * @param int $enabletracking if tracking is enabled + * @param int $gradingmethod new grading method + * @param int $result the expected result + */ + public function test_get_selected_attempt(int $enabletracking, int $gradingmethod, int $result) { + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module('h5pactivity', + ['course' => $course, 'enabletracking' => $enabletracking, 'grademethod' => $gradingmethod]); + + $manager = manager::create_from_instance($activity); + + $selected = $manager->get_selected_attempt(); + + $this->assertEquals($result, $selected[0]); + $this->assertNotEmpty($selected[1]); + } + + /** + * Data provider for get_users_scaled_score. + * + * @return array + */ + public function get_selected_attempt_data(): array { + return [ + 'Tracking with max attempt method' => [ + 1, manager::GRADEHIGHESTATTEMPT, manager::GRADEHIGHESTATTEMPT + ], + 'Tracking with average attempt method' => [ + 1, manager::GRADEAVERAGEATTEMPT, manager::GRADEAVERAGEATTEMPT + ], + 'Tracking with last attempt method' => [ + 1, manager::GRADELASTATTEMPT, manager::GRADELASTATTEMPT + ], + 'Tracking with first attempt method' => [ + 1, manager::GRADEFIRSTATTEMPT, manager::GRADEFIRSTATTEMPT + ], + 'Tracking with manual attempt grading' => [ + 1, manager::GRADEMANUAL, manager::GRADEMANUAL + ], + 'No tracking with max attempt method' => [ + 0, manager::GRADEHIGHESTATTEMPT, manager::GRADEMANUAL + ], + 'No tracking with average attempt method' => [ + 0, manager::GRADEAVERAGEATTEMPT, manager::GRADEMANUAL + ], + 'No tracking with last attempt method' => [ + 0, manager::GRADELASTATTEMPT, manager::GRADEMANUAL + ], + 'No tracking with first attempt method' => [ + 0, manager::GRADEFIRSTATTEMPT, manager::GRADEMANUAL + ], + 'No tracking with manual attempt grading' => [ + 0, manager::GRADEMANUAL, manager::GRADEMANUAL + ], + ]; + } + + /** + * Test static get_review_modes. + */ + public function test_get_review_modes() { + $methods = manager::get_review_modes(); + $this->assertCount(2, $methods); + $this->assertNotEmpty($methods[manager::REVIEWCOMPLETION]); + $this->assertNotEmpty($methods[manager::REVIEWNONE]); + } + /** * Test get_grader method. */ @@ -282,6 +356,388 @@ class manager_testcase extends \advanced_testcase { $this->assertInstanceOf('mod_h5pactivity\local\grader', $grader); } + + /** + * Test static can_view_all_attempts. + * + * @dataProvider can_view_all_attempts_data + * @param int $enabletracking if tracking is enabled + * @param bool $usestudent if test must be done with a user role + * @param bool $useloggedin if test must be done with the loggedin user + * @param bool $result the expected result + */ + public function test_can_view_all_attempts(int $enabletracking, bool $usestudent, bool $useloggedin, bool $result) { + global $USER; + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module('h5pactivity', + ['course' => $course, 'enabletracking' => $enabletracking]); + + $manager = manager::create_from_instance($activity); + + $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $loggedin = $USER; + + // We want to test what when the method is called to check a different user than $USER. + if (!$usestudent) { + $loggedin = $user; + $user = $USER; + } + + if ($useloggedin) { + $this->setUser($user); + $user = null; + } else { + $this->setUser($loggedin); + } + + $this->assertEquals($result, $manager->can_view_all_attempts($user)); + } + + /** + * Data provider for test_can_view_all_attempts. + * + * @return array + */ + public function can_view_all_attempts_data(): array { + return [ + // No tracking cases. + 'No tracking with admin using $USER' => [ + 0, false, false, false + ], + 'No tracking with student using $USER' => [ + 0, true, false, false + ], + 'No tracking with admin loggedin' => [ + 0, false, true, false + ], + 'No tracking with student loggedin' => [ + 0, true, true, false + ], + // Tracking enabled cases. + 'Tracking with admin using $USER' => [ + 1, false, false, true + ], + 'Tracking with student using $USER' => [ + 1, true, false, false + ], + 'Tracking with admin loggedin' => [ + 1, false, true, true + ], + 'Tracking with student loggedin' => [ + 1, true, true, false + ], + ]; + } + + /** + * Test static can_view_own_attempts. + * + * @dataProvider can_view_own_attempts_data + * @param int $enabletracking if tracking is enabled + * @param int $reviewmode the attempt review mode + * @param bool $useloggedin if test must be done with the loggedin user + * @param bool $hasattempts if the student have attempts + * @param bool $result the expected result + */ + public function test_can_view_own_attempts(int $enabletracking, int $reviewmode, + bool $useloggedin, bool $hasattempts, bool $result) { + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module('h5pactivity', + ['course' => $course, 'enabletracking' => $enabletracking, 'reviewmode' => $reviewmode]); + + $manager = manager::create_from_instance($activity); + + $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + if ($hasattempts) { + $this->generate_fake_attempts($activity, $user, 1); + } + + if ($useloggedin) { + $this->setUser($user); + $user = null; + } + + $this->assertEquals($result, $manager->can_view_own_attempts($user)); + } + + /** + * Data provider for test_can_view_own_attempts. + * + * @return array + */ + public function can_view_own_attempts_data(): array { + return [ + // No tracking cases. + 'No tracking, review none, using $USER, without attempts' => [ + 0, manager::REVIEWNONE, false, false, false + ], + 'No tracking, review enabled, using $USER, without attempts' => [ + 0, manager::REVIEWCOMPLETION, false, false, false + ], + 'No tracking, review none, loggedin, without attempts' => [ + 0, manager::REVIEWNONE, true, false, false + ], + 'No tracking, review enabled, loggedin, without attempts' => [ + 0, manager::REVIEWCOMPLETION, true, false, false + ], + 'No tracking, review none, using $USER, with attempts' => [ + 0, manager::REVIEWNONE, false, true, false + ], + 'No tracking, review enabled, using $USER, with attempts' => [ + 0, manager::REVIEWCOMPLETION, false, true, false + ], + 'No tracking, review none, loggedin, with attempts' => [ + 0, manager::REVIEWNONE, true, true, false + ], + 'No tracking, review enabled, loggedin, with attempts' => [ + 0, manager::REVIEWCOMPLETION, true, true, false + ], + // Tracking enabled cases. + 'Tracking enabled, review none, using $USER, without attempts' => [ + 1, manager::REVIEWNONE, false, false, false + ], + 'Tracking enabled, review enabled, using $USER, without attempts' => [ + 1, manager::REVIEWCOMPLETION, false, false, true + ], + 'Tracking enabled, review none, loggedin, without attempts' => [ + 1, manager::REVIEWNONE, true, false, false + ], + 'Tracking enabled, review enabled, loggedin, without attempts' => [ + 1, manager::REVIEWCOMPLETION, true, false, true + ], + 'Tracking enabled, review none, using $USER, with attempts' => [ + 1, manager::REVIEWNONE, false, true, false + ], + 'Tracking enabled, review enabled, using $USER, with attempts' => [ + 1, manager::REVIEWCOMPLETION, false, true, true + ], + 'Tracking enabled, review none, loggedin, with attempts' => [ + 1, manager::REVIEWNONE, true, true, false + ], + 'Tracking enabled, review enabled, loggedin, with attempts' => [ + 1, manager::REVIEWCOMPLETION, true, true, true + ], + ]; + } + + /** + * Test static count_attempts. + */ + public function test_count_attempts() { + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module('h5pactivity', + ['course' => $course]); + + $manager = manager::create_from_instance($activity); + + // User without attempts. + $user1 = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + // User with 1 attempt. + $user2 = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $this->generate_fake_attempts($activity, $user2, 1); + + // User with 2 attempts. + $user3 = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $this->generate_fake_attempts($activity, $user3, 1); + + // Incomplete user2 and 3 has only 3 attempts completed. + $this->assertEquals(0, $manager->count_attempts($user1->id)); + $this->assertEquals(3, $manager->count_attempts($user2->id)); + $this->assertEquals(3, $manager->count_attempts($user3->id)); + } + + /** + * Test static count_attempts. + */ + public function test_count_users_attempts() { + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module('h5pactivity', + ['course' => $course]); + + $manager = manager::create_from_instance($activity); + + // User without attempts. + $user1 = $this->getDataGenerator()->create_and_enrol($course, 'student'); + + // User with 1 attempt. + $user2 = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $this->generate_fake_attempts($activity, $user2, 1); + + // User with 2 attempts. + $user3 = $this->getDataGenerator()->create_and_enrol($course, 'student'); + $this->generate_fake_attempts($activity, $user3, 1); + + $attempts = $manager->count_users_attempts(); + $this->assertArrayNotHasKey($user1->id, $attempts); + $this->assertArrayHasKey($user2->id, $attempts); + $this->assertEquals(4, $attempts[$user2->id]); + $this->assertArrayHasKey($user3->id, $attempts); + $this->assertEquals(4, $attempts[$user3->id]); + } + + /** + * Test static get_report. + * + * @dataProvider get_report_data + * @param int $enabletracking if tracking is enabled + * @param int $reviewmode the attempt review mode + * @param bool $createattempts if the student have attempts + * @param string $role the user role (student or editingteacher) + * @param array $results the expected classname (or null) + */ + public function test_get_report(int $enabletracking, int $reviewmode, bool $createattempts, + string $role, array $results) { + + $this->resetAfterTest(); + $this->setAdminUser(); + + $course = $this->getDataGenerator()->create_course(); + $activity = $this->getDataGenerator()->create_module('h5pactivity', + ['course' => $course, 'enabletracking' => $enabletracking, 'reviewmode' => $reviewmode]); + + $manager = manager::create_from_instance($activity); + $cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST); + + $users = [ + 'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'), + 'student' => $this->getDataGenerator()->create_and_enrol($course, 'student'), + 'otheruser' => $this->getDataGenerator()->create_and_enrol($course, 'student'), + ]; + + $attempts = []; + if ($createattempts) { + $this->generate_fake_attempts($activity, $users['student'], 1); + $this->generate_fake_attempts($activity, $users['otheruser'], 2); + $attempts['student'] = attempt::last_attempt($users['student'], $cm); + $attempts['otheruser'] = attempt::last_attempt($users['otheruser'], $cm); + } + + $classnamebase = 'mod_h5pactivity\\local\\report\\'; + + $attemptid = null; + if (isset($attempts['student'])) { + $attemptid = $attempts['student']->get_id() ?? null; + } + $userid = $users['student']->id; + + // Check reports. + $this->setUser($users[$role]); + + $report = $manager->get_report(null, null); + if ($results[0] === null) { + $this->assertNull($report); + } else { + $this->assertEquals($classnamebase.$results[0], get_class($report)); + } + + $report = $manager->get_report($userid, null); + if ($results[1] === null) { + $this->assertNull($report); + } else { + $this->assertEquals($classnamebase.$results[1], get_class($report)); + } + + $report = $manager->get_report($userid, $attemptid); + if ($results[2] === null) { + $this->assertNull($report); + } else { + $this->assertEquals($classnamebase.$results[2], get_class($report)); + } + + // Check that student cannot access another student reports. + if ($role == 'student') { + $attemptid = null; + if (isset($attempts['otheruser'])) { + $attemptid = $attempts['otheruser']->get_id() ?? null; + } + $userid = $users['otheruser']->id; + + $report = $manager->get_report($userid, null); + $this->assertNull($report); + + $report = $manager->get_report($userid, $attemptid); + $this->assertNull($report); + } + } + + /** + * Data provider for test_get_report. + * + * @return array + */ + public function get_report_data(): array { + return [ + // No tracking scenarios. + 'No tracking, review none, no attempts, teacher' => [ + 0, manager::REVIEWNONE, false, 'editingteacher', [null, null, null] + ], + 'No tracking, review own, no attempts, teacher' => [ + 0, manager::REVIEWCOMPLETION, false, 'editingteacher', [null, null, null] + ], + 'No tracking, review none, no attempts, student' => [ + 0, manager::REVIEWNONE, false, 'student', [null, null, null] + ], + 'No tracking, review own, no attempts, student' => [ + 0, manager::REVIEWCOMPLETION, false, 'student', [null, null, null] + ], + 'No tracking, review none, with attempts, teacher' => [ + 0, manager::REVIEWNONE, true, 'editingteacher', [null, null, null] + ], + 'No tracking, review own, with attempts, teacher' => [ + 0, manager::REVIEWCOMPLETION, true, 'editingteacher', [null, null, null] + ], + 'No tracking, review none, with attempts, student' => [ + 0, manager::REVIEWNONE, true, 'student', [null, null, null] + ], + 'No tracking, review own, with attempts, student' => [ + 0, manager::REVIEWCOMPLETION, true, 'student', [null, null, null] + ], + // Tracking enabled scenarios. + 'Tracking enabled, review none, no attempts, teacher' => [ + 1, manager::REVIEWNONE, false, 'editingteacher', ['participants', 'attempts', 'attempts'] + ], + 'Tracking enabled, review own, no attempts, teacher' => [ + 1, manager::REVIEWCOMPLETION, false, 'editingteacher', ['participants', 'attempts', 'attempts'] + ], + 'Tracking enabled, review none, no attempts, student' => [ + 1, manager::REVIEWNONE, false, 'student', [null, null, null] + ], + 'Tracking enabled, review own, no attempts, student' => [ + 1, manager::REVIEWCOMPLETION, false, 'student', ['attempts', 'attempts', 'attempts'] + ], + 'Tracking enabled, review none, with attempts, teacher' => [ + 1, manager::REVIEWNONE, true, 'editingteacher', ['participants', 'attempts', 'results'] + ], + 'Tracking enabled, review own, with attempts, teacher' => [ + 1, manager::REVIEWCOMPLETION, true, 'editingteacher', ['participants', 'attempts', 'results'] + ], + 'Tracking enabled, review none, with attempts, student' => [ + 1, manager::REVIEWNONE, true, 'student', [null, null, null] + ], + 'Tracking enabled, review own, with attempts, student' => [ + 1, manager::REVIEWCOMPLETION, true, 'student', ['attempts', 'attempts', 'results'] + ], + ]; + } + /** * Insert fake attempt data into h5pactiviyt_attempts. * diff --git a/mod/h5pactivity/version.php b/mod/h5pactivity/version.php index 2d8caf89b67..ad71475886a 100644 --- a/mod/h5pactivity/version.php +++ b/mod/h5pactivity/version.php @@ -25,5 +25,5 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_h5pactivity'; -$plugin->version = 2020041401; +$plugin->version = 2020042202; $plugin->requires = 2020013000; diff --git a/mod/h5pactivity/view.php b/mod/h5pactivity/view.php index 7a47edb3839..67bdc505bcd 100644 --- a/mod/h5pactivity/view.php +++ b/mod/h5pactivity/view.php @@ -81,6 +81,19 @@ $PAGE->set_context($context); echo $OUTPUT->header(); echo $OUTPUT->heading(format_string($moduleinstance->name)); +// Attempts review. +if ($manager->can_view_all_attempts()) { + $reviewurl = new moodle_url('report.php', ['a' => $cm->instance]); + $reviewmessage = get_string('review_all_attempts', 'mod_h5pactivity', $manager->count_attempts()); +} else if ($manager->can_view_own_attempts() && $manager->count_attempts($USER->id)) { + $reviewurl = new moodle_url('report.php', ['a' => $cm->instance, 'userid' => $USER->id]); + $reviewmessage = get_string('review_my_attempts', 'mod_h5pactivity'); +} +if (isset($reviewurl)) { + $widget = new mod_h5pactivity\output\reportlink($reviewurl, $reviewmessage); + echo $OUTPUT->render($widget); +} + if ($manager->is_tracking_enabled()) { $trackcomponent = 'mod_h5pactivity'; } else {