ÿØÿà JFIF    ÿÛ „  ( %!1"%)+...383-7(-.+  -%%--------+------------/----------------------------ÿÀ  ¶" ÿÄ     ÿÄ @    !1AQaq‘¡"±ÁÑð2’BRráS‚ñ3b¢CT²Ò #cÿÄ    ÿÄ '   1!AQ2a"‘±#ÿÚ   ? ò(/p‘—–¢·,M|¯G»R£üešT. ôŸEoCFñ‡ 4ÔkåóxR|µÁ¨ÓDà!Ç´µ˜‡p ÍQ‹§!þž^õWsŒy  åÁ§$á«%ºq˜™JyyË0y«^Ϗ -3ÃÏËÈ£Æ\P’ä„s&•‹Üs üô'àWt¹pñCp$bÚF`·7.^)¾!)ÒÙ˜–ÅÏš^h¾ø³·ìe±ŒaeÖ—Í,e=B}Jéߦ$$½—šÙÜCC¯¶è8β–Åkèwx LÉngT±æ¹£Ócœ¿Í/©Éÿ ‰ÚCÒ­mq;Tt¶–}0"zÖPcd1š­¤tˆ„ÐÓâqú[™öUCŠ2޿د­É(¸ßúçšmW,Ò ºf‹Ån™ÄLêÁ8üÁkÁ€L¥ÍuJ1œôrÂSŒÓöt!7EUíïb)AËi¬â-ÙHk8òÁiXYwÅ)O^"ÇØriÛô{»wRUKÙÏÚlÓ¬€‚’שT ] ªb—ÊîKC^×M“xùäñz¬zJ× {4[¹M'IÆeH1LCWÕ]œûº WDæà°OÅ‚¹c²ÞuA–rL`HšpCez‡V’— Ž‚Ë„–ñ­'µR0ÕÖ=À:±G»C\nÆÉ5*¢ †3šhQ4ýÒi$Õ1"Ü]¢p-q3zcQ$ªö¨¥æf›‘»´ÝÚHá„e²JÇ–iÊ:·à¬Øc5Åk»Laª*wi [0Ówk ¦a¦0Õ²ÅÅ‚T¸’³q:Æ<¹ú Ò˜Æ}2VE•Ѐp¡Ô3Úº …h³^LJºçì¥Ánó|˜ÖˆÎˆ+« ¦dø‘Mº–d\ªDðÛ„Ì×LÝ——Ð9Ç|•FèçVm™¨€™3õS–9Äå¢<̉Ìm¢±`·º˜&Z²ž Ë®D«¶»6™‰©­HÈ Àá«h­i³04æ’hxœi3ï¨(ÓL·†‹¶ËpŒ ƒÜÒ? :õdsÀ –3æÓ·Üjâ¬9—]8n¼&]I F£¼…ì–—Œ0v°LǐÅfÛ2TÖ’aµ˜N@ÖBSÏ^à´4-±°b×nÈÈœÆD=KdŒø…ÄÈK,›ÖR§fjü$^ÔÎ!„6^ s:Üun\ôkCÞò÷<—Lúl Í›G¹ÀPá¼Nc¡šÒ…ÙWͳœ±vÊáЄ×9‹P‰cEØ‘™›µŸP<×g1 º©Ø¬wiÁ^l5× I/';Ë+´Z‡ÓÄFÕp[Èð’ HÄ ó 9¬Dl4“é±Ï”V=^Hð;žLädÉa‚°Ë,#/Ž~²BÔÛ QãñQtIeórVB,ÏÂf‹4#Μc‹É3ZIÖ Œ¢¥·üxa‡Ãçè„jŠ[5 Ä!U?,3žÎ×…ðdÓ÷háŠA‰ìÄ4÷ÃÜZÃE{‰®+W\ZÍE[‰»µk»MÝ­`¢©†˜ÃV‹-f¢¡†¢a«eŠ%«Y¨©q%jâd,Ôq`) ¤¦…šˆ ¦E>0Ùb²t–Ÿ  ÷jsςҒKÈcø(é›;k(’#ôŒQ ™C> õ³M9óºÆµ¤çâ;uÀ)5>RÔ¸fÓ~Ȧ—’-$fe«"FâUˆÊwµPÏu w+"åÉó¨6s™Û9(À¨ù©hE°½†n`m~“@©›p!Vkhµ Á1šbEqqÊ@ºèl›2 –Yã%‘‚hÛ{a¶‡ÅÞM²¬çL34®Çl]®‹ŽØŒm'RÜe35æöˆ­Á­ cP9šeÁköo´L³Ì=p8]”‡B¶,šº|ɏdzXˆØk*ÇÚ{#ñ‰pã(€¶‡9ý'šÜ³½¯kƒ†Â’ëSOƒ•Å®H6+a¢µˆ‹Y¨b˜b+XˆÖ!°u!¢"µˆˆn2ˆ Å0Ä`Å Ô®c(ÓÜGº•Ä»Œ âWî%q.áÐÄ÷î%umÍ£+ÜQ,Vn¦-GphU¸¢X­&,Gt ·K¢ÅÅ·¥RÄÊÉbKn N­R2g$VµÕó Qr#–ÓZUÏð0ÎtÙÀœz ëöo“Îk¯n„T°"SÞ+³/…€½"ižãO%Í%&Δâ‘ËØ´IˆÓ°Ë‰ÞM3.™yçÁzit›`4C`k¢¼á!álÄï sÂ[×!¤l¾æo"ƒIÔ'@ªu)3›ô®“@Z à¾] üœ'‡4¸PÞkÁ-qÄH‰ÓÍBd´ÿ —¹°ÄA&ÊUC„öz‚‚RæŠtf¬>˜ÑidšEçÑL÷áý¡V±h§­c¿¥¨ƒ@g¼…Ýö^ÞËc.ž„æ“I[ ª+·zÐÒÚ,½Ð»¶€C›7jk$๖€Ž¾ÅÙpp¶ÍþéΤ ½ØÎŒq ñpæžÅ¥#Øâ49ÃÂK^MYV“,¶Éz_òæ:…;Õ«¦I3âJ£¤û5$7Èxün\â]Êe¯(Ÿ&Ž‹¶2<6Ćf× æ¢ƱqWı[(ÙÂŽç‚T7¨@ÈÉÍß‚ïÙ"&3TYlGށ†)†#5ŠA©^T2ÆÁ†)©†©†¤yGXÁ†©"†)©¼ÅV WS†#©]R–r‹q5ÕbâWþBh¯u+ªÅÄ×ü„nÑ^êkªÁj‰jeœWˆÕÕ`µDµ2Ì+Ä µDµµ5Ô{¤Þ0RF’H÷AÛ<ÊÁ§ ƈæ0Ñ£ê4 öÚµ[¬/1²€â<-¬šö²r×0o×w ´xÐÒË„ g01¬„åEM蛇Ã\6µE›‘fÙO k÷CºnÓ1`iîÒ² áÂ!ÑLÄÿ ³¬\õ*:M=Ç»c‹2[‰ØsÕLò\ŒK L^™üÄN\¶#» Ç\……ßïIœÉ$¸ÎñÍ®ÔHÇ Ûí/´>Li”„†g^$ 7NÍÑÑ2M2ªNg=k½Ð}Ÿl6‡ —JWˆ”ÞéT·+µ¦üÂFè¢Vs6ýÆ4»2+2IÝ9ô9®ÇGÙÇ1+Ä8;*Õ§ž½D*îÑWÚçá3Ma¤·ˆ¥t}š²Ü…Q+Æüµ8HíS”üxGéÁˆ¦Ël„éÈ 5 µÄƒ6äÜ(kIš¯S†fÖ&¸®×v|Äyˆ*禌Éù@7-®Ä[]ÎÖ¿ê`º…=:…žJVnݳvðÖKE¼‚ »mà ÷f®:Ë ™–4rð¡4` ܹçž~‹C=œ…¾Ïj|VDtK.ƒ"9 Ï ­–éXá³6WÒïIL-Ї Ç PK1B\Tbdb/services.phpnu[. /** * H5P activity external functions and service definitions. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $functions = [ 'mod_h5pactivity_get_h5pactivity_access_information' => [ 'classname' => 'mod_h5pactivity\external\get_h5pactivity_access_information', 'classpath' => '', 'description' => 'Return access information for a given h5p activity.', 'type' => 'read', 'capabilities' => 'mod/h5pactivity:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_h5pactivity_view_h5pactivity' => [ 'classname' => 'mod_h5pactivity\external\view_h5pactivity', 'classpath' => '', 'description' => 'Trigger the course module viewed event and update the module completion status.', 'type' => 'write', 'capabilities' => 'mod/h5pactivity:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_h5pactivity_get_attempts' => [ 'classname' => 'mod_h5pactivity\external\get_attempts', 'classpath' => '', 'description' => 'Return the information needed to list a user attempts.', 'type' => 'read', 'capabilities' => 'mod/h5pactivity:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_h5pactivity_get_results' => [ 'classname' => 'mod_h5pactivity\external\get_results', 'classpath' => '', 'description' => 'Return the information needed to list a user attempt results.', 'type' => 'read', 'capabilities' => 'mod/h5pactivity:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_h5pactivity_get_h5pactivities_by_courses' => [ 'classname' => 'mod_h5pactivity\external\get_h5pactivities_by_courses', 'classpath' => '', 'description' => 'Returns a list of h5p activities in a list of provided courses, if no list is provided all h5p activities that the user can view will be returned.', 'type' => 'read', 'capabilities' => 'mod/h5pactivity:view', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_h5pactivity_log_report_viewed' => [ 'classname' => 'mod_h5pactivity\external\log_report_viewed', 'classpath' => '', 'description' => 'Log that the h5pactivity was viewed.', 'type' => 'write', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], 'mod_h5pactivity_get_user_attempts' => [ 'classname' => 'mod_h5pactivity\external\get_user_attempts', 'classpath' => '', 'description' => 'Return the information needed to list all enrolled user attempts.', 'type' => 'read', 'capabilities' => 'mod/h5pactivity:reviewattempts', 'services' => [MOODLE_OFFICIAL_MOBILE_SERVICE], ], ]; PK1B\6aa db/access.phpnu[. /** * Plugin capabilities are defined here. * * @package mod_h5pactivity * @category access * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = [ 'mod/h5pactivity:view' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'guest' => CAP_ALLOW, 'student' => CAP_ALLOW, 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ], ], 'mod/h5pactivity:addinstance' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => [ 'manager' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, ], 'clonepermissionsfrom' => 'moodle/course:manageactivities', ], 'mod/h5pactivity:submit' => [ 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ '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:viewall', ), ]; PK1B\$B Tz z db/upgrade.phpnu[. /** * This file keeps track of upgrades to the h5pactivity module * * Sometimes, changes between versions involve * alterations to database structures and other * major things that may break installations. * * The upgrade function in this file will attempt * to perform all the necessary actions to upgrade * your older installation to the current version. * * If there's something it cannot do itself, it * will tell you what you need to do. * * The commands in here will all be database-neutral, * using the methods of database_manager class * * Please do not forget to use upgrade_set_timeout() * before any action that may take longer time to finish. * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Function to upgrade mod_h5pactivity. * @param int $oldversion the version we are upgrading from * @return bool result */ function xmldb_h5pactivity_upgrade($oldversion) { global $DB; // Automatically generated Moodle v4.2.0 release upgrade line. // Put any upgrade step following this. if ($oldversion < 2023042401) { // Remove any orphaned attempt/result records (pointing to non-existing activities). $DB->delete_records_select('h5pactivity_attempts', 'NOT EXISTS ( SELECT 1 FROM {h5pactivity} h5p WHERE h5p.id = {h5pactivity_attempts}.h5pactivityid )'); $DB->delete_records_select('h5pactivity_attempts_results', 'NOT EXISTS ( SELECT 1 FROM {h5pactivity_attempts} attempt WHERE attempt.id = {h5pactivity_attempts_results}.attemptid )'); // H5pactivity savepoint reached. upgrade_mod_savepoint(true, 2023042401, 'h5pactivity'); } // Automatically generated Moodle v4.3.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.4.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v4.5.0 release upgrade line. // Put any upgrade step following this. // Automatically generated Moodle v5.0.0 release upgrade line. // Put any upgrade step following this. return true; } PK1B\x ]]db/install.xmlnu[
PK1B\TY lang/en/h5pactivity.phpnu[. /** * Strings for component 'mod_h5pactivity', language 'en'. * * @package mod_h5pactivity * @category string * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ 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'] = 'Unknown answer'; $string['answer_text'] = 'Answer text'; $string['answer_noanswer'] = 'None'; $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_header_label'] = 'Attempts ({$a})'; $string['attempts_report'] = 'Attempts report'; $string['attempts_none'] = 'This user has no attempts to display.'; $string['choice'] = 'Choice'; $string['completion'] = 'Completion'; $string['contentbank'] = 'More information about the content bank'; $string['contentbank_help'] = 'In the content bank you can create and store content using several authoring tools, including an integrated H5P creator.'; $string['correct_answer'] = 'Correct answer'; $string['deleteallattempts'] = 'All H5P attempts'; $string['displayexport'] = 'Allow download'; $string['displayembed'] = 'Embed button'; $string['displaycopyright'] = 'Copyright button'; $string['dnduploadh5pactivity'] = 'Add an H5P activity'; $string['duration'] = 'Duration'; $string['enablesavestate'] = 'Save state'; $string['enablesavestate_help'] = 'Automatically save the user\'s current state. The user can return later and resume where they left off.'; $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: * Highest grade of all attempts * Average (mean) grade of all attempts * First attempt (all other attempts are ignored) * Last attempt (all other attempts are ignored) * Don\'t use attempts for grading (disable grading calculation)'; $string['grade_manual'] = 'Don\'t calculate a grade'; $string['grade_highest_attempt'] = 'Highest grade'; $string['grade_average_attempt'] = 'Average grade'; $string['grade_last_attempt'] = 'Last attempt'; $string['grade_first_attempt'] = 'First attempt'; $string['gradenoun_h5p'] = 'Grade: {$a}'; $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'; $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. Any question attempts are marked automatically, and the grade is recorded in the gradebook.'; $string['modulename_link'] = 'mod/h5pactivity/view'; $string['modulenameplural'] = 'H5P'; $string['myattempts'] = 'My attempts'; $string['newsubmissions'] = 'H5P submitted'; $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['opencontentbank'] = 'Access the H5P file in the content bank (opens in a new window).'; $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'; $string['pluginadministration'] = 'H5P administration'; $string['pluginname'] = 'H5P'; $string['previewmode'] = 'You are in preview mode.'; $string['privacy:metadata:attempt'] = 'The attempt number'; $string['privacy:metadata:rawscore'] = 'The score obtained'; $string['privacy:metadata:timecreated'] = 'The time when the tracked element was created'; $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:xapisummary'] = 'The H5P activity contains information relating to the xAPI content state stored by the user.'; $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'] = 'Unknown interaction type'; $string['result_sequencing_choice'] = 'Positions'; $string['result_sequencing_answer'] = 'Position value'; $string['result_sequencing_position'] = '#{$a}'; $string['review_mode'] = 'Review attempts'; $string['review_my_attempts'] = 'View my attempts'; $string['review_user_attempts'] = 'View ({$a})'; $string['review_none'] = 'Participants cannot review their own attempts'; $string['review_on_completion'] = 'Participants can review their own attempts'; $string['savestatefreq'] = 'Save state frequency'; $string['savestatefreq_help'] = 'How often (in seconds) that the user\'s current state is saved.'; $string['score'] = 'Score'; $string['score_out_of'] = '{$a->rawscore} out of {$a->maxscore}'; $string['search:activity'] = 'H5P - activity information'; $string['startdate'] = 'Start date'; $string['statement_received'] = 'xAPI statement received'; $string['totalscore'] = 'Total score'; $string['trackingdisabled'] = 'Attempt tracking is not enabled for this activity.'; $string['trackingdisabled_enable'] = 'Attempt tracking is not enabled for this activity. You can enable it in Settings.'; $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['usecontentbank'] = 'Use the content bank (opens in new window) to manage your H5P files'; $string['view'] = 'View'; $string['viewattempts'] = 'View attempts ({$a})'; $string['view_report'] = 'View report'; PK1B\ޟ grade.phpnu[. /** * Redirect the user to the appropiate submission related page. * * @package mod_h5pactivity * @category grade * @copyright 2020 Ferran Recio * @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. $id = required_param('id', PARAM_INT); // Item number may be != 0 for activities that allow more than one grade per user. $itemnumber = optional_param('itemnumber', 0, PARAM_INT); // Graded user ID (optional). $userid = optional_param('userid', 0, PARAM_INT); list ($course, $cm) = get_course_and_cm_from_cmid($id, 'h5pactivity'); require_login($course, true, $cm); $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)); PK1B\ {?/backup/moodle2/restore_h5pactivity_stepslib.phpnu[. /** * All the steps to restore mod_h5pactivity are defined here. * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Defines the structure step to restore one mod_h5pactivity activity. */ class restore_h5pactivity_activity_structure_step extends restore_activity_structure_step { /** * Defines the structure to be restored. * * @return restore_path_element[]. */ protected function define_structure(): array { $paths = []; $userinfo = $this->get_setting_value('userinfo'); $paths[] = new restore_path_element('h5pactivity', '/activity/h5pactivity'); if ($userinfo) { $paths[] = new restore_path_element('h5pactivity_attempt', '/activity/h5pactivity/attempts/attempt'); $paths[] = new restore_path_element('h5pactivity_attempt_result', '/activity/h5pactivity/attempts/attempt/results/result'); } return $this->prepare_activity_structure($paths); } /** * Processes the h5pactivity restore data. * * @param array $data Parsed element data. */ protected function process_h5pactivity(array $data): void { global $DB; $data = (object)$data; $data->course = $this->get_courseid(); // Insert the record. $newitemid = $DB->insert_record('h5pactivity', $data); // Immediately after inserting "activity" record, call this. $this->apply_activity_instance($newitemid); } /** * Processes the h5pactivity_attempts restore data. * * @param array $data Parsed element data. */ protected function process_h5pactivity_attempt(array $data): void { global $DB; $data = (object)$data; $oldid = $data->id; $data->h5pactivityid = $this->get_new_parentid('h5pactivity'); $data->userid = $this->get_mappingid('user', $data->userid); $newitemid = $DB->insert_record('h5pactivity_attempts', $data); $this->set_mapping('h5pactivity_attempt', $oldid, $newitemid); } /** * Processes the h5pactivity_attempts_results restore data. * * @param array $data Parsed element data. */ protected function process_h5pactivity_attempt_result(array $data): void { global $DB; $data = (object)$data; $oldid = $data->id; $data->attemptid = $this->get_new_parentid('h5pactivity_attempt'); $newitemid = $DB->insert_record('h5pactivity_attempts_results', $data); $this->set_mapping('h5pactivity_attempt_result', $oldid, $newitemid); } /** * Defines post-execution actions. */ protected function after_execute(): void { // Add related files, no need to match by itemname (just internally handled context). $this->add_related_files('mod_h5pactivity', 'intro', null); $this->add_related_files('mod_h5pactivity', 'package', null); } } PK1B\X :backup/moodle2/restore_h5pactivity_activity_task.class.phpnu[. /** * The task that provides a complete restore of mod_h5pactivity is defined here. * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/mod/h5pactivity/backup/moodle2/restore_h5pactivity_stepslib.php'); /** * Restore task for mod_h5pactivity. */ class restore_h5pactivity_activity_task extends restore_activity_task { /** * Defines particular settings that this activity can have. */ protected function define_my_settings(): void { return; } /** * Defines particular steps that this activity can have. * * @return base_step. */ protected function define_my_steps(): void { $this->add_step(new restore_h5pactivity_activity_structure_step('h5pactivity_structure', 'h5pactivity.xml')); } /** * Defines the contents in the activity that must be processed by the link decoder. * * @return array. */ public static function define_decode_contents(): array { $contents = []; // Define the contents. $contents[] = new restore_decode_content('h5pactivity', ['intro'], 'h5pactivity'); return $contents; } /** * Defines the decoding rules for links belonging to the activity to be executed by the link decoder. * * @return restore_decode_rule[]. */ public static function define_decode_rules(): array { $rules = []; $rules[] = new restore_decode_rule('H5PACTIVITYVIEWBYID', '/mod/h5pactivity/view.php?id=$1', 'course_module'); $rules[] = new restore_decode_rule('H5PACTIVITYINDEX', '/mod/h5pactivity/index.php?id=$1', 'course'); return $rules; } /** * Defines the restore log rules that will be applied by the * {@link restore_logs_processor} when restoring mod_h5pactivity logs. It * must return one array of {@link restore_log_rule} objects. * * @return restore_log_rule[]. */ public static function define_restore_log_rules(): array { $rules = []; // Define the rules. $rules[] = new restore_log_rule('h5pactivity', 'view all', 'index.php?id={course}', null); return $rules; } } PK1B\ɐ .backup/moodle2/backup_h5pactivity_stepslib.phpnu[. /** * Backup steps for mod_h5pactivity are defined here. * * @package mod_h5pactivity * @category backup * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Define the complete structure for backup, with file and id annotations. */ class backup_h5pactivity_activity_structure_step extends backup_activity_structure_step { /** * Defines the structure of the resulting xml file. * * @return backup_nested_element The structure wrapped by the common 'activity' element. */ protected function define_structure() { $userinfo = $this->get_setting_value('userinfo'); // Replace with the attributes and final elements that the element will handle. $attributes = ['id']; $finalelements = ['name', 'timecreated', 'timemodified', 'intro', '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', 'scaled'] ); $results = new backup_nested_element('results'); $result = new backup_nested_element('result', ['id'], [ 'attemptid', 'subcontent', 'timecreated', 'interactiontype', 'description', 'correctpattern', 'response', 'additionals', 'rawscore', 'maxscore', 'duration', 'completion', 'success' ] ); // Build the tree. $root->add_child($attempts); $attempts->add_child($attempt); $attempt->add_child($results); $results->add_child($result); // Define the source tables for the elements. $root->set_source_table('h5pactivity', ['id' => backup::VAR_ACTIVITYID]); // All the rest of elements only happen if we are including user info. if ($userinfo) { $attempt->set_source_table('h5pactivity_attempts', ['h5pactivityid' => backup::VAR_PARENTID], 'id ASC'); $result->set_source_table('h5pactivity_attempts_results', ['attemptid' => backup::VAR_PARENTID], 'id ASC'); } // Define id annotations. $attempt->annotate_ids('user', 'userid'); // Define file annotations. $root->annotate_files('mod_h5pactivity', 'intro', null); // This file area hasn't itemid. $root->annotate_files('mod_h5pactivity', 'package', null); // This file area hasn't itemid. return $this->prepare_activity_structure($root); } } PK1B\ҹY 9backup/moodle2/backup_h5pactivity_activity_task.class.phpnu[. /** * The task that provides all the steps to perform a complete backup is defined here. * * @package mod_h5pactivity * @category backup * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/mod/h5pactivity/backup/moodle2/backup_h5pactivity_stepslib.php'); /** * The class provides all the settings and steps to perform one complete backup of mod_h5pactivity. */ class backup_h5pactivity_activity_task extends backup_activity_task { /** * Defines particular settings for the plugin. */ protected function define_my_settings() { return; } /** * Defines particular steps for the backup process. */ protected function define_my_steps() { $this->add_step(new backup_h5pactivity_activity_structure_step('h5pactivity_structure', 'h5pactivity.xml')); } /** * Codes the transformations to perform in the activity in order to get transportable (encoded) links. * * @param string $content content to encode. * @return string encoded string */ public static function encode_content_links($content) { global $CFG; $base = preg_quote($CFG->wwwroot, "/"); // Link to the list of choices. $search = "/(".$base."\/mod\/h5pactivity\/index.php\?id\=)([0-9]+)/"; $content = preg_replace($search, '$@H5PACTIVITYINDEX*$2@$', $content); // Link to choice view by moduleid. $search = "/(".$base."\/mod\/h5pactivity\/view.php\?id\=)([0-9]+)/"; $content = preg_replace($search, '$@H5PACTIVITYVIEWBYID*$2@$', $content); return $content; } } PK1B\mview.phpnu[. /** * 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 core_h5p\factory; use core_h5p\player; use core_h5p\helper; require(__DIR__.'/../../config.php'); require_once(__DIR__.'/lib.php'); $id = required_param('id', PARAM_INT); list ($course, $cm) = get_course_and_cm_from_cmid($id, 'h5pactivity'); require_login($course, true, $cm); $manager = manager::create_from_coursemodule($cm); $moduleinstance = $manager->get_instance(); $context = $manager->get_context(); // Trigger module viewed event and completion. $manager->set_module_viewed($course); // Convert display options to a valid object. $factory = new factory(); $core = $factory->get_core(); $config = core_h5p\helper::decode_display_options($core, $moduleinstance->displayoptions); // Instantiate player. $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'mod_h5pactivity', 'package', 0, 'id', false); $file = reset($files); $fileurl = moodle_url::make_pluginfile_url($file->get_contextid(), $file->get_component(), $file->get_filearea(), $file->get_itemid(), $file->get_filepath(), $file->get_filename(), false); $PAGE->set_url('/mod/h5pactivity/view.php', ['id' => $cm->id]); $shortname = format_string($course->shortname, true, ['context' => $context]); $pagetitle = strip_tags($shortname.': '.format_string($moduleinstance->name)); $PAGE->set_title(format_string($pagetitle)); $PAGE->set_heading(format_string($course->fullname)); $PAGE->set_context($context); echo $OUTPUT->header(); $instance = $manager->get_instance(); // Only non-guest users without permission to submit can see the warning messages (typically a teacher or a content creator). if (!$manager->can_submit() && !isguestuser()) { // Show preview mode message. $message = get_string('previewmode', 'mod_h5pactivity'); echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_INFO, false); // If tracking is disabled, show a warning. if (!$manager->is_tracking_enabled()) { if (has_capability('moodle/course:manageactivities', $context)) { $url = new moodle_url('/course/modedit.php', ['update' => $cm->id]); $message = get_string('trackingdisabled_enable', 'mod_h5pactivity', $url->out()); } else { $message = get_string('trackingdisabled', 'mod_h5pactivity'); } echo $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING); } } $extraactions = []; if ($manager->can_view_all_attempts() && $manager->is_tracking_enabled()) { $extraactions[] = new action_link( new moodle_url('/mod/h5pactivity/report.php', ['id' => $cm->id]), get_string('viewattempts', 'mod_h5pactivity', $manager->count_attempts()), null, null, new pix_icon('i/chartbar', '', 'core') ); } echo player::display($fileurl, $config, true, 'mod_h5pactivity', true, $extraactions); echo $OUTPUT->footer(); PK1B\AoZSSpix/monologo.svgnu[ PK1B\7`z`zlib.phpnu[. /** * Library of interface functions and constants. * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); use mod_h5pactivity\local\manager; use mod_h5pactivity\local\grader; use mod_h5pactivity\xapi\handler; /** * Checks if H5P activity supports a specific feature. * * @uses FEATURE_GROUPS * @uses FEATURE_GROUPINGS * @uses FEATURE_MOD_INTRO * @uses FEATURE_SHOW_DESCRIPTION * @uses FEATURE_COMPLETION_TRACKS_VIEWS * @uses FEATURE_COMPLETION_HAS_RULES * @uses FEATURE_MODEDIT_DEFAULT_COMPLETION * @uses FEATURE_GRADE_HAS_GRADE * @uses FEATURE_GRADE_OUTCOMES * @uses FEATURE_BACKUP_MOODLE2 * @param string $feature FEATURE_xx constant for requested feature * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose. */ function h5pactivity_supports(string $feature) { switch($feature) { case FEATURE_GROUPS: return true; case FEATURE_GROUPINGS: return true; case FEATURE_MOD_INTRO: return true; case FEATURE_SHOW_DESCRIPTION: return true; case FEATURE_COMPLETION_TRACKS_VIEWS: return true; case FEATURE_MODEDIT_DEFAULT_COMPLETION: return true; case FEATURE_GRADE_HAS_GRADE: return true; case FEATURE_GRADE_OUTCOMES: return true; case FEATURE_BACKUP_MOODLE2: return true; case FEATURE_MOD_PURPOSE: return MOD_PURPOSE_INTERACTIVECONTENT; default: return null; } } /** * Saves a new instance of the mod_h5pactivity into the database. * * Given an object containing all the necessary data, (defined by the form * in mod_form.php) this function will create a new instance and return the id * number of the instance. * * @param stdClass $data An object from the form. * @param mod_h5pactivity_mod_form $mform The form. * @return int The id of the newly inserted record. */ function h5pactivity_add_instance(stdClass $data, ?mod_h5pactivity_mod_form $mform = null): int { global $DB; $data->timecreated = time(); $data->timemodified = $data->timecreated; $cmid = $data->coursemodule; $data->id = $DB->insert_record('h5pactivity', $data); // We need to use context now, so we need to make sure all needed info is already in db. $DB->set_field('course_modules', 'instance', $data->id, ['id' => $cmid]); h5pactivity_set_mainfile($data); // Extra fields required in grade related functions. $data->cmid = $data->coursemodule; h5pactivity_grade_item_update($data); return $data->id; } /** * Updates an instance of the mod_h5pactivity in the database. * * Given an object containing all the necessary data (defined in mod_form.php), * this function will update an existing instance with new data. * * @param stdClass $data An object from the form in mod_form.php. * @param mod_h5pactivity_mod_form $mform The form. * @return bool True if successful, false otherwise. */ function h5pactivity_update_instance(stdClass $data, ?mod_h5pactivity_mod_form $mform = null): bool { global $DB; $data->timemodified = time(); $data->id = $data->instance; h5pactivity_set_mainfile($data); // Update gradings if grading method or tracking are modified. $data->cmid = $data->coursemodule; $moduleinstance = $DB->get_record('h5pactivity', ['id' => $data->id]); if (($moduleinstance->grademethod != $data->grademethod) || $data->enabletracking != $moduleinstance->enabletracking) { h5pactivity_update_grades($data); } else { h5pactivity_grade_item_update($data); } return $DB->update_record('h5pactivity', $data); } /** * Removes an instance of the mod_h5pactivity from the database. * * @param int $id Id of the module instance. * @return bool True if successful, false on failure. */ function h5pactivity_delete_instance(int $id): bool { global $DB; $activity = $DB->get_record('h5pactivity', ['id' => $id]); if (!$activity) { return false; } if ($cm = get_coursemodule_from_instance('h5pactivity', $activity->id)) { $context = context_module::instance($cm->id); $xapihandler = handler::create('mod_h5pactivity'); $xapihandler->wipe_states($context->id); } // Remove activity record, and all associated attempt data. $attemptids = $DB->get_fieldset_select('h5pactivity_attempts', 'id', 'h5pactivityid = ?', [$id]); if ($attemptids) { $DB->delete_records_list('h5pactivity_attempts_results', 'attemptid', $attemptids); $DB->delete_records_list('h5pactivity_attempts', 'id', $attemptids); } $DB->delete_records('h5pactivity', ['id' => $id]); h5pactivity_grade_item_delete($activity); return true; } /** * Checks if scale is being used by any instance of mod_h5pactivity. * * This is used to find out if scale used anywhere. * * @param int $scaleid ID of the scale. * @return bool True if the scale is used by any mod_h5pactivity instance. */ function h5pactivity_scale_used_anywhere(int $scaleid): bool { global $DB; if ($scaleid and $DB->record_exists('h5pactivity', ['grade' => -$scaleid])) { return true; } else { return false; } } /** * Creates or updates grade item for the given mod_h5pactivity instance. * * Needed by {@link grade_update_mod_grades()}. * * @param stdClass $moduleinstance Instance object with extra cmidnumber and modname property. * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook * @return int int 0 if ok, error code otherwise */ function h5pactivity_grade_item_update(stdClass $moduleinstance, $grades = null): int { $idnumber = $moduleinstance->idnumber ?? ''; $grader = new grader($moduleinstance, $idnumber); return $grader->grade_item_update($grades); } /** * Delete grade item for given mod_h5pactivity instance. * * @param stdClass $moduleinstance Instance object. * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED */ function h5pactivity_grade_item_delete(stdClass $moduleinstance): ?int { $idnumber = $moduleinstance->idnumber ?? ''; $grader = new grader($moduleinstance, $idnumber); return $grader->grade_item_delete(); } /** * Update mod_h5pactivity grades in the gradebook. * * Needed by {@link grade_update_mod_grades()}. * * @param stdClass $moduleinstance Instance object with extra cmidnumber and modname property. * @param int $userid Update grade of specific user only, 0 means all participants. */ function h5pactivity_update_grades(stdClass $moduleinstance, int $userid = 0): void { $idnumber = $moduleinstance->idnumber ?? ''; $grader = new grader($moduleinstance, $idnumber); $grader->update_grades($userid); } /** * Rescale all grades for this activity and push the new grades to the gradebook. * * @param stdClass $course Course db record * @param stdClass $cm Course module db record * @param float $oldmin * @param float $oldmax * @param float $newmin * @param float $newmax * @return bool true if reescale is successful */ function h5pactivity_rescale_activity_grades(stdClass $course, stdClass $cm, float $oldmin, float $oldmax, float $newmin, float $newmax): bool { $manager = manager::create_from_coursemodule($cm); $grader = $manager->get_grader(); $grader->update_grades(); return true; } /** * Implementation of the function for printing the form elements that control * whether the course reset functionality affects the H5P activity. * * @param MoodleQuickForm $mform form passed by reference */ function h5pactivity_reset_course_form_definition(&$mform): void { $mform->addElement('header', 'h5pactivityheader', get_string('modulenameplural', 'mod_h5pactivity')); $mform->addElement('static', 'h5pactivitydelete', get_string('delete')); $mform->addElement('advcheckbox', 'reset_h5pactivity', get_string('deleteallattempts', 'mod_h5pactivity')); } /** * Course reset form defaults. * * @param stdClass $course the course object * @return array */ function h5pactivity_reset_course_form_defaults(stdClass $course): array { return ['reset_h5pactivity' => 1]; } /** * This function is used by the reset_course_userdata function in moodlelib. * * This function will remove all H5P attempts in the database * and clean up any related data. * * @param stdClass $data the data submitted from the reset course. * @return array of reseting status */ function h5pactivity_reset_userdata(stdClass $data): array { global $DB; $componentstr = get_string('modulenameplural', 'mod_h5pactivity'); $status = []; if (!empty($data->reset_h5pactivity)) { $params = ['courseid' => $data->courseid]; $sql = "SELECT a.id FROM {h5pactivity} a WHERE a.course=:courseid"; if ($activities = $DB->get_records_sql($sql, $params)) { $xapihandler = handler::create('mod_h5pactivity'); foreach ($activities as $activity) { $cm = get_coursemodule_from_instance('h5pactivity', $activity->id, $data->courseid, false, MUST_EXIST); mod_h5pactivity\local\attempt::delete_all_attempts ($cm); $context = context_module::instance($cm->id); $xapihandler->wipe_states($context->id); } } // Remove all grades from gradebook. if (empty($data->reset_gradebook_grades)) { h5pactivity_reset_gradebook($data->courseid, 'reset'); } $status[] = [ 'component' => $componentstr, 'item' => get_string('deleteallattempts', 'mod_h5pactivity'), 'error' => false, ]; } return $status; } /** * Removes all grades from gradebook * * @param int $courseid Coude ID * @param string $type optional type (default '') */ function h5pactivity_reset_gradebook(int $courseid, string $type=''): void { global $DB; $sql = "SELECT a.*, cm.idnumber as cmidnumber, a.course as courseid FROM {h5pactivity} a, {course_modules} cm, {modules} m WHERE m.name='h5pactivity' AND m.id=cm.module AND cm.instance=a.id AND a.course=?"; if ($activities = $DB->get_records_sql($sql, [$courseid])) { foreach ($activities as $activity) { h5pactivity_grade_item_update($activity, 'reset'); } } } /** * Return a list of page types * * @param string $pagetype current page type * @param context|null $parentcontext Block's parent context * @param context|null $currentcontext Current context of block * @return array array of page types and it's names */ function h5pactivity_page_type_list(string $pagetype, ?context $parentcontext, ?context $currentcontext): array { $modulepagetype = [ 'mod-h5pactivity-*' => get_string('page-mod-h5pactivity-x', 'h5pactivity'), ]; return $modulepagetype; } /** * Check if the module has any update that affects the current user since a given time. * * @param cm_info $cm course module data * @param int $from the time to check updates from * @param array $filter if we need to check only specific updates * @return stdClass an object with the different type of areas indicating if they were updated or not */ function h5pactivity_check_updates_since(cm_info $cm, int $from, array $filter = []): stdClass { global $DB, $USER; $updates = course_check_module_updates_since($cm, $from, ['package'], $filter); $updates->tracks = (object) ['updated' => false]; $select = 'h5pactivityid = ? AND userid = ? AND timemodified > ?'; $params = [$cm->instance, $USER->id, $from]; $tracks = $DB->get_records_select('h5pactivity_attempts', $select, $params, '', 'id'); if (!empty($tracks)) { $updates->tracks->updated = true; $updates->tracks->itemids = array_keys($tracks); } // Now, teachers should see other students updates. if (has_capability('mod/h5pactivity:reviewattempts', $cm->context)) { $select = 'h5pactivityid = ? AND timemodified > ?'; $params = [$cm->instance, $from]; if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) { $groupusers = array_keys(groups_get_activity_shared_group_members($cm)); if (empty($groupusers)) { return $updates; } list($insql, $inparams) = $DB->get_in_or_equal($groupusers); $select .= ' AND userid ' . $insql; $params = array_merge($params, $inparams); } $updates->usertracks = (object) ['updated' => false]; $tracks = $DB->get_records_select('h5pactivity_attempts', $select, $params, '', 'id'); if (!empty($tracks)) { $updates->usertracks->updated = true; $updates->usertracks->itemids = array_keys($tracks); } } return $updates; } /** * Returns the lists of all browsable file areas within the given module context. * * The file area 'intro' for the activity introduction field is added automatically * by {@link file_browser::get_file_info_context_module()}. * * @param stdClass $course course object * @param stdClass $cm course module object * @param stdClass $context context object * @return string[] array of pair file area => human file area name */ function h5pactivity_get_file_areas(stdClass $course, stdClass $cm, stdClass $context): array { $areas = []; $areas['package'] = get_string('areapackage', 'mod_h5pactivity'); return $areas; } /** * File browsing support for data module. * * @param file_browser $browser * @param array $areas * @param stdClass $course * @param stdClass $cm * @param context $context * @param string $filearea * @param int|null $itemid * @param string|null $filepath * @param string|null $filename * @return file_info_stored|null file_info_stored instance or null if not found */ function h5pactivity_get_file_info(file_browser $browser, array $areas, stdClass $course, stdClass $cm, context $context, string $filearea, ?int $itemid = null, ?string $filepath = null, ?string $filename = null): ?file_info_stored { global $CFG; if (!has_capability('moodle/course:managefiles', $context)) { return null; } $fs = get_file_storage(); if ($filearea === 'package') { $filepath = is_null($filepath) ? '/' : $filepath; $filename = is_null($filename) ? '.' : $filename; $urlbase = $CFG->wwwroot.'/pluginfile.php'; if (!$storedfile = $fs->get_file($context->id, 'mod_h5pactivity', 'package', 0, $filepath, $filename)) { if ($filepath === '/' and $filename === '.') { $storedfile = new virtual_root_file($context->id, 'mod_h5pactivity', 'package', 0); } else { // Not found. return null; } } return new file_info_stored($browser, $context, $storedfile, $urlbase, $areas[$filearea], false, true, false, false); } return null; } /** * Serves the files from the mod_h5pactivity file areas. * * @param mixed $course course or id of the course * @param mixed $cm course module or id of the course module * @param context $context * @param string $filearea * @param array $args * @param bool $forcedownload * @param array $options additional options affecting the file serving * @return bool false if file not found, does not return if found - just send the file */ function h5pactivity_pluginfile($course, $cm, context $context, string $filearea, array $args, bool $forcedownload, array $options = []): bool { if ($context->contextlevel != CONTEXT_MODULE) { return false; } require_login($course, true, $cm); $fullpath = ''; if ($filearea === 'package') { $revision = (int)array_shift($args); // Prevents caching problems - ignored here. $relativepath = implode('/', $args); $fullpath = "/$context->id/mod_h5pactivity/package/0/$relativepath"; } if (empty($fullpath)) { return false; } $fs = get_file_storage(); $file = $fs->get_file_by_hash(sha1($fullpath)); if (empty($file)) { return false; } send_stored_file($file, $lifetime, 0, false, $options); } /** * Saves draft files as the activity package. * * @param stdClass $data an object from the form */ function h5pactivity_set_mainfile(stdClass $data): void { $fs = get_file_storage(); $cmid = $data->coursemodule; $context = context_module::instance($cmid); if (!empty($data->packagefile)) { $fs = get_file_storage(); $fs->delete_area_files($context->id, 'mod_h5pactivity', 'package'); file_save_draft_area_files($data->packagefile, $context->id, 'mod_h5pactivity', 'package', 0, ['subdirs' => 0, 'maxfiles' => 1]); } } /** * Register the ability to handle drag and drop file uploads * @return array containing details of the files / types the mod can handle */ function h5pactivity_dndupload_register(): array { return [ 'files' => [ [ 'extension' => 'h5p', 'message' => get_string('dnduploadh5pactivity', 'h5pactivity') ] ] ]; } /** * Handle a file that has been uploaded * @param object $uploadinfo details of the file / content that has been uploaded * @return int instance id of the newly created mod */ function h5pactivity_dndupload_handle($uploadinfo): int { global $CFG; $context = context_module::instance($uploadinfo->coursemodule); file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_h5pactivity', 'package', 0); $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'mod_h5pactivity', 'package', 0, 'sortorder, itemid, filepath, filename', false); $file = reset($files); // Create a default h5pactivity object to pass to h5pactivity_add_instance()! $h5p = get_config('h5pactivity'); $h5p->intro = ''; $h5p->introformat = FORMAT_HTML; $h5p->course = $uploadinfo->course->id; $h5p->coursemodule = $uploadinfo->coursemodule; $h5p->grade = $CFG->gradepointdefault; // Add some special handling for the H5P options checkboxes. $factory = new \core_h5p\factory(); $core = $factory->get_core(); if (isset($uploadinfo->displayopt)) { $config = (object) $uploadinfo->displayopt; } else { $config = \core_h5p\helper::decode_display_options($core); } $h5p->displayoptions = \core_h5p\helper::get_display_options($core, $config); $h5p->cmidnumber = ''; $h5p->name = $uploadinfo->displayname; $h5p->reference = $file->get_filename(); return h5pactivity_add_instance($h5p, null); } /** * Print recent activity from all h5pactivities in a given course * * This is used by the recent activity block * @param mixed $course the course to print activity for * @param bool $viewfullnames boolean to determine whether to show full names or not * @param int $timestart the time the rendering started * @return bool true if activity was printed, false otherwise. */ function h5pactivity_print_recent_activity($course, bool $viewfullnames, int $timestart): bool { global $CFG, $DB, $OUTPUT, $USER; $dbparams = [$timestart, $course->id, 'h5pactivity']; $userfieldsapi = \core_user\fields::for_userpic(); $namefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects;; $sql = "SELECT h5pa.id, h5pa.timemodified, cm.id as cmid, $namefields FROM {h5pactivity_attempts} h5pa JOIN {h5pactivity} h5p ON h5p.id = h5pa.h5pactivityid JOIN {course_modules} cm ON cm.instance = h5p.id JOIN {modules} md ON md.id = cm.module JOIN {user} u ON u.id = h5pa.userid WHERE h5pa.timemodified >= ? AND h5p.course = ? AND md.name = ? ORDER BY h5pa.timemodified ASC"; if (!$submissions = $DB->get_records_sql($sql, $dbparams)) { return false; } $modinfo = get_fast_modinfo($course); $recentactivity = h5pactivity_fetch_recent_activity($submissions, $course->id); if (empty($recentactivity)) { return false; } $cms = $modinfo->get_cms(); echo $OUTPUT->heading(get_string('newsubmissions', 'h5pactivity') . ':', 6); foreach ($recentactivity as $submission) { $cm = $cms[$submission->cmid]; $link = $CFG->wwwroot.'/mod/h5pactivity/view.php?id='.$cm->id; print_recent_activity_note($submission->timemodified, $submission, $cm->name, $link, false, $viewfullnames); } return true; } /** * Returns all h5pactivities since a given time. * * @param array $activities The activity information is returned in this array * @param int $index The current index in the activities array * @param int $timestart The earliest activity to show * @param int $courseid Limit the search to this course * @param int $cmid The course module id * @param int $userid Optional user id * @param int $groupid Optional group id * @return void */ function h5pactivity_get_recent_mod_activity(array &$activities, int &$index, int $timestart, int $courseid, int $cmid, int $userid=0, int $groupid=0) { global $CFG, $DB, $USER; $course = get_course($courseid); $modinfo = get_fast_modinfo($course); $cm = $modinfo->get_cm($cmid); $params = []; if ($userid) { $userselect = 'AND u.id = :userid'; $params['userid'] = $userid; } else { $userselect = ''; } if ($groupid) { $groupselect = 'AND gm.groupid = :groupid'; $groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id'; $params['groupid'] = $groupid; } else { $groupselect = ''; $groupjoin = ''; } $params['cminstance'] = $cm->instance; $params['timestart'] = $timestart; $params['cmid'] = $cmid; $userfieldsapi = \core_user\fields::for_userpic(); $userfields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects; $sql = "SELECT h5pa.id, h5pa.timemodified, cm.id as cmid, $userfields FROM {h5pactivity_attempts} h5pa JOIN {h5pactivity} h5p ON h5p.id = h5pa.h5pactivityid JOIN {course_modules} cm ON cm.instance = h5p.id JOIN {modules} md ON md.id = cm.module JOIN {user} u ON u.id = h5pa.userid $groupjoin WHERE h5pa.timemodified >= :timestart AND h5p.id = :cminstance $userselect $groupselect AND cm.id = :cmid ORDER BY h5pa.timemodified ASC"; if (!$submissions = $DB->get_records_sql($sql, $params)) { return; } $cmcontext = context_module::instance($cm->id); $grader = has_capability('mod/h5pactivity:reviewattempts', $cmcontext); $viewfullnames = has_capability('moodle/site:viewfullnames', $cmcontext); $recentactivity = h5pactivity_fetch_recent_activity($submissions, $courseid); if (empty($recentactivity)) { return; } if ($grader) { require_once($CFG->libdir.'/gradelib.php'); $userids = []; foreach ($recentactivity as $id => $submission) { $userids[] = $submission->userid; } $grades = grade_get_grades($courseid, 'mod', 'h5pactivity', $cm->instance, $userids); } $aname = format_string($cm->name, true); foreach ($recentactivity as $submission) { $activity = new stdClass(); $activity->type = 'h5pactivity'; $activity->cmid = $cm->id; $activity->name = $aname; $activity->sectionnum = $cm->sectionnum; $activity->timestamp = $submission->timemodified; $activity->user = new stdClass(); if ($grader) { $activity->grade = $grades->items[0]->grades[$submission->userid]->str_long_grade; } $userfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); foreach ($userfields as $userfield) { if ($userfield == 'id') { // Aliased in SQL above. $activity->user->{$userfield} = $submission->userid; } else { $activity->user->{$userfield} = $submission->{$userfield}; } } $activity->user->fullname = fullname($submission, $viewfullnames); $activities[$index++] = $activity; } return; } /** * Print recent activity from all h5pactivities in a given course * * This is used by course/recent.php * @param stdClass $activity * @param int $courseid * @param bool $detail * @param array $modnames */ function h5pactivity_print_recent_mod_activity(stdClass $activity, int $courseid, bool $detail, array $modnames) { global $OUTPUT; $modinfo = []; if ($detail) { $modinfo['modname'] = $activity->name; $modinfo['modurl'] = new moodle_url('/mod/h5pactivity/view.php', ['id' => $activity->cmid]); $modinfo['modicon'] = $OUTPUT->image_icon('monologo', $modnames[$activity->type], 'h5pactivity'); } $userpicture = $OUTPUT->user_picture($activity->user); $template = ['userpicture' => $userpicture, 'submissiontimestamp' => $activity->timestamp, 'modinfo' => $modinfo, 'userurl' => new moodle_url('/user/view.php', array('id' => $activity->user->id, 'course' => $courseid)), 'fullname' => $activity->user->fullname]; if (isset($activity->grade)) { $template['grade'] = get_string('gradenoun_h5p', 'h5pactivity', $activity->grade); } echo $OUTPUT->render_from_template('mod_h5pactivity/reviewattempts', $template); } /** * Fetches recent activity for course module. * * @param array $submissions The activity submissions * @param int $courseid Limit the search to this course * @return array $recentactivity recent activity in a course. */ function h5pactivity_fetch_recent_activity(array $submissions, int $courseid): array { global $USER; $course = get_course($courseid); $modinfo = get_fast_modinfo($course); $recentactivity = []; $grader = []; $cms = $modinfo->get_cms(); foreach ($submissions as $submission) { if (!array_key_exists($submission->cmid, $cms)) { continue; } $cm = $cms[$submission->cmid]; if (!$cm->uservisible) { continue; } if ($USER->id == $submission->userid) { $recentactivity[$submission->userid] = $submission; continue; } $cmcontext = context_module::instance($cm->id); // The act of submitting of attempt may be considered private - // only graders will see it if specified. if (!array_key_exists($cm->id, $grader)) { $grader[$cm->id] = has_capability('mod/h5pactivity:reviewattempts', $cmcontext); } if (!$grader[$cm->id]) { continue; } $groups = []; $usersgroups = []; $groupmode = groups_get_activity_groupmode($cm, $course); $accessallgroups = has_capability('moodle/site:accessallgroups', $cmcontext); if ($groupmode == SEPARATEGROUPS && !$accessallgroups) { if (isguestuser()) { // Shortcut - guest user does not belong into any group. continue; } if (!isset($groups[$cm->groupingid])) { $groups[$cm->groupingid] = $modinfo->get_groups($cm->groupingid); if (!$groups[$cm->groupingid]) { continue; } } if (!isset($usersgroups[$cm->groupingid][$submission->userid])) { $usersgroups[$cm->groupingid][$submission->userid] = groups_get_all_groups($course->id, $submission->userid, $cm->groupingid, 'g.*', false, true); } if (is_array($usersgroups[$cm->groupingid][$submission->userid])) { $usersgroupstmp = array_keys($usersgroups[$cm->groupingid][$submission->userid]); $intersect = array_intersect($usersgroupstmp, $groups[$cm->groupingid]); if (empty($intersect)) { continue; } } } $recentactivity[$submission->userid] = $submission; } return $recentactivity; } /** * Extends the settings navigation with the H5P activity settings * This function is called when the context for the page is an H5P activity. This is not called by AJAX * so it is safe to rely on the $PAGE. * * @param settings_navigation $settingsnav The settings navigation object * @param navigation_node $h5pactivitynode The node to add module settings to */ function h5pactivity_extend_settings_navigation(settings_navigation $settingsnav, ?navigation_node $h5pactivitynode = null) { global $USER; $manager = manager::create_from_coursemodule($settingsnav->get_page()->cm); // Attempts report. if ($manager->can_view_all_attempts()) { $attemptsreporturl = new moodle_url('/mod/h5pactivity/report.php', ['a' => $settingsnav->get_page()->cm->instance]); $h5pactivitynode->add(get_string('attempts_report', 'h5pactivity'), $attemptsreporturl, settings_navigation::TYPE_SETTING, '', 'attemptsreport'); } else if ($manager->can_view_own_attempts() && $manager->count_attempts($USER->id)) { $attemptsreporturl = new moodle_url('/mod/h5pactivity/report.php', ['a' => $settingsnav->get_page()->cm->instance, 'userid' => $USER->id]); $h5pactivitynode->add(get_string('attempts_report', 'h5pactivity'), $attemptsreporturl, settings_navigation::TYPE_SETTING, '', 'attemptsreport'); } } /** * Whether the activity is branded. * This information is used, for instance, to decide if a filter should be applied to the icon or not. * * @return bool True if the activity is branded, false otherwise. */ function h5pactivity_is_branded(): bool { return true; } PK1B\í"" version.phpnu[. /** * Plugin version and other meta-data are defined here. * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_h5pactivity'; $plugin->version = 2025041400; $plugin->requires = 2025040800; PK1B\I[.[.&classes/external/get_user_attempts.phpnu[. namespace mod_h5pactivity\external; use mod_h5pactivity\local\manager; use mod_h5pactivity\local\attempt; use mod_h5pactivity\local\report; use mod_h5pactivity\local\report\attempts as report_attempts; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use moodle_exception; use context_module; use stdClass; /** * This is the external method to return the information needed to list all enrolled user attempts. * * @package mod_h5pactivity * @since Moodle 3.11 * @copyright 2020 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_user_attempts extends external_api { /** * Webservice parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'h5pactivityid' => new external_value(PARAM_INT, 'h5p activity instance id'), 'sortorder' => new external_value(PARAM_TEXT, 'sort by either user id, firstname or lastname (with optional asc/desc)', VALUE_DEFAULT, 'id ASC'), 'page' => new external_value(PARAM_INT, 'current page', VALUE_DEFAULT, -1), 'perpage' => new external_value(PARAM_INT, 'items per page', VALUE_DEFAULT, 0), 'firstinitial' => new external_value(PARAM_TEXT, 'Users whose first name ' . 'starts with $firstinitial', VALUE_DEFAULT, ''), 'lastinitial' => new external_value(PARAM_TEXT, 'Users whose last name ' . 'starts with $lastinitial', VALUE_DEFAULT, ''), ] ); } /** * Return user attempts information in a h5p activity. * * @throws moodle_exception if the user cannot see the report * @param int $h5pactivityid The h5p activity id * @param int $sortorder The sort order * @param int $page page number * @param int $perpage items per page * @param int $firstinitial Users whose first name starts with $firstinitial * @param int $lastinitial Users whose last name starts with $lastinitial * @return stdClass report data */ public static function execute(int $h5pactivityid, $sortorder = 'id ASC', ?int $page = 0, ?int $perpage = 0, $firstinitial = '', $lastinitial = ''): stdClass { [ 'h5pactivityid' => $h5pactivityid, 'sortorder' => $sortorder, 'page' => $page, 'perpage' => $perpage, 'firstinitial' => $firstinitial, 'lastinitial' => $lastinitial, ] = external_api::validate_parameters(self::execute_parameters(), [ 'h5pactivityid' => $h5pactivityid, 'sortorder' => $sortorder, 'page' => $page, 'perpage' => $perpage, 'firstinitial' => $firstinitial, 'lastinitial' => $lastinitial, ]); $warnings = []; [$course, $cm] = get_course_and_cm_from_instance($h5pactivityid, 'h5pactivity'); $context = context_module::instance($cm->id); self::validate_context($context); $manager = manager::create_from_coursemodule($cm); $instance = $manager->get_instance(); if (!$manager->can_view_all_attempts()) { throw new moodle_exception('nopermissiontoviewattempts', 'error', '', null, 'h5pactivity:reviewattempts required view attempts of all enrolled users.'); } // Ensure sortorder parameter is safe to use. Fallback to default value of the parameter itself. $sortorderparts = explode(' ', $sortorder, 2); $sortorder = get_safe_orderby([ 'id' => 'u.id', 'firstname' => 'u.firstname', 'lastname' => 'u.lastname', 'default' => 'u.id', ], $sortorderparts[0], $sortorderparts[1] ?? ''); $users = self::get_active_users($manager, 'u.id, u.firstname, u.lastname', $sortorder, $page * $perpage, $perpage); $usersattempts = []; foreach ($users as $user) { if ($firstinitial) { if (strpos($user->firstname, $firstinitial) === false) { continue; } } if ($lastinitial) { if (strpos($user->lastname, $lastinitial) === false) { continue; } } $report = $manager->get_report($user->id); if ($report && $report instanceof report_attempts) { $usersattempts[] = self::export_user_attempts($report, $user->id); } else { $warnings[] = [ 'item' => 'user', 'itemid' => $user->id, 'warningcode' => '1', 'message' => "Cannot access user attempts", ]; } } $result = (object)[ 'activityid' => $instance->id, 'usersattempts' => $usersattempts, 'totalattempts' => $manager->count_attempts(), 'warnings' => $warnings, ]; return $result; } /** * Generate the active users list * * @param manager $manager the h5pactivity manager * @param string $userfields the user fields to get * @param string $sortorder the SQL sortorder * @param int $limitfrom SQL limit from * @param int $limitnum SQL limit num */ private static function get_active_users( manager $manager, string $userfields = 'u.*', string $sortorder = '', int $limitfrom = 0, int $limitnum = 0 ): array { global $DB; $capjoin = $manager->get_active_users_join(true); // Final SQL. $sql = "SELECT DISTINCT {$userfields} FROM {user} u {$capjoin->joins} WHERE {$capjoin->wheres} {$sortorder}"; return $DB->get_records_sql($sql, $capjoin->params, $limitfrom, $limitnum); } /** * Export attempts data for a specific user. * * @param report $report the report attempts object * @param int $userid the user id * @return stdClass */ private static function export_user_attempts(report $report, int $userid): stdClass { $scored = $report->get_scored(); $attempts = $report->get_attempts(); $result = (object)[ 'userid' => $userid, 'attempts' => [], ]; foreach ($attempts as $attempt) { $result->attempts[] = self::export_attempt($attempt); } if (!empty($scored)) { $result->scored = (object)[ 'title' => $scored->title, 'grademethod' => $scored->grademethod, 'attempts' => [self::export_attempt($scored->attempt)], ]; } return $result; } /** * Return a data object from an attempt. * * @param attempt $attempt the attempt object * @return stdClass a WS compatible version of the attempt */ private static function export_attempt(attempt $attempt): stdClass { $result = (object)[ 'id' => $attempt->get_id(), 'h5pactivityid' => $attempt->get_h5pactivityid(), 'userid' => $attempt->get_userid(), 'timecreated' => $attempt->get_timecreated(), 'timemodified' => $attempt->get_timemodified(), 'attempt' => $attempt->get_attempt(), 'rawscore' => $attempt->get_rawscore(), 'maxscore' => $attempt->get_maxscore(), 'duration' => $attempt->get_duration(), 'scaled' => $attempt->get_scaled(), ]; if ($attempt->get_completion() !== null) { $result->completion = $attempt->get_completion(); } if ($attempt->get_success() !== null) { $result->success = $attempt->get_success(); } return $result; } /** * Describes the get_h5pactivity_access_information return value. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'activityid' => new external_value(PARAM_INT, 'Activity course module ID'), 'usersattempts' => new external_multiple_structure( self::get_user_attempts_returns(), 'The complete users attempts list' ), 'totalattempts' => new external_value(PARAM_INT, 'Total number of attempts'), 'warnings' => new external_warnings(), ], 'Activity attempts data'); } /** * Describes the get_h5pactivity_access_information return value. * * @return external_single_structure */ private static function get_user_attempts_returns(): external_single_structure { $structure = [ 'userid' => new external_value(PARAM_INT, 'The user id'), 'attempts' => new external_multiple_structure(self::get_user_attempt_returns(), 'The complete attempts list'), 'scored' => new external_single_structure([ 'title' => new external_value(PARAM_NOTAGS, 'Scored attempts title'), 'grademethod' => new external_value(PARAM_NOTAGS, 'Grading method'), 'attempts' => new external_multiple_structure(self::get_user_attempt_returns(), 'List of the grading attempts'), ], 'Attempts used to grade the activity', VALUE_OPTIONAL), ]; return new external_single_structure($structure); } /** * Return the external structure of an attempt. * * @return external_single_structure */ private static function get_user_attempt_returns(): external_single_structure { $result = new external_single_structure([ 'id' => new external_value(PARAM_INT, 'ID of the context'), 'h5pactivityid' => new external_value(PARAM_INT, 'ID of the H5P activity'), 'userid' => new external_value(PARAM_INT, 'ID of the user'), 'timecreated' => new external_value(PARAM_INT, 'Attempt creation'), 'timemodified' => new external_value(PARAM_INT, 'Attempt modified'), 'attempt' => new external_value(PARAM_INT, 'Attempt number'), 'rawscore' => new external_value(PARAM_INT, 'Attempt score value'), 'maxscore' => new external_value(PARAM_INT, 'Attempt max score'), 'duration' => new external_value(PARAM_INT, 'Attempt duration in seconds'), 'completion' => new external_value(PARAM_INT, 'Attempt completion', VALUE_OPTIONAL), 'success' => new external_value(PARAM_INT, 'Attempt success', VALUE_OPTIONAL), 'scaled' => new external_value(PARAM_FLOAT, 'Attempt scaled'), ]); return $result; } } PK1B\$S"S"!classes/external/get_attempts.phpnu[. namespace mod_h5pactivity\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use mod_h5pactivity\local\manager; use mod_h5pactivity\local\attempt; use mod_h5pactivity\local\report\attempts as report_attempts; use context_module; use stdClass; /** * This is the external method for getting the information needed to present an attempts report. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_attempts extends external_api { /** * Webservice parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'h5pactivityid' => new external_value(PARAM_INT, 'h5p activity instance id'), 'userids' => new external_multiple_structure( new external_value(PARAM_INT, 'The user ids to get attempts (null means only current user)', VALUE_DEFAULT), 'User ids', VALUE_DEFAULT, [] ), ] ); } /** * Return user attempts information in a h5p activity. * * @throws moodle_exception if the user cannot see the report * @param int $h5pactivityid The h5p activity id * @param int[]|null $userids The user ids (if no provided $USER will be used) * @return stdClass report data */ public static function execute(int $h5pactivityid, ?array $userids = []): stdClass { global $USER; $params = external_api::validate_parameters(self::execute_parameters(), [ 'h5pactivityid' => $h5pactivityid, 'userids' => $userids, ]); $h5pactivityid = $params['h5pactivityid']; $userids = $params['userids']; if (empty($userids)) { $userids = [$USER->id]; } $warnings = []; // Request and permission validation. list ($course, $cm) = get_course_and_cm_from_instance($h5pactivityid, 'h5pactivity'); $context = context_module::instance($cm->id); self::validate_context($context); $manager = manager::create_from_coursemodule($cm); $instance = $manager->get_instance(); $usersattempts = []; foreach ($userids as $userid) { $report = $manager->get_report($userid); if ($report && $report instanceof report_attempts) { $usersattempts[] = self::export_user_attempts($report, $userid); } else { $warnings[] = [ 'item' => 'user', 'itemid' => $userid, 'warningcode' => '1', 'message' => "Cannot access user attempts", ]; } } $result = (object)[ 'activityid' => $instance->id, 'usersattempts' => $usersattempts, 'warnings' => $warnings, ]; return $result; } /** * Export attempts data for a specific user. * * @param report_attempts $report the report attempts object * @param int $userid the user id * @return stdClass */ public static function export_user_attempts(report_attempts $report, int $userid): stdClass { $scored = $report->get_scored(); $attempts = $report->get_attempts(); $result = (object)[ 'userid' => $userid, 'attempts' => [], ]; foreach ($attempts as $attempt) { $result->attempts[] = self::export_attempt($attempt); } if (!empty($scored)) { $result->scored = (object)[ 'title' => $scored->title, 'grademethod' => $scored->grademethod, 'attempts' => [self::export_attempt($scored->attempt)], ]; } return $result; } /** * Return a data object from an attempt. * * @param attempt $attempt the attempt object * @return stdClass a WS compatible version of the attempt */ private static function export_attempt(attempt $attempt): stdClass { $result = (object)[ 'id' => $attempt->get_id(), 'h5pactivityid' => $attempt->get_h5pactivityid(), 'userid' => $attempt->get_userid(), 'timecreated' => $attempt->get_timecreated(), 'timemodified' => $attempt->get_timemodified(), 'attempt' => $attempt->get_attempt(), 'rawscore' => $attempt->get_rawscore(), 'maxscore' => $attempt->get_maxscore(), 'duration' => $attempt->get_duration(), 'scaled' => $attempt->get_scaled(), ]; if ($attempt->get_completion() !== null) { $result->completion = $attempt->get_completion(); } if ($attempt->get_success() !== null) { $result->success = $attempt->get_success(); } return $result; } /** * Describes the get_h5pactivity_access_information return value. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'activityid' => new external_value(PARAM_INT, 'Activity course module ID'), 'usersattempts' => new external_multiple_structure( self::get_user_attempts_returns(), 'The complete users attempts list' ), 'warnings' => new external_warnings(), ], 'Activity attempts data'); } /** * Describes the get_h5pactivity_access_information return value. * * @return external_single_structure */ public static function get_user_attempts_returns(): external_single_structure { $structure = [ 'userid' => new external_value(PARAM_INT, 'The user id'), 'attempts' => new external_multiple_structure(self::get_attempt_returns(), 'The complete attempts list'), 'scored' => new external_single_structure([ 'title' => new external_value(PARAM_NOTAGS, 'Scored attempts title'), 'grademethod' => new external_value(PARAM_NOTAGS, 'Scored attempts title'), 'attempts' => new external_multiple_structure(self::get_attempt_returns(), 'List of the grading attempts'), ], 'Attempts used to grade the activity', VALUE_OPTIONAL), ]; return new external_single_structure($structure); } /** * Return the external structure of an attempt. * * @return external_single_structure */ private static function get_attempt_returns(): external_single_structure { $result = new external_single_structure([ 'id' => new external_value(PARAM_INT, 'ID of the context'), 'h5pactivityid' => new external_value(PARAM_INT, 'ID of the H5P activity'), 'userid' => new external_value(PARAM_INT, 'ID of the user'), 'timecreated' => new external_value(PARAM_INT, 'Attempt creation'), 'timemodified' => new external_value(PARAM_INT, 'Attempt modified'), 'attempt' => new external_value(PARAM_INT, 'Attempt number'), 'rawscore' => new external_value(PARAM_INT, 'Attempt score value'), 'maxscore' => new external_value(PARAM_INT, 'Attempt max score'), 'duration' => new external_value(PARAM_INT, 'Attempt duration in seconds'), 'completion' => new external_value(PARAM_INT, 'Attempt completion', VALUE_OPTIONAL), 'success' => new external_value(PARAM_INT, 'Attempt success', VALUE_OPTIONAL), 'scaled' => new external_value(PARAM_FLOAT, 'Attempt scaled'), ]); return $result; } } PK1B\9.L  %classes/external/view_h5pactivity.phpnu[. namespace mod_h5pactivity\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use context_module; use mod_h5pactivity\local\manager; /** * This is the external method for triggering the course module viewed event. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class view_h5pactivity extends external_api { /** * Parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'h5pactivityid' => new external_value(PARAM_INT, 'H5P activity instance id') ] ); } /** * Trigger the course module viewed event and update the module completion status. * * @param int $h5pactivityid The h5p activity id. * @return array of warnings and the access information * @since Moodle 3.9 * @throws moodle_exception */ public static function execute(int $h5pactivityid): array { $params = external_api::validate_parameters(self::execute_parameters(), [ 'h5pactivityid' => $h5pactivityid ]); $warnings = []; // Request and permission validation. list($course, $cm) = get_course_and_cm_from_instance($params['h5pactivityid'], 'h5pactivity'); $context = context_module::instance($cm->id); self::validate_context($context); $manager = manager::create_from_coursemodule($cm); $manager->set_module_viewed($course); $result = array( 'status' => true, 'warnings' => $warnings, ); return $result; } /** * Describes the view_h5pactivity return value. * * @return external_single_structure * @since Moodle 3.9 */ public static function execute_returns() { return new external_single_structure( [ 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ] ); } } PK1B\21classes/external/get_h5pactivities_by_courses.phpnu[. namespace mod_h5pactivity\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use context_module; use core_h5p\factory; /** * This is the external method for returning a list of h5p activities. * * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_h5pactivities_by_courses extends external_api { /** * Parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters ( [ 'courseids' => new external_multiple_structure( new external_value(PARAM_INT, 'Course id'), 'Array of course ids', VALUE_DEFAULT, [] ), ] ); } /** * Returns a list of h5p activities in a provided list of courses. * If no list is provided all h5p activities that the user can view will be returned. * * @param array $courseids course ids * @return array of h5p activities and warnings * @since Moodle 3.9 */ public static function execute(array $courseids): array { global $PAGE; $warnings = []; $returnedh5pactivities = []; $params = external_api::validate_parameters(self::execute_parameters(), [ 'courseids' => $courseids ]); $mycourses = []; if (empty($params['courseids'])) { $mycourses = enrol_get_my_courses(); $params['courseids'] = array_keys($mycourses); } // Ensure there are courseids to loop through. if (!empty($params['courseids'])) { $factory = new factory(); list($courses, $warnings) = \core_external\util::validate_courses($params['courseids'], $mycourses); $output = $PAGE->get_renderer('core'); // Get the h5p activities in this course, this function checks users visibility permissions. // We can avoid then additional validate_context calls. $h5pactivities = get_all_instances_in_courses('h5pactivity', $courses); foreach ($h5pactivities as $h5pactivity) { $context = context_module::instance($h5pactivity->coursemodule); // Remove fields that are not from the h5p activity (added by get_all_instances_in_courses). unset($h5pactivity->coursemodule, $h5pactivity->context, $h5pactivity->visible, $h5pactivity->section, $h5pactivity->groupmode, $h5pactivity->groupingid); $exporter = new h5pactivity_summary_exporter($h5pactivity, ['context' => $context, 'factory' => $factory]); $summary = $exporter->export($output); $returnedh5pactivities[] = $summary; } } $h5pglobalsettings = [ 'enablesavestate' => get_config('mod_h5pactivity', 'enablesavestate'), ]; if (!empty($h5pglobalsettings['enablesavestate'])) { $h5pglobalsettings['savestatefreq'] = get_config('mod_h5pactivity', 'savestatefreq'); } $result = [ 'h5pactivities' => $returnedh5pactivities, 'h5pglobalsettings' => $h5pglobalsettings, 'warnings' => $warnings ]; return $result; } /** * Describes the get_h5pactivities_by_courses return value. * * @return external_single_structure * @since Moodle 3.9 */ public static function execute_returns() { return new external_single_structure( [ 'h5pactivities' => new external_multiple_structure( h5pactivity_summary_exporter::get_read_structure() ), 'h5pglobalsettings' => new external_single_structure( [ 'enablesavestate' => new external_value(PARAM_BOOL, 'Whether saving state is enabled.'), 'savestatefreq' => new external_value(PARAM_INT, 'How often (in seconds) state is saved.', VALUE_OPTIONAL), ], 'H5P global settings', VALUE_OPTIONAL, ), 'warnings' => new external_warnings(), ] ); } } PK1B\&classes/external/log_report_viewed.phpnu[. namespace mod_h5pactivity\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use mod_h5pactivity\local\manager; use mod_h5pactivity\event\report_viewed; use context_module; use stdClass; /** * This is the external method for logging that the h5pactivity was viewed. * * @package mod_h5pactivity * @copyright 2021 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 3.11 */ class log_report_viewed extends external_api { /** * Webservice parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'h5pactivityid' => new external_value(PARAM_INT, 'h5p activity instance id'), 'userid' => new external_value( PARAM_INT, 'The user id to log attempt (null means only current user)', VALUE_DEFAULT ), 'attemptid' => new external_value(PARAM_INT, 'The attempt id', VALUE_DEFAULT), ] ); } /** * Logs that the h5pactivity was viewed. * * @throws moodle_exception if the user cannot see the report * @param int $h5pactivityid The h5p activity id * @param int|null $userid The user id * @param int|null $attemptid The attempt id * @return array of warnings and status result */ public static function execute(int $h5pactivityid, ?int $userid = null, ?int $attemptid = null): stdClass { $params = external_api::validate_parameters(self::execute_parameters(), [ 'h5pactivityid' => $h5pactivityid, 'userid' => $userid, 'attemptid' => $attemptid, ]); $h5pactivityid = $params['h5pactivityid']; $userid = $params['userid']; $attemptid = $params['attemptid']; // Request and permission validation. list ($course, $cm) = get_course_and_cm_from_instance($h5pactivityid, 'h5pactivity'); $context = context_module::instance($cm->id); self::validate_context($context); $manager = manager::create_from_coursemodule($cm); $instance = $manager->get_instance(); // Trigger event. $other = [ 'instanceid' => $instance->id, 'userid' => $userid, 'attemptid' => $attemptid, ]; $event = report_viewed::create([ 'objectid' => $instance->id, 'context' => $context, 'other' => $other, ]); $event->trigger(); $result = (object)[ 'status' => true, 'warnings' => [], ]; return $result; } /** * Describes the report_viewed return value. * * @return external_single_structure * @since Moodle 3.11 */ public static function execute_returns() { return new external_single_structure( [ 'status' => new external_value(PARAM_BOOL, 'status: true if success'), 'warnings' => new external_warnings() ] ); } } PK1B\j1classes/external/h5pactivity_summary_exporter.phpnu[. /** * Class for exporting h5p activity data. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\external; use core\external\exporter; use renderer_base; use core_external\util as external_util; use core_external\external_files; use core_h5p\api; /** * Class for exporting h5p activity data. * * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class h5pactivity_summary_exporter extends exporter { /** * Properties definition. * * @return array */ protected static function define_properties() { return [ 'id' => [ 'type' => PARAM_INT, 'description' => 'The primary key of the record.', ], 'course' => [ 'type' => PARAM_INT, 'description' => 'Course id this h5p activity is part of.', ], 'name' => [ 'type' => PARAM_TEXT, 'description' => 'The name of the activity module instance.', ], 'timecreated' => [ 'type' => PARAM_INT, 'description' => 'Timestamp of when the instance was added to the course.', 'optional' => true, ], 'timemodified' => [ 'type' => PARAM_INT, 'description' => 'Timestamp of when the instance was last modified.', 'optional' => true, ], 'intro' => [ 'default' => '', 'type' => PARAM_RAW, 'description' => 'H5P activity description.', 'null' => NULL_ALLOWED, ], 'introformat' => [ 'choices' => [FORMAT_HTML, FORMAT_MOODLE, FORMAT_PLAIN, FORMAT_MARKDOWN], 'type' => PARAM_INT, 'default' => FORMAT_MOODLE, 'description' => 'The format of the intro field.', ], 'grade' => [ 'type' => PARAM_INT, 'default' => 0, 'description' => 'The maximum grade for submission.', 'optional' => true, ], 'displayoptions' => [ 'type' => PARAM_INT, 'default' => 0, 'description' => 'H5P Button display options.', ], 'enabletracking' => [ 'type' => PARAM_INT, 'default' => 1, 'description' => 'Enable xAPI tracking.', ], 'grademethod' => [ 'type' => PARAM_INT, 'default' => 1, 'description' => 'Which H5P attempt is used for grading.', ], 'contenthash' => [ 'type' => PARAM_ALPHANUM, 'description' => 'Sha1 hash of file content.', 'optional' => true, ], ]; } /** * Related objects definition. * * @return array */ protected static function define_related() { return [ 'context' => 'context', 'factory' => 'core_h5p\\factory' ]; } /** * Other properties definition. * * @return array */ protected static function define_other_properties() { return [ 'coursemodule' => [ 'type' => PARAM_INT ], 'context' => [ 'type' => PARAM_INT ], 'introfiles' => [ 'type' => external_files::get_properties_for_exporter(), 'multiple' => true ], 'package' => [ 'type' => external_files::get_properties_for_exporter(), 'multiple' => true ], 'deployedfile' => [ 'optional' => true, 'description' => 'H5P file deployed.', 'type' => [ 'filename' => array( 'type' => PARAM_FILE, 'description' => 'File name.', 'optional' => true, 'null' => NULL_NOT_ALLOWED, ), 'filepath' => array( 'type' => PARAM_PATH, 'description' => 'File path.', 'optional' => true, 'null' => NULL_NOT_ALLOWED, ), 'filesize' => array( 'type' => PARAM_INT, 'description' => 'File size.', 'optional' => true, 'null' => NULL_NOT_ALLOWED, ), 'fileurl' => array( 'type' => PARAM_URL, 'description' => 'Downloadable file url.', 'optional' => true, 'null' => NULL_NOT_ALLOWED, ), 'timemodified' => array( 'type' => PARAM_INT, 'description' => 'Time modified.', 'optional' => true, 'null' => NULL_NOT_ALLOWED, ), 'mimetype' => array( 'type' => PARAM_RAW, 'description' => 'File mime type.', 'optional' => true, 'null' => NULL_NOT_ALLOWED, ) ] ], ]; } /** * Assign values to the defined other properties. * * @param renderer_base $output The output renderer object. * @return array */ protected function get_other_values(renderer_base $output) { $context = $this->related['context']; $factory = $this->related['factory']; $values = [ 'coursemodule' => $context->instanceid, 'context' => $context->id, ]; $values['introfiles'] = external_util::get_area_files($context->id, 'mod_h5pactivity', 'intro', false, false); $values['package'] = external_util::get_area_files($context->id, 'mod_h5pactivity', 'package', false, true); // Only if this H5P activity has been deployed, return the exported file. $fileh5p = api::get_export_info_from_context_id($context->id, $factory, 'mod_h5pactivity', 'package'); if ($fileh5p) { $values['deployedfile'] = $fileh5p; } return $values; } /** * Get the formatting parameters for the intro. * * @return array with the formatting parameters */ protected function get_format_parameters_for_intro() { return [ 'component' => 'mod_h5pactivity', 'filearea' => 'intro', 'options' => ['noclean' => true], ]; } /** * Get the formatting parameters for the package. * * @return array with the formatting parameters */ protected function get_format_parameters_for_package() { return [ 'component' => 'mod_h5pactivity', 'filearea' => 'package', 'itemid' => 0, 'options' => ['noclean' => true], ]; } } PK1B\[ڢ22 classes/external/get_results.phpnu[. namespace mod_h5pactivity\external; use mod_h5pactivity\local\manager; use mod_h5pactivity\local\report\results as report_results; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_multiple_structure; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use context_module; use stdClass; /** * This is the external method for getting the information needed to present a results report. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_results extends external_api { /** * Webservice parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'h5pactivityid' => new external_value(PARAM_INT, 'h5p activity instance id'), 'attemptids' => new external_multiple_structure( new external_value(PARAM_INT, 'The attempt id'), 'Attempt ids', VALUE_DEFAULT, [] ), ] ); } /** * Return user attempts results information in a h5p activity. * * In case an empty array of attempt ids is passed, the method will load all * activity attempts from the current user. * * @throws moodle_exception if the user cannot see the report * @param int $h5pactivityid The h5p activity id * @param int[] $attemptids The attempt ids * @return stdClass report data */ public static function execute(int $h5pactivityid, array $attemptids = []): stdClass { global $USER; $params = external_api::validate_parameters(self::execute_parameters(), [ 'h5pactivityid' => $h5pactivityid, 'attemptids' => $attemptids, ]); $h5pactivityid = $params['h5pactivityid']; $attemptids = $params['attemptids']; $warnings = []; // Request and permission validation. list ($course, $cm) = get_course_and_cm_from_instance($h5pactivityid, 'h5pactivity'); $context = context_module::instance($cm->id); self::validate_context($context); $manager = manager::create_from_coursemodule($cm); if (empty($attemptids)) { $attemptids = []; foreach ($manager->get_user_attempts($USER->id) as $attempt) { $attemptids[] = $attempt->get_id(); } } $attempts = []; foreach ($attemptids as $attemptid) { $report = $manager->get_report(null, $attemptid); if ($report && $report instanceof report_results) { $attempts[] = self::export_attempt($report); } else { $warnings[] = [ 'item' => 'h5pactivity_attempts', 'itemid' => $attemptid, 'warningcode' => '1', 'message' => "Cannot access attempt", ]; } } $result = (object)[ 'activityid' => $h5pactivityid, 'attempts' => $attempts, 'warnings' => $warnings, ]; return $result; } /** * Return a data object from an attempt. * * @param report_results $report the attempt data * @return stdClass a WS compatible version of the attempt */ private static function export_attempt(report_results $report): stdClass { $data = $report->export_data_for_external(); $attemptdata = $data->attempt; $attempt = (object)[ 'id' => $attemptdata->id, 'h5pactivityid' => $attemptdata->h5pactivityid, 'userid' => $attemptdata->userid, 'timecreated' => $attemptdata->timecreated, 'timemodified' => $attemptdata->timemodified, 'attempt' => $attemptdata->attempt, 'rawscore' => $attemptdata->rawscore, 'maxscore' => $attemptdata->maxscore, 'duration' => (empty($attemptdata->durationvalue)) ? 0 : $attemptdata->durationvalue, 'scaled' => (empty($attemptdata->scaled)) ? 0 : $attemptdata->scaled, 'results' => [], ]; if (isset($attemptdata->completion) && $attemptdata->completion !== null) { $attempt->completion = $attemptdata->completion; } if (isset($attemptdata->success) && $attemptdata->success !== null) { $attempt->success = $attemptdata->success; } foreach ($data->results as $result) { $attempt->results[] = self::export_result($result); } return $attempt; } /** * Return a data object from a result. * * @param stdClass $data the result data * @return stdClass a WS compatible version of the result */ private static function export_result(stdClass $data): stdClass { $result = (object)[ 'id' => $data->id, 'attemptid' => $data->attemptid, 'subcontent' => $data->subcontent, 'timecreated' => $data->timecreated, 'interactiontype' => $data->interactiontype, 'description' => $data->description, 'rawscore' => $data->rawscore, 'maxscore' => $data->maxscore, 'duration' => $data->duration, 'optionslabel' => $data->optionslabel ?? get_string('choice', 'mod_h5pactivity'), 'correctlabel' => $data->correctlabel ?? get_string('correct_answer', 'mod_h5pactivity'), 'answerlabel' => $data->answerlabel ?? get_string('attempt_answer', 'mod_h5pactivity'), 'track' => $data->track ?? false, ]; if (isset($data->completion) && $data->completion !== null) { $result->completion = $data->completion; } if (isset($data->success) && $data->success !== null) { $result->success = $data->success; } if (isset($data->options)) { $result->options = $data->options; } if (isset($data->content)) { $result->content = $data->content; } return $result; } /** * Describes the get_h5pactivity_access_information return value. * * @return external_single_structure */ public static function execute_returns(): external_single_structure { return new external_single_structure([ 'activityid' => new external_value(PARAM_INT, 'Activity course module ID'), 'attempts' => new external_multiple_structure( self::get_attempt_returns(), 'The complete attempts list' ), 'warnings' => new external_warnings(), ], 'Activity attempts results data'); } /** * Return the external structure of an attempt * @return external_single_structure */ private static function get_attempt_returns(): external_single_structure { $result = new external_single_structure([ 'id' => new external_value(PARAM_INT, 'ID of the context'), 'h5pactivityid' => new external_value(PARAM_INT, 'ID of the H5P activity'), 'userid' => new external_value(PARAM_INT, 'ID of the user'), 'timecreated' => new external_value(PARAM_INT, 'Attempt creation'), 'timemodified' => new external_value(PARAM_INT, 'Attempt modified'), 'attempt' => new external_value(PARAM_INT, 'Attempt number'), 'rawscore' => new external_value(PARAM_INT, 'Attempt score value'), 'maxscore' => new external_value(PARAM_INT, 'Attempt max score'), 'duration' => new external_value(PARAM_INT, 'Attempt duration in seconds'), 'completion' => new external_value(PARAM_INT, 'Attempt completion', VALUE_OPTIONAL), 'success' => new external_value(PARAM_INT, 'Attempt success', VALUE_OPTIONAL), 'scaled' => new external_value(PARAM_FLOAT, 'Attempt scaled'), 'results' => new external_multiple_structure( self::get_result_returns(), 'The results of the attempt', VALUE_OPTIONAL ), ], 'The attempt general information'); return $result; } /** * Return the external structure of a result * @return external_single_structure */ private static function get_result_returns(): external_single_structure { $result = new external_single_structure([ 'id' => new external_value(PARAM_INT, 'ID of the context'), 'attemptid' => new external_value(PARAM_INT, 'ID of the H5P attempt'), 'subcontent' => new external_value(PARAM_NOTAGS, 'Subcontent identifier'), 'timecreated' => new external_value(PARAM_INT, 'Result creation'), 'interactiontype' => new external_value(PARAM_NOTAGS, 'Interaction type'), 'description' => new external_value(PARAM_RAW, 'Result description'), 'content' => new external_value(PARAM_RAW, 'Result extra content', VALUE_OPTIONAL), 'rawscore' => new external_value(PARAM_INT, 'Result score value'), 'maxscore' => new external_value(PARAM_INT, 'Result max score'), 'duration' => new external_value(PARAM_INT, 'Result duration in seconds', VALUE_OPTIONAL, 0), 'completion' => new external_value(PARAM_INT, 'Result completion', VALUE_OPTIONAL), 'success' => new external_value(PARAM_INT, 'Result success', VALUE_OPTIONAL), 'optionslabel' => new external_value(PARAM_NOTAGS, 'Label used for result options', VALUE_OPTIONAL), 'correctlabel' => new external_value(PARAM_NOTAGS, 'Label used for correct answers', VALUE_OPTIONAL), 'answerlabel' => new external_value(PARAM_NOTAGS, 'Label used for user answers', VALUE_OPTIONAL), 'track' => new external_value(PARAM_BOOL, 'If the result has valid track information', VALUE_OPTIONAL), 'options' => new external_multiple_structure( new external_single_structure([ 'description' => new external_value(PARAM_RAW, 'Option description', VALUE_OPTIONAL), 'id' => new external_value(PARAM_TEXT, 'Option string identifier', VALUE_OPTIONAL), 'correctanswer' => self::get_answer_returns('The option correct answer', VALUE_OPTIONAL), 'useranswer' => self::get_answer_returns('The option user answer', VALUE_OPTIONAL), ]), 'The statement options', VALUE_OPTIONAL ), ], 'A single result statement tracking information'); return $result; } /** * Return the external structure of an answer or correctanswer * * @param string $description the return description * @param int $required the return required value * @return external_single_structure */ private static function get_answer_returns(string $description, int $required = VALUE_REQUIRED): external_single_structure { $result = new external_single_structure([ 'answer' => new external_value(PARAM_NOTAGS, 'Option text value', VALUE_OPTIONAL), 'correct' => new external_value(PARAM_BOOL, 'If has to be displayed as correct', VALUE_OPTIONAL), 'incorrect' => new external_value(PARAM_BOOL, 'If has to be displayed as incorrect', VALUE_OPTIONAL), 'text' => new external_value(PARAM_BOOL, 'If has to be displayed as simple text', VALUE_OPTIONAL), 'checked' => new external_value(PARAM_BOOL, 'If has to be displayed as a checked option', VALUE_OPTIONAL), 'unchecked' => new external_value(PARAM_BOOL, 'If has to be displayed as a unchecked option', VALUE_OPTIONAL), 'pass' => new external_value(PARAM_BOOL, 'If has to be displayed as passed', VALUE_OPTIONAL), 'fail' => new external_value(PARAM_BOOL, 'If has to be displayed as failed', VALUE_OPTIONAL), ], $description, $required); return $result; } } PK1B\_cc7classes/external/get_h5pactivity_access_information.phpnu[. namespace mod_h5pactivity\external; use core_external\external_api; use core_external\external_function_parameters; use core_external\external_single_structure; use core_external\external_value; use core_external\external_warnings; use context_module; use mod_h5pactivity\local\manager; /** * This is the external method for getting access information for a h5p activity. * * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class get_h5pactivity_access_information extends external_api { /** * Parameters. * * @return external_function_parameters */ public static function execute_parameters(): external_function_parameters { return new external_function_parameters( [ 'h5pactivityid' => new external_value(PARAM_INT, 'h5p activity instance id') ] ); } /** * Return access information for a given h5p activity. * * @param int $h5pactivityid The h5p activity id. * @return array of warnings and the access information * @since Moodle 3.9 * @throws moodle_exception */ public static function execute(int $h5pactivityid): array { global $DB; $params = external_api::validate_parameters(self::execute_parameters(), [ 'h5pactivityid' => $h5pactivityid ]); // Request and permission validation. $h5pactivity = $DB->get_record('h5pactivity', ['id' => $params['h5pactivityid']], '*', MUST_EXIST); list($course, $cm) = get_course_and_cm_from_instance($h5pactivity, 'h5pactivity'); $context = context_module::instance($cm->id); self::validate_context($context); $result = []; // Return all the available capabilities. $manager = manager::create_from_coursemodule($cm); $capabilities = load_capability_def('mod_h5pactivity'); foreach ($capabilities as $capname => $capdata) { $field = 'can' . str_replace('mod/h5pactivity:', '', $capname); // For mod/h5pactivity:submit we need to check if tracking is enabled in the h5pactivity for the current user. if ($field == 'cansubmit') { $result[$field] = $manager->is_tracking_enabled() && $manager->can_submit(); } else { $result[$field] = has_capability($capname, $context); } } $result['warnings'] = []; return $result; } /** * Describes the get_h5pactivity_access_information return value. * * @return external_single_structure * @since Moodle 3.9 */ public static function execute_returns() { $structure = [ 'warnings' => new external_warnings() ]; $capabilities = load_capability_def('mod_h5pactivity'); foreach ($capabilities as $capname => $capdata) { $field = 'can' . str_replace('mod/h5pactivity:', '', $capname); $structure[$field] = new external_value(PARAM_BOOL, 'Whether the user has the capability ' . $capname . ' allowed.', VALUE_OPTIONAL); } return new external_single_structure($structure); } } PK1B\5bs00classes/privacy/provider.phpnu[. namespace mod_h5pactivity\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request\approved_contextlist; use core_privacy\local\request\approved_userlist; use core_privacy\local\request\contextlist; use core_privacy\local\request\helper; use core_privacy\local\request\transform; use core_privacy\local\request\userlist; use core_privacy\local\request\writer; use stdClass; /** * Privacy API implementation for the H5P activity plugin. * * @package mod_h5pactivity * @category privacy * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\core_userlist_provider, \core_privacy\local\request\plugin\provider { /** * Return the fields which contain personal data. * * @param collection $collection The initialised collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { $collection->add_database_table('h5pactivity_attempts', [ 'userid' => 'privacy:metadata:userid', 'attempt' => 'privacy:metadata:attempt', 'timecreated' => 'privacy:metadata:timecreated', 'timemodified' => 'privacy:metadata:timemodified', 'rawscore' => 'privacy:metadata:rawscore', ], 'privacy:metadata:xapi_track'); $collection->add_database_table('h5pactivity_attempts_results', [ 'attempt' => 'privacy:metadata:attempt', 'timecreated' => 'privacy:metadata:timecreated', 'rawscore' => 'privacy:metadata:rawscore', ], 'privacy:metadata:xapi_track_results'); $collection->add_subsystem_link('core_xapi', [], 'privacy:metadata:xapisummary'); return $collection; } /** * Get the list of contexts that contain user information for the specified user. * * @param int $userid The user to search. * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin. */ public static function get_contexts_for_userid(int $userid): contextlist { $sql = "SELECT ctx.id FROM {h5pactivity_attempts} ss JOIN {modules} m ON m.name = :activityname JOIN {course_modules} cm ON cm.instance = ss.h5pactivityid AND cm.module = m.id JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :modlevel WHERE ss.userid = :userid"; $params = ['activityname' => 'h5pactivity', 'modlevel' => CONTEXT_MODULE, 'userid' => $userid]; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); \core_xapi\privacy\provider::add_contexts_for_userid($contextlist, $userid, 'mod_h5pactivity'); return $contextlist; } /** * Get the list of users who have data within a context. * * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination. */ public static function get_users_in_context(userlist $userlist) { $context = $userlist->get_context(); if (!is_a($context, \context_module::class)) { return; } $sql = "SELECT ss.userid FROM {h5pactivity_attempts} ss JOIN {modules} m ON m.name = 'h5pactivity' JOIN {course_modules} cm ON cm.instance = ss.h5pactivityid AND cm.module = m.id JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :modlevel WHERE ctx.id = :contextid"; $params = ['modlevel' => CONTEXT_MODULE, 'contextid' => $context->id]; $userlist->add_from_sql('userid', $sql, $params); \core_xapi\privacy\provider::add_userids_for_context($userlist); } /** * Export all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts to export information for. */ public static function export_user_data(approved_contextlist $contextlist) { global $DB; // Remove contexts different from CONTEXT_MODULE. $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) { if ($context->contextlevel == CONTEXT_MODULE) { $carry[] = $context->id; } return $carry; }, []); if (empty($contexts)) { return; } $user = $contextlist->get_user(); $userid = $user->id; // Get H5P attempts data. foreach ($contexts as $contextid) { $context = \context::instance_by_id($contextid); $data = helper::get_context_data($context, $user); writer::with_context($context)->export_data([], $data); helper::export_context_files($context, $user); // Get user's xAPI state data for the particular context. $state = \core_xapi\privacy\provider::get_xapi_states_for_user($contextlist->get_user()->id, 'mod_h5pactivity', $context->instanceid); if ($state) { // If the activity has xAPI state data by the user, include it in the export. writer::with_context($context)->export_data( [get_string('privacy:xapistate', 'core_xapi')], (object) $state); } } // Get attempts track data. list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED); $sql = "SELECT har.id, ha.attempt, har.description, har.interactiontype, har.response, har.additionals, har.rawscore, har.maxscore, har.duration, har.timecreated, ctx.id as contextid FROM {h5pactivity_attempts_results} har JOIN {h5pactivity_attempts} ha ON har.attemptid = ha.id JOIN {course_modules} cm ON cm.instance = ha.h5pactivityid JOIN {context} ctx ON ctx.instanceid = cm.id WHERE ctx.id $insql AND ha.userid = :userid"; $params = array_merge($inparams, ['userid' => $userid]); $alldata = []; $attemptsdata = $DB->get_recordset_sql($sql, $params); foreach ($attemptsdata as $track) { $alldata[$track->contextid][$track->attempt][] = (object)[ 'description' => $track->description, 'response' => $track->response, 'interactiontype' => $track->interactiontype, 'additionals' => $track->additionals, 'rawscore' => $track->rawscore, 'maxscore' => $track->maxscore, 'duration' => $track->duration, 'timecreated' => transform::datetime($track->timecreated), ]; } $attemptsdata->close(); // The result data is organised in: // {Course name}/{H5P activity name}/{My attempts}/{Attempt X}/data.json // where X is the attempt number. array_walk($alldata, function($attemptsdata, $contextid) { $context = \context::instance_by_id($contextid); array_walk($attemptsdata, function($data, $attempt) use ($context) { $subcontext = [ get_string('myattempts', 'mod_h5pactivity'), get_string('attempt', 'mod_h5pactivity'). " $attempt" ]; writer::with_context($context)->export_data( $subcontext, (object)['results' => $data] ); }); }); } /** * Delete all user data which matches the specified context. * * @param \context $context A user context. */ public static function delete_data_for_all_users_in_context(\context $context) { // This should not happen, but just in case. if ($context->contextlevel != CONTEXT_MODULE) { return; } $cm = get_coursemodule_from_id('h5pactivity', $context->instanceid); if (!$cm) { // Only h5pactivity module will be handled. return; } self::delete_all_attempts($cm); // Delete xAPI state data. \core_xapi\privacy\provider::delete_states_for_all_users($context, 'mod_h5pactivity'); } /** * Delete all user data for the specified user, in the specified contexts. * * @param approved_contextlist $contextlist The approved contexts and user information to delete information for. */ public static function delete_data_for_user(approved_contextlist $contextlist) { foreach ($contextlist as $context) { if ($context->contextlevel != CONTEXT_MODULE) { continue; } $cm = get_coursemodule_from_id('h5pactivity', $context->instanceid); if (!$cm) { // Only h5pactivity module will be handled. continue; } $user = $contextlist->get_user(); self::delete_all_attempts($cm, $user); // Delete xAPI state data. \core_xapi\privacy\provider::delete_states_for_user($contextlist, 'mod_h5pactivity'); } } /** * Delete multiple users within a single context. * * @param approved_userlist $userlist The approved context and user information to delete information for. */ public static function delete_data_for_users(approved_userlist $userlist) { $context = $userlist->get_context(); if (!is_a($context, \context_module::class)) { return; } $cm = get_coursemodule_from_id('h5pactivity', $context->instanceid); if (!$cm) { // Only h5pactivity module will be handled. return; } $userids = $userlist->get_userids(); foreach ($userids as $userid) { self::delete_all_attempts ($cm, (object)['id' => $userid]); } // Delete xAPI states data. \core_xapi\privacy\provider::delete_states_for_userlist($userlist); } /** * Wipe all attempt data for specific course_module and an optional user. * * @param stdClass $cm a course_module record * @param stdClass $user a user record */ private static function delete_all_attempts(stdClass $cm, ?stdClass $user = null): void { global $DB; $where = 'a.h5pactivityid = :h5pactivityid'; $conditions = ['h5pactivityid' => $cm->instance]; if (!empty($user)) { $where .= ' AND a.userid = :userid'; $conditions['userid'] = $user->id; } $DB->delete_records_select('h5pactivity_attempts_results', "attemptid IN ( SELECT a.id FROM {h5pactivity_attempts} a WHERE $where)", $conditions); $DB->delete_records('h5pactivity_attempts', $conditions); } } PK1B\&nSclasses/local/grader.phpnu[. /** * H5P activity grader class. * * @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 context_module; use cm_info; use moodle_recordset; use stdClass; /** * Class for handling H5P activity grading. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class grader { /** @var stdClass course_module record. */ private $instance; /** @var string idnumber course_modules idnumber. */ private $idnumber; /** * Class contructor. * * @param stdClass $instance H5Pactivity instance object * @param string $idnumber course_modules idnumber */ public function __construct(stdClass $instance, string $idnumber = '') { $this->instance = $instance; $this->idnumber = $idnumber; } /** * Delete grade item for given mod_h5pactivity instance. * * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED */ public function grade_item_delete(): ?int { global $CFG; require_once($CFG->libdir.'/gradelib.php'); return grade_update('mod/h5pactivity', $this->instance->course, 'mod', 'h5pactivity', $this->instance->id, 0, null, ['deleted' => 1]); } /** * Creates or updates grade item for the given mod_h5pactivity instance. * * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook * @return int 0 if ok, error code otherwise */ public function grade_item_update($grades = null): int { global $CFG; require_once($CFG->libdir.'/gradelib.php'); $item = []; $item['itemname'] = clean_param($this->instance->name, PARAM_NOTAGS); $item['gradetype'] = GRADE_TYPE_VALUE; if (!empty($this->idnumber)) { $item['idnumber'] = $this->idnumber; } if ($this->instance->grade > 0) { $item['gradetype'] = GRADE_TYPE_VALUE; $item['grademax'] = $this->instance->grade; $item['grademin'] = 0; } else if ($this->instance->grade < 0) { $item['gradetype'] = GRADE_TYPE_SCALE; $item['scaleid'] = -$this->instance->grade; } else { $item['gradetype'] = GRADE_TYPE_NONE; } if ($grades === 'reset') { $item['reset'] = true; $grades = null; } return grade_update('mod/h5pactivity', $this->instance->course, 'mod', 'h5pactivity', $this->instance->id, 0, $grades, $item); } /** * Update grades in the gradebook. * * @param int $userid Update grade of specific user only, 0 means all participants. */ public function update_grades(int $userid = 0): void { // Scaled and none grading doesn't have grade calculation. if ($this->instance->grade <= 0) { $this->grade_item_update(); return; } // Populate array of grade objects indexed by userid. $grades = $this->get_user_grades_for_gradebook($userid); if (!empty($grades)) { $this->grade_item_update($grades); } else { $this->grade_item_update(); } } /** * Get an updated list of user grades and feedback for the gradebook. * * @param int $userid int or 0 for all users * @return array of grade data formated for the gradebook api * The data required by the gradebook api is userid, * rawgrade, * feedback, * feedbackformat, * usermodified, * dategraded, * datesubmitted */ private function get_user_grades_for_gradebook(int $userid = 0): array { $grades = []; // In case of using manual grading this update must delete previous automatic gradings. if ($this->instance->grademethod == manager::GRADEMANUAL || !$this->instance->enabletracking) { return $this->get_user_grades_for_deletion($userid); } $manager = manager::create_from_instance($this->instance); $scores = $manager->get_users_scaled_score($userid); if (!$scores) { return $grades; } // Maxgrade depends on the type of grade used: // - grade > 0: regular quantitative grading. // - grade = 0: no grading. // - grade < 0: scale used. $maxgrade = floatval($this->instance->grade); // Convert scaled scores into gradebok compatible objects. foreach ($scores as $userid => $score) { $grades[$userid] = [ 'userid' => $userid, 'rawgrade' => $maxgrade * $score->scaled, 'dategraded' => $score->timemodified, 'datesubmitted' => $score->timemodified, ]; } return $grades; } /** * Get an deletion list of user grades and feedback for the gradebook. * * This method is used to delete all autmatic gradings when grading method is set to manual. * * @param int $userid int or 0 for all users * @return array of grade data formated for the gradebook api * The data required by the gradebook api is userid, * rawgrade (null to delete), * dategraded, * datesubmitted */ private function get_user_grades_for_deletion(int $userid = 0): array { $grades = []; if ($userid) { $grades[$userid] = [ 'userid' => $userid, 'rawgrade' => null, 'dategraded' => time(), 'datesubmitted' => time(), ]; } else { $manager = manager::create_from_instance($this->instance); $users = get_enrolled_users($manager->get_context(), 'mod/h5pactivity:submit'); foreach ($users as $user) { $grades[$user->id] = [ 'userid' => $user->id, 'rawgrade' => null, 'dategraded' => time(), 'datesubmitted' => time(), ]; } } return $grades; } } PK1B\!H??classes/local/attempt.phpnu[. /** * H5P activity attempt object * * @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 core_xapi\handler; use stdClass; use core_xapi\local\statement; /** * Class attempt for H5P activity * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class attempt { /** @var stdClass the h5pactivity_attempts record. */ private $record; /** @var boolean if the DB statement has been updated. */ private $scoreupdated = false; /** * Create a new attempt object. * * @param stdClass $record the h5pactivity_attempts record */ public function __construct(stdClass $record) { $this->record = $record; } /** * Create a new user attempt in a specific H5P activity. * * @param stdClass $user a user record * @param stdClass $cm a course_module record * @return attempt|null a new attempt object or null if fail */ public static function new_attempt(stdClass $user, stdClass $cm): ?attempt { global $DB; $record = new stdClass(); $record->h5pactivityid = $cm->instance; $record->userid = $user->id; $record->timecreated = time(); $record->timemodified = $record->timecreated; $record->rawscore = 0; $record->maxscore = 0; $record->duration = 0; $record->completion = null; $record->success = null; // Get last attempt number. $conditions = ['h5pactivityid' => $cm->instance, 'userid' => $user->id]; $countattempts = $DB->count_records('h5pactivity_attempts', $conditions); $record->attempt = $countattempts + 1; $record->id = $DB->insert_record('h5pactivity_attempts', $record); if (!$record->id) { return null; } // Remove any xAPI State associated to this attempt. $context = \context_module::instance($cm->id); $xapihandler = handler::create('mod_h5pactivity'); $xapihandler->wipe_states($context->id, $user->id); return new attempt($record); } /** * Get the last user attempt in a specific H5P activity. * * If no previous attempt exists, it generates a new one. * * @param stdClass $user a user record * @param stdClass $cm a course_module record * @return attempt|null a new attempt object or null if some problem accured */ public static function last_attempt(stdClass $user, stdClass $cm): ?attempt { global $DB; $conditions = ['h5pactivityid' => $cm->instance, 'userid' => $user->id]; $records = $DB->get_records('h5pactivity_attempts', $conditions, 'attempt DESC', '*', 0, 1); if (empty($records)) { return self::new_attempt($user, $cm); } return new attempt(array_shift($records)); } /** * Wipe all attempt data for specific course_module and an optional user. * * @param stdClass $cm a course_module record * @param stdClass $user a user record */ public static function delete_all_attempts(stdClass $cm, ?stdClass $user = null): void { global $DB; $where = 'a.h5pactivityid = :h5pactivityid'; $conditions = ['h5pactivityid' => $cm->instance]; if (!empty($user)) { $where .= ' AND a.userid = :userid'; $conditions['userid'] = $user->id; } $DB->delete_records_select('h5pactivity_attempts_results', "attemptid IN ( SELECT a.id FROM {h5pactivity_attempts} a WHERE $where)", $conditions); $DB->delete_records('h5pactivity_attempts', $conditions); } /** * Delete a specific attempt. * * @param attempt $attempt the attempt object to delete */ public static function delete_attempt(attempt $attempt): void { global $DB; $attempt->delete_results(); $DB->delete_records('h5pactivity_attempts', ['id' => $attempt->get_id()]); } /** * Save a new result statement into the attempt. * * It also updates the rawscore and maxscore if necessary. * * @param statement $statement the xAPI statement object * @param string $subcontent = '' optional subcontent identifier * @return bool if it can save the statement into db */ public function save_statement(statement $statement, string $subcontent = ''): bool { global $DB; // Check statement data. $xapiobject = $statement->get_object(); if (empty($xapiobject)) { return false; } $xapiresult = $statement->get_result(); $xapidefinition = $xapiobject->get_definition(); if (empty($xapidefinition) || empty($xapiresult)) { return false; } $xapicontext = $statement->get_context(); if ($xapicontext) { $context = $xapicontext->get_data(); } else { $context = new stdClass(); } $definition = $xapidefinition->get_data(); $result = $xapiresult->get_data(); $duration = $xapiresult->get_duration(); // Insert attempt_results record. $record = new stdClass(); $record->attemptid = $this->record->id; $record->subcontent = $subcontent; $record->timecreated = time(); $record->interactiontype = $definition->interactionType ?? 'other'; $record->description = $this->get_description_from_definition($definition); $record->correctpattern = $this->get_correctpattern_from_definition($definition); $record->response = $result->response ?? ''; $record->additionals = $this->get_additionals($definition, $context); $record->rawscore = 0; $record->maxscore = 0; if (isset($result->score)) { $record->rawscore = $result->score->raw ?? 0; $record->maxscore = $result->score->max ?? 0; } $record->duration = $duration; if (isset($result->completion)) { $record->completion = ($result->completion) ? 1 : 0; } if (isset($result->success)) { $record->success = ($result->success) ? 1 : 0; } if (!$DB->insert_record('h5pactivity_attempts_results', $record)) { return false; } // If no subcontent provided, results are propagated to the attempt itself. if (empty($subcontent)) { $this->set_duration($record->duration); $this->set_completion($record->completion ?? null); $this->set_success($record->success ?? null); // If Maxscore is not empty means that the rawscore is valid (even if it's 0) // and scaled score can be calculated. if ($record->maxscore) { $this->set_score($record->rawscore, $record->maxscore); } } // Refresh current attempt. return $this->save(); } /** * Update the current attempt record into DB. * * @return bool true if update is succesful */ public function save(): bool { global $DB; $this->record->timemodified = time(); // Calculate scaled score. if ($this->scoreupdated) { if (empty($this->record->maxscore)) { $this->record->scaled = 0; } else { $this->record->scaled = $this->record->rawscore / $this->record->maxscore; } } return $DB->update_record('h5pactivity_attempts', $this->record); } /** * Set the attempt score. * * @param int|null $rawscore the attempt rawscore * @param int|null $maxscore the attempt maxscore */ public function set_score(?int $rawscore, ?int $maxscore): void { $this->record->rawscore = $rawscore; $this->record->maxscore = $maxscore; $this->scoreupdated = true; } /** * Set the attempt duration. * * @param int|null $duration the attempt duration */ public function set_duration(?int $duration): void { $this->record->duration = $duration; } /** * Set the attempt completion. * * @param int|null $completion the attempt completion */ public function set_completion(?int $completion): void { $this->record->completion = $completion; } /** * Set the attempt success. * * @param int|null $success the attempt success */ public function set_success(?int $success): void { $this->record->success = $success; } /** * Delete the current attempt results from the DB. */ public function delete_results(): void { global $DB; $conditions = ['attemptid' => $this->record->id]; $DB->delete_records('h5pactivity_attempts_results', $conditions); } /** * Return de number of results stored in this attempt. * * @return int the number of results stored in this attempt. */ public function count_results(): int { global $DB; $conditions = ['attemptid' => $this->record->id]; return $DB->count_records('h5pactivity_attempts_results', $conditions); } /** * Return all results stored in this attempt. * * @return stdClass[] results records. */ public function get_results(): array { global $DB; $conditions = ['attemptid' => $this->record->id]; return $DB->get_records('h5pactivity_attempts_results', $conditions, 'id ASC'); } /** * Get additional data for some interaction types. * * @param stdClass $definition the statement object definition data * @param stdClass $context the statement optional context * @return string JSON encoded additional information */ private function get_additionals(stdClass $definition, stdClass $context): string { $additionals = []; $interactiontype = $definition->interactionType ?? 'other'; switch ($interactiontype) { case 'choice': case 'sequencing': $additionals['choices'] = $definition->choices ?? []; break; case 'matching': $additionals['source'] = $definition->source ?? []; $additionals['target'] = $definition->target ?? []; break; case 'likert': $additionals['scale'] = $definition->scale ?? []; break; case 'performance': $additionals['steps'] = $definition->steps ?? []; break; } $additionals['extensions'] = $definition->extensions ?? new stdClass(); // Add context extensions. $additionals['contextExtensions'] = $context->extensions ?? new stdClass(); if (empty($additionals)) { return ''; } return json_encode($additionals); } /** * Extract the result description from statement object definition. * * In principle, H5P package can send a multilang description but the reality * is that most activities only send the "en_US" description if any and the * activity does not have any control over it. * * @param stdClass $definition the statement object definition * @return string The available description if any */ private function get_description_from_definition(stdClass $definition): string { if (!isset($definition->description)) { return ''; } $translations = (array) $definition->description; if (empty($translations)) { return ''; } // By default, H5P packages only send "en-US" descriptions. return $translations['en-US'] ?? array_shift($translations); } /** * Extract the correct pattern from statement object definition. * * The correct pattern depends on the type of content and the plugin * has no control over it so we just store it in case that the statement * data have it. * * @param stdClass $definition the statement object definition * @return string The correct pattern if any */ private function get_correctpattern_from_definition(stdClass $definition): string { if (!isset($definition->correctResponsesPattern)) { return ''; } // Only arrays are allowed. if (is_array($definition->correctResponsesPattern)) { return json_encode($definition->correctResponsesPattern); } return ''; } /** * Return the attempt number. * * @return int the attempt number */ public function get_attempt(): int { return $this->record->attempt; } /** * Return the attempt ID. * * @return int the attempt id */ public function get_id(): int { return $this->record->id; } /** * Return the attempt user ID. * * @return int the attempt userid */ public function get_userid(): int { return $this->record->userid; } /** * Return the attempt H5P timecreated. * * @return int the attempt timecreated */ public function get_timecreated(): int { return $this->record->timecreated; } /** * Return the attempt H5P timemodified. * * @return int the attempt timemodified */ public function get_timemodified(): int { return $this->record->timemodified; } /** * Return the attempt H5P activity ID. * * @return int the attempt userid */ public function get_h5pactivityid(): int { return $this->record->h5pactivityid; } /** * Return the attempt maxscore. * * @return int the maxscore value */ public function get_maxscore(): int { return $this->record->maxscore; } /** * Return the attempt rawscore. * * @return int the rawscore value */ public function get_rawscore(): int { return $this->record->rawscore; } /** * Return the attempt duration. * * @return int|null the duration value */ public function get_duration(): ?int { return $this->record->duration; } /** * Return the attempt completion. * * @return int|null the completion value */ public function get_completion(): ?int { return $this->record->completion; } /** * Return the attempt success. * * @return int|null the success value */ public function get_success(): ?int { return $this->record->success; } /** * Return the attempt scaled. * * @return int|null the scaled value */ public function get_scaled(): ?int { return is_null($this->record->scaled) ? $this->record->scaled : (int)$this->record->scaled; } /** * Return if the attempt has been modified. * * Note: adding a result only add track information unless the statement does * not specify subcontent. In this case this will update also the statement. * * @return bool if the attempt score have been modified */ public function get_scoreupdated(): bool { return $this->scoreupdated; } } PK1B\nZiڻNNclasses/local/manager.phpnu[. /** * H5P activity manager class * * @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 mod_h5pactivity\local\report\participants; use mod_h5pactivity\local\report\attempts; use mod_h5pactivity\local\report\results; use context_module; use cm_info; use moodle_recordset; use core_user; use stdClass; use core\dml\sql_join; use mod_h5pactivity\event\course_module_viewed; /** * Class manager for H5P activity * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class manager { /** No automathic grading using attempt results. */ const GRADEMANUAL = 0; /** Use highest attempt results for grading. */ const GRADEHIGHESTATTEMPT = 1; /** Use average attempt results for grading. */ const GRADEAVERAGEATTEMPT = 2; /** Use last attempt results for grading. */ const GRADELASTATTEMPT = 3; /** Use first attempt results for grading. */ const GRADEFIRSTATTEMPT = 4; /** Participants cannot review their own attempts. */ const REVIEWNONE = 0; /** Participants can review their own attempts when have one attempt completed. */ const REVIEWCOMPLETION = 1; /** @var stdClass course_module record. */ private $instance; /** @var context_module the current context. */ private $context; /** @var cm_info course_modules record. */ private $coursemodule; /** * Class contructor. * * @param cm_info $coursemodule course module info object * @param stdClass $instance H5Pactivity instance object. */ public function __construct(cm_info $coursemodule, stdClass $instance) { $this->coursemodule = $coursemodule; $this->instance = $instance; $this->context = context_module::instance($coursemodule->id); $this->instance->cmidnumber = $coursemodule->idnumber; } /** * Create a manager instance from an instance record. * * @param stdClass $instance a h5pactivity record * @return manager */ public static function create_from_instance(stdClass $instance): self { $coursemodule = get_coursemodule_from_instance('h5pactivity', $instance->id); // Ensure that $this->coursemodule is a cm_info object. $coursemodule = cm_info::create($coursemodule); return new self($coursemodule, $instance); } /** * Create a manager instance from an course_modules record. * * @param stdClass|cm_info $coursemodule a h5pactivity record * @return manager */ public static function create_from_coursemodule($coursemodule): self { global $DB; // Ensure that $this->coursemodule is a cm_info object. $coursemodule = cm_info::create($coursemodule); $instance = $DB->get_record('h5pactivity', ['id' => $coursemodule->instance], '*', MUST_EXIST); return new self($coursemodule, $instance); } /** * Return the available grading methods. * @return string[] an array "option value" => "option description" */ public static function get_grading_methods(): array { return [ self::GRADEHIGHESTATTEMPT => get_string('grade_highest_attempt', 'mod_h5pactivity'), self::GRADEAVERAGEATTEMPT => get_string('grade_average_attempt', 'mod_h5pactivity'), self::GRADELASTATTEMPT => get_string('grade_last_attempt', 'mod_h5pactivity'), self::GRADEFIRSTATTEMPT => get_string('grade_first_attempt', 'mod_h5pactivity'), self::GRADEMANUAL => get_string('grade_manual', 'mod_h5pactivity'), ]; } /** * Return the selected attempt criteria. * @return string[] an array "grademethod value", "attempt description" */ public function get_selected_attempt(): array { $types = [ self::GRADEHIGHESTATTEMPT => get_string('attempt_highest', 'mod_h5pactivity'), self::GRADEAVERAGEATTEMPT => get_string('attempt_average', 'mod_h5pactivity'), self::GRADELASTATTEMPT => get_string('attempt_last', 'mod_h5pactivity'), self::GRADEFIRSTATTEMPT => get_string('attempt_first', 'mod_h5pactivity'), self::GRADEMANUAL => get_string('attempt_none', 'mod_h5pactivity'), ]; if ($this->instance->enabletracking) { $key = $this->instance->grademethod; } else { $key = self::GRADEMANUAL; } return [$key, $types[$key]]; } /** * Return the available review modes. * * @return string[] an array "option value" => "option description" */ public static function get_review_modes(): array { return [ self::REVIEWCOMPLETION => get_string('review_on_completion', 'mod_h5pactivity'), self::REVIEWNONE => get_string('review_none', 'mod_h5pactivity'), ]; } /** * Check if tracking is enabled in a particular h5pactivity for a specific user. * * @return bool if tracking is enabled in this activity */ public function is_tracking_enabled(): bool { return $this->instance->enabletracking; } /** * Check if the user has permission to submit a particular h5pactivity for a specific user. * * @param stdClass|null $user user record (default $USER) * @return bool if the user has permission to submit in this activity */ public function can_submit(?stdClass $user = null): bool { global $USER; if (empty($user)) { $user = $USER; } return has_capability('mod/h5pactivity:submit', $this->context, $user, false); } /** * Check if a user can see the activity attempts list. * * @param stdClass|null $user user record (default $USER) * @return bool if the user can see the attempts link */ public function can_view_all_attempts(?stdClass $user = null): bool { global $USER; if (!$this->instance->enabletracking) { return false; } if (empty($user)) { $user = $USER; } return has_capability('mod/h5pactivity:reviewattempts', $this->context, $user); } /** * Check if a user can see own attempts. * * @param stdClass|null $user user record (default $USER) * @return bool if the user can see the own attempts link */ public function can_view_own_attempts(?stdClass $user = null): bool { global $USER; if (!$this->instance->enabletracking) { return false; } if (empty($user)) { $user = $USER; } if (has_capability('mod/h5pactivity:reviewattempts', $this->context, $user, false)) { return true; } if ($this->instance->reviewmode == self::REVIEWNONE) { return false; } if ($this->instance->reviewmode == self::REVIEWCOMPLETION) { return true; } return false; } /** * Return a relation of userid and the valid attempt's scaled score. * * The returned elements contain a record * of userid, scaled value, attemptid and timemodified. In case the grading method is "GRADEAVERAGEATTEMPT" * the attemptid will be zero. In case that tracking is disabled or grading method is "GRADEMANUAL" * the method will return null. * * @param int $userid a specific userid or 0 for all user attempts. * @return array|null of userid, scaled value and, if exists, the attempt id */ public function get_users_scaled_score(int $userid = 0): ?array { global $DB; $scaled = []; if (!$this->instance->enabletracking) { return null; } if ($this->instance->grademethod == self::GRADEMANUAL) { return null; } $sql = ''; // General filter. $where = 'a.h5pactivityid = :h5pactivityid'; $params['h5pactivityid'] = $this->instance->id; if ($userid) { $where .= ' AND a.userid = :userid'; $params['userid'] = $userid; } // Average grading needs aggregation query. if ($this->instance->grademethod == self::GRADEAVERAGEATTEMPT) { $sql = "SELECT a.userid, AVG(a.scaled) AS scaled, 0 AS attemptid, MAX(timemodified) AS timemodified FROM {h5pactivity_attempts} a WHERE $where AND a.completion = 1 GROUP BY a.userid"; } if (empty($sql)) { // Decide which attempt is used for the calculation. $condition = [ self::GRADEHIGHESTATTEMPT => "a.scaled < b.scaled", self::GRADELASTATTEMPT => "a.attempt < b.attempt", self::GRADEFIRSTATTEMPT => "a.attempt > b.attempt", ]; $join = $condition[$this->instance->grademethod] ?? $condition[self::GRADEHIGHESTATTEMPT]; $sql = "SELECT a.userid, a.scaled, MAX(a.id) AS attemptid, MAX(a.timemodified) AS timemodified FROM {h5pactivity_attempts} a LEFT JOIN {h5pactivity_attempts} b ON a.h5pactivityid = b.h5pactivityid AND a.userid = b.userid AND b.completion = 1 AND $join WHERE $where AND b.id IS NULL AND a.completion = 1 GROUP BY a.userid, a.scaled"; } return $DB->get_records_sql($sql, $params); } /** * Count the activity completed attempts. * * If no user is provided the method will count all active users attempts. * Check get_active_users_join PHPdoc to a more detailed description of "active users". * * @param int|null $userid optional user id (default null) * @return int the total amount of attempts */ public function count_attempts(?int $userid = null): int { global $DB; // Counting records is enough for one user. if ($userid) { $params['userid'] = $userid; $params = [ 'h5pactivityid' => $this->instance->id, 'userid' => $userid, 'completion' => 1, ]; return $DB->count_records('h5pactivity_attempts', $params); } $usersjoin = $this->get_active_users_join(); // Final SQL. return $DB->count_records_sql( "SELECT COUNT(*) FROM {user} u $usersjoin->joins WHERE $usersjoin->wheres", array_merge($usersjoin->params) ); } /** * Return the join to collect all activity active users. * * The concept of active user is relative to the activity permissions. All users with * "mod/h5pactivity:view" are potential users but those with "mod/h5pactivity:reviewattempts" * are evaluators and they don't count as valid submitters. * * Note that, in general, the active list has the same effect as checking for "mod/h5pactivity:submit" * but submit capability cannot be used because is a write capability and does not apply to frozen contexts. * * @since Moodle 3.11 * @param bool $allpotentialusers if true, the join will return all active users, not only the ones with attempts. * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group * @return sql_join the active users attempts join */ public function get_active_users_join(bool $allpotentialusers = false, $currentgroup = false): sql_join { // Only valid users counts. By default, all users with submit capability are considered potential ones. $context = $this->get_context(); $coursemodule = $this->get_coursemodule(); // Ensure user can view users from all groups. if ($currentgroup === 0 && $coursemodule->effectivegroupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $context)) { return new sql_join('', '1=2', [], true); } // We want to present all potential users. $capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view', $currentgroup); if ($capjoin->cannotmatchanyrows) { return $capjoin; } // But excluding all reviewattempts users converting a capabilities join into left join. $reviewersjoin = get_with_capability_join($context, 'mod/h5pactivity:reviewattempts', 'u.id'); if ($reviewersjoin->cannotmatchanyrows) { return $capjoin; } if (str_contains($reviewersjoin->joins, 'ra')) { $capjoin = new sql_join( $capjoin->joins . "\n LEFT " . str_replace('ra', 'reviewer', $reviewersjoin->joins), $capjoin->wheres . " AND reviewer.userid IS NULL", $capjoin->params ); } if ($allpotentialusers) { return $capjoin; } // Add attempts join. $where = "ha.h5pactivityid = :h5pactivityid AND ha.completion = :completion"; $params = [ 'h5pactivityid' => $this->instance->id, 'completion' => 1, ]; return new sql_join( $capjoin->joins . "\n JOIN {h5pactivity_attempts} ha ON ha.userid = u.id", $capjoin->wheres . " AND $where", array_merge($capjoin->params, $params) ); } /** * Return an array of all users and it's total attempts. * * Note: this funciton only returns the list of users with attempts, * it does not check all participants. * * @return array indexed count userid => total number of attempts */ public function count_users_attempts(): array { global $DB; $params = [ 'h5pactivityid' => $this->instance->id, ]; $sql = "SELECT userid, count(*) FROM {h5pactivity_attempts} WHERE h5pactivityid = :h5pactivityid GROUP BY userid"; return $DB->get_records_sql_menu($sql, $params); } /** * Return the current context. * * @return context_module */ public function get_context(): context_module { return $this->context; } /** * Return the current instance. * * @return stdClass the instance record */ public function get_instance(): stdClass { return $this->instance; } /** * Return the current cm_info. * * @return cm_info the course module */ public function get_coursemodule(): cm_info { return $this->coursemodule; } /** * Return the specific grader object for this activity. * * @return grader */ public function get_grader(): grader { $idnumber = $this->coursemodule->idnumber ?? ''; return new grader($this->instance, $idnumber); } /** * Return the suitable report to show the attempts. * * This method controls the access to the different reports * the activity have. * * @param int $userid an opional userid to show * @param int $attemptid an optional $attemptid to show * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group * @return report|null available report (or null if no report available) */ public function get_report(?int $userid = null, ?int $attemptid = null, $currentgroup = false): ?report { global $USER, $CFG; require_once("{$CFG->dirroot}/user/lib.php"); // If tracking is disabled, no reports are available. if (!$this->instance->enabletracking) { return null; } $attempt = null; if ($attemptid) { $attempt = $this->get_attempt($attemptid); if (!$attempt) { return null; } // If we have and attempt we can ignore the provided $userid. $userid = $attempt->get_userid(); } if ($this->can_view_all_attempts()) { $user = core_user::get_user($userid); // Ensure user can view the attempt of specific userid, respecting access checks. if ($user && $user->id != $USER->id) { $course = get_course($this->coursemodule->course); if (!groups_user_groups_visible($course, $user->id, $this->coursemodule)) { return null; } } } else if ($this->can_view_own_attempts()) { $user = core_user::get_user($USER->id); if ($userid && $user->id != $userid) { return null; } } else { return null; } // Only enrolled users has reports. if ($user && !is_enrolled($this->context, $user, 'mod/h5pactivity:view')) { return null; } // Create the proper report. if ($user && $attempt) { return new results($this, $user, $attempt); } else if ($user) { return new attempts($this, $user); } return new participants($this, $currentgroup); } /** * Return a single attempt. * * @param int $attemptid the attempt id * @return attempt */ public function get_attempt(int $attemptid): ?attempt { global $DB; $record = $DB->get_record('h5pactivity_attempts', [ 'id' => $attemptid, 'h5pactivityid' => $this->instance->id, ]); if (!$record) { return null; } return new attempt($record); } /** * Return an array of all user attempts (including incompleted) * * @param int $userid the user id * @return attempt[] */ public function get_user_attempts(int $userid): array { global $DB; $records = $DB->get_records( 'h5pactivity_attempts', ['userid' => $userid, 'h5pactivityid' => $this->instance->id], 'id ASC' ); if (!$records) { return []; } $result = []; foreach ($records as $record) { $result[] = new attempt($record); } return $result; } /** * Trigger module viewed event and set the module viewed for completion. * * @param stdClass $course course object * @return void */ public function set_module_viewed(stdClass $course): void { global $CFG; require_once($CFG->libdir . '/completionlib.php'); // Trigger module viewed event. $event = course_module_viewed::create([ 'objectid' => $this->instance->id, 'context' => $this->context ]); $event->add_record_snapshot('course', $course); $event->add_record_snapshot('course_modules', $this->coursemodule); $event->add_record_snapshot('h5pactivity', $this->instance); $event->trigger(); // Completion. $completion = new \completion_info($course); $completion->set_module_viewed($this->coursemodule); } } PK1B\*G&&classes/local/report.phpnu[. /** * 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; }PK1B\B { { classes/local/report/results.phpnu[. /** * 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); } /** * Get the export data form this report. * * This method is used to render the report in mobile. */ public function export_data_for_external(): stdClass { global $PAGE; $manager = $this->manager; $attempt = $this->attempt; $cm = $manager->get_coursemodule(); $widget = new reportresults($attempt, $this->user, $cm->course); return $widget->export_for_template($PAGE->get_renderer('core')); } } PK1B\>D!classes/local/report/attempts.phpnu[. /** * 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; } } PK1B\h=F߮%classes/local/report/participants.phpnu[. /** * 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 core\dml\sql_join; use table_sql; use moodle_url; use html_writer; use stdClass; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir.'/tablelib.php'); /** * Class H5P activity participants report. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio */ class participants extends table_sql implements report { /** @var manager the H5P activity manager instance. */ private $manager; /** @var array the users scored attempts. */ private $scores; /** @var array the user attempts count. */ private $count; /** * Create a new participants report. * * @param manager $manager h5pactivitymanager object * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group */ public function __construct(manager $manager, $currentgroup = false) { parent::__construct('mod_h5pactivity-participants'); $this->manager = $manager; $this->scores = $manager->get_users_scaled_score(); $this->count = $manager->count_users_attempts(); // Setup table_sql. $columns = ['fullname', 'timemodified', 'score', 'attempts']; $headers = [ get_string('fullname'), get_string('date'), get_string('score', 'mod_h5pactivity'), get_string('attempts', 'mod_h5pactivity'), ]; $this->define_columns($columns); $this->define_headers($headers); $this->set_attribute('class', 'generaltable generalbox boxaligncenter boxwidthwide'); $this->sortable(true); $this->no_sorting('score'); $this->no_sorting('timemodified'); $this->no_sorting('attempts'); $this->pageable(true); $capjoin = $this->manager->get_active_users_join(true, $currentgroup); // Final SQL. $this->set_sql( 'DISTINCT u.id, u.picture, u.firstname, u.lastname, u.firstnamephonetic, u.lastnamephonetic, u.middlename, u.alternatename, u.imagealt, u.email', "{user} u $capjoin->joins", $capjoin->wheres, $capjoin->params); } /** * Return the report user record. * * Participants report has no specific user. * * @return stdClass|null a user or null */ public function get_user(): ?stdClass { return null; } /** * Return the report attempt object. * * Participants report has no specific attempt. * * @return attempt|null the attempt object or null */ public function get_attempt(): ?attempt { return null; } /** * Print the report. */ public function print(): void { global $PAGE, $OUTPUT; $this->define_baseurl($PAGE->url); $this->out($this->get_page_size(), true); } /** * Warning in case no user has the selected initials letters. * */ public function print_nothing_to_display() { global $OUTPUT; echo $this->render_reset_button(); $this->print_initials_bar(); echo $OUTPUT->notification(get_string('noparticipants', 'mod_h5pactivity'), 'warning'); } /** * Generate the fullname column. * * @param stdClass $user * @return string */ public function col_fullname($user): string { global $OUTPUT; $cm = $this->manager->get_coursemodule(); return $OUTPUT->user_picture($user, ['size' => 35, 'courseid' => $cm->course, 'includefullname' => true]); } /** * Generate score column. * * @param stdClass $user the user record * @return string */ public function col_score(stdClass $user): string { $cm = $this->manager->get_coursemodule(); if (isset($this->scores[$user->id])) { $score = $this->scores[$user->id]; $maxgrade = floatval(100); $scaled = round($maxgrade * $score->scaled).'%'; if (empty($score->attemptid)) { return $scaled; } else { $url = new moodle_url('/mod/h5pactivity/report.php', ['a' => $cm->instance, 'attemptid' => $score->attemptid]); return html_writer::link($url, $scaled); } } return ''; } /** * Generate attempts count column, if any. * * @param stdClass $user the user record * @return string */ public function col_attempts(stdClass $user): string { $cm = $this->manager->get_coursemodule(); if (isset($this->count[$user->id])) { $msg = get_string('review_user_attempts', 'mod_h5pactivity', $this->count[$user->id]); $url = new moodle_url('/mod/h5pactivity/report.php', ['a' => $cm->instance, 'userid' => $user->id]); return html_writer::link($url, $msg); } return ''; } /** * Generate attempt timemodified column, if any. * * @param stdClass $user the user record * @return string */ public function col_timemodified(stdClass $user): string { if (isset($this->scores[$user->id])) { $score = $this->scores[$user->id]; return userdate($score->timemodified); } return ''; } /** * Print headers * * Note: as per MDL-80754, we have to modify the header dynamically to display the total * attempts in the column header. */ public function print_headers(): void { $totalcount = array_sum($this->count); $this->headers[$this->columns['attempts']] = get_string('attempts_report_header_label', 'mod_h5pactivity', $totalcount); parent::print_headers(); } } PK1B\>V77classes/xapi/handler.phpnu[. namespace mod_h5pactivity\xapi; use mod_h5pactivity\local\attempt; use mod_h5pactivity\local\manager; use mod_h5pactivity\event\statement_received; use core_xapi\local\statement; use core_xapi\handler as handler_base; use core\event\base as event_base; use core_xapi\local\state; use moodle_exception; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/mod/h5pactivity/lib.php'); /** * Class xapi_handler for H5P statements and states. * * @package mod_h5pactivity * @since Moodle 3.9 * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class handler extends handler_base { /** * Convert a statement object into a Moodle xAPI Event. * * If a statement is accepted by the xAPI webservice the component must provide * an event to handle that statement, otherwise the statement will be rejected. * * @param statement $statement * @return core\event\base|null a Moodle event to trigger */ public function statement_to_event(statement $statement): ?event_base { // Only process statements with results. $xapiresult = $statement->get_result(); if (empty($xapiresult)) { return null; } // Statements can contain any verb, for security reasons each // plugin needs to filter it's own specific verbs. For now the only verbs the H5P // plugin keeps track on are "answered" and "completed" because they are realted to grading. // In the future this list can be increased to track more user interactions. $validvalues = [ 'http://adlnet.gov/expapi/verbs/answered', 'http://adlnet.gov/expapi/verbs/completed', ]; $xapiverbid = $statement->get_verb_id(); if (!in_array($xapiverbid, $validvalues)) { return null; } // Validate object. $xapiobject = $statement->get_activity_id(); // H5P add some extra params to ID to define subcontents. $parts = explode('?', $xapiobject, 2); $contextid = array_shift($parts); $subcontent = str_replace('subContentId=', '', array_shift($parts) ?? ''); if (empty($contextid) || !is_numeric($contextid)) { return null; } $context = \context::instance_by_id($contextid); if (!$context instanceof \context_module) { return null; } // As the activity does not accept group statement, the code can assume that the // statement user is valid (otherwise the xAPI library will reject the statement). $user = $statement->get_user(); if (!has_capability('mod/h5pactivity:view', $context, $user)) { return null; } $cm = get_coursemodule_from_id('h5pactivity', $context->instanceid, 0, false); if (!$cm) { return null; } $manager = manager::create_from_coursemodule($cm); if (!($manager->is_tracking_enabled() && $manager->can_submit($user))) { return null; } // For now, attempts are only processed on a single batch starting with the final "completed" // and "answered" statements (this could change in the future). This initial statement have no // subcontent defined as they are the main finishing statement. For this reason, this statement // indicates a new attempt creation. This way, simpler H5P activies like multichoice can generate // an attempt each time the user answers while complex like question-set could group all questions // in a single attempt (using subcontents). if (empty($subcontent)) { $attempt = attempt::new_attempt($user, $cm); } else { $attempt = attempt::last_attempt($user, $cm); } if (!$attempt) { return null; } $result = $attempt->save_statement($statement, $subcontent); if (!$result) { return null; } // Update activity if necessary. if ($attempt->get_scoreupdated()) { $grader = $manager->get_grader(); $grader->update_grades($user->id); } // Convert into a Moodle event. $minstatement = $statement->minify(); $params = [ 'other' => $minstatement, 'context' => $context, 'objectid' => $cm->instance, 'userid' => $user->id, ]; return statement_received::create($params); } /** * Validate a xAPI state. * * Check if the state is valid for this handler. * * This method is used also for the state get requests so the validation * cannot rely on having state data. * * @param state $state * @return bool if the state is valid or not */ protected function validate_state(state $state): bool { $xapiobject = $state->get_activity_id(); // H5P add some extra params to ID to define subcontents. $parts = explode('?', $xapiobject, 2); $contextid = array_shift($parts); if (empty($contextid) || !is_numeric($contextid)) { return false; } try { $context = \context::instance_by_id($contextid); if (!$context instanceof \context_module) { return false; } } catch (moodle_exception $exception) { return false; } $cm = get_coursemodule_from_id('h5pactivity', $context->instanceid, 0, false); if (!$cm) { return false; } // If tracking is not enabled or the user can't submit, the state won't be considered valid. $manager = manager::create_from_coursemodule($cm); $user = $state->get_user(); if (!($manager->is_tracking_enabled() && $manager->can_submit($user))) { return false; } return true; } } PK1B\uQ1eeclasses/search/activity.phpnu[. /** * Search area for mod_h5pactivity activities. * * @package mod_h5pactivity * @copyright 2022 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\search; /** * Search area for mod_h5pactivity activities. * * @package mod_h5pactivity * @copyright 2022 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activity extends \core_search\base_activity { /** * Returns true if this area uses file indexing. * * @return bool */ public function uses_file_indexing() { return true; } } PK1B\,$eeclasses/output/reportlink.phpnu[. /** * 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; } } PK1B\:^classes/output/attempt.phpnu[. /** * Contains class mod_h5pactivity\output\reportlink * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\output; defined('MOODLE_INTERNAL') || die(); use mod_h5pactivity\local\attempt as activity_attempt; use renderable; use templatable; use renderer_base; use moodle_url; use user_picture; use stdClass; /** * Class to help display report link in mod_h5pactivity. * * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class attempt implements renderable, templatable { /** @var activity_attempt attempt */ public $attempt; /** @var stdClass user record */ public $user; /** @var int courseid necesary to present user picture */ public $courseid; /** * Constructor. * * @param activity_attempt $attempt the attempt object * @param stdClass $user a user record (default null). * @param int $courseid optional course id (default null). */ public function __construct(activity_attempt $attempt, ?stdClass $user = null, ?int $courseid = null) { $this->attempt = $attempt; $this->user = $user; $this->courseid = $courseid; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { $attempt = $this->attempt; $data = (object)[ 'id' => $attempt->get_id(), 'h5pactivityid' => $attempt->get_h5pactivityid(), 'userid' => $attempt->get_userid(), 'timecreated' => $attempt->get_timecreated(), 'timemodified' => $attempt->get_timemodified(), 'attempt' => $attempt->get_attempt(), 'rawscore' => $attempt->get_rawscore(), 'maxscore' => $attempt->get_maxscore(), 'duration' => '-', 'durationcompact' => '-', 'completion' => $attempt->get_completion(), 'completionicon' => $this->completion_icon($output, $attempt->get_completion()), 'completiontext' => $this->completion_icon($output, $attempt->get_completion(), true), 'success' => $attempt->get_success(), 'successicon' => $this->success_icon($output, $attempt->get_success()), 'successtext' => $this->success_icon($output, $attempt->get_success(), true), 'scaled' => $attempt->get_scaled(), 'reporturl' => new moodle_url('/mod/h5pactivity/report.php', [ 'a' => $attempt->get_h5pactivityid(), 'attemptid' => $attempt->get_id() ]), ]; if ($attempt->get_duration() !== null) { $data->durationvalue = $attempt->get_duration(); $duration = $this->extract_duration($data->durationvalue); $data->duration = $this->format_duration($duration); $data->durationcompact = $this->format_duration_short($duration); } if (!empty($data->maxscore)) { $data->score = get_string('score_out_of', 'mod_h5pactivity', $data); } if ($this->user) { $data->user = $this->user; $userpicture = new user_picture($this->user); $userpicture->courseid = $this->courseid; $data->user->picture = $output->render($userpicture); $data->user->fullname = fullname($this->user); } return $data; } /** * Return a completion icon HTML. * * @param renderer_base $output the renderer base object * @param int|null $completion the current completion value * @param bool $showtext if the icon must have a text or only icon * @return string icon HTML */ private function completion_icon(renderer_base $output, ?int $completion = null, bool $showtext = false): string { if ($completion === null) { return ''; } if ($completion) { $alt = get_string('attempt_completion_yes', 'mod_h5pactivity'); $icon = 'i/completion-auto-y'; } else { $alt = get_string('attempt_completion_no', 'mod_h5pactivity'); $icon = 'i/completion-auto-n'; } $text = ''; if ($showtext) { $text = $alt; $alt = ''; } return $output->pix_icon($icon, $alt).$text; } /** * Return a success icon * @param renderer_base $output the renderer base object * @param int|null $success the current success value * @param bool $showtext if the icon must have a text or only icon * @return string icon HTML */ private function success_icon(renderer_base $output, ?int $success = null, bool $showtext = false): string { if ($success === null) { $alt = get_string('attempt_success_unknown', 'mod_h5pactivity'); if ($showtext) { return $alt; } $icon = 'i/empty'; } else if ($success) { $alt = get_string('attempt_success_pass', 'mod_h5pactivity'); $icon = 'i/checkedcircle'; } else { $alt = get_string('attempt_success_fail', 'mod_h5pactivity'); $icon = 'i/uncheckedcircle'; } $text = ''; if ($showtext) { $text = $alt; $alt = ''; } return $output->pix_icon($icon, $alt).$text; } /** * Return the duration in long format (localized) * * @param stdClass $duration object with (h)hours, (m)minutes and (s)seconds * @return string the long format duration */ private function format_duration(stdClass $duration): string { $result = []; if ($duration->h) { $result[] = get_string('numhours', 'moodle', $duration->h); } if ($duration->m) { $result[] = get_string('numminutes', 'moodle', $duration->m); } if ($duration->s) { $result[] = get_string('numseconds', 'moodle', $duration->s); } return implode(' ', $result); } /** * Return the duration en short format (for example: 145' 43'') * * Note: this method is used to make duration responsive. * * @param stdClass $duration object with (h)hours, (m)minutes and (s)seconds * @return string the short format duration */ private function format_duration_short(stdClass $duration): string { $result = []; if ($duration->h || $duration->m) { $result[] = ($duration->h * 60 + $duration->m)."'"; } if ($duration->s) { $result[] = $duration->s."''"; } return implode(' ', $result); } /** * Extract hours and minutes from second duration. * * Note: this function is used to generate the param for format_duration * and format_duration_short * * @param int $seconds number of second * @return stdClass with (h)hours, (m)minutes and (s)seconds */ private function extract_duration(int $seconds): stdClass { $h = floor($seconds / 3600); $m = floor(($seconds - $h * 3600) / 60); $s = $seconds - ($h * 3600 + $m * 60); return (object)['h' => $h, 'm' => $m, 's' => $s]; } } PK1B\2Ra]]!classes/output/reportattempts.phpnu[. /** * 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; } } PK1B\f- classes/output/reportresults.phpnu[. /** * 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; } } PK1B\Iu  #classes/output/result/truefalse.phpnu[. /** * Contains class mod_h5pactivity\output\result\truefalse * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\output\result; defined('MOODLE_INTERNAL') || die(); use mod_h5pactivity\output\result; use renderer_base; /** * Class to display H5P choice result. * * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class truefalse extends result { /** * Return the options data structure. * * @return array of options */ protected function export_options(): ?array { // This interaction type have only one entry which is the correct option. $correctpattern = reset($this->correctpattern); $correctpattern = filter_var(reset($correctpattern), FILTER_VALIDATE_BOOLEAN); $correctpattern = $correctpattern ? 'true' : 'false'; $response = filter_var(reset($this->response), FILTER_VALIDATE_BOOLEAN); $response = $response ? 'true' : 'false'; $options = [ (object)[ 'id' => 'true', 'description' => get_string('true', 'mod_h5pactivity'), ], (object)[ 'id' => 'false', 'description' => get_string('false', 'mod_h5pactivity'), ], ]; foreach ($options as $value) { $correctstate = ($value->id == $correctpattern) ? parent::CHECKED : parent::UNCHECKED; if ($value->id == $response) { $answerstate = ($correctstate == parent::CHECKED) ? parent::PASS : parent::FAIL; $value->useranswer = $this->get_answer($answerstate); } $value->correctanswer = $this->get_answer($correctstate); } return $options; } } PK1B\|}"classes/output/result/matching.phpnu[. /** * Contains class mod_h5pactivity\output\result\matching * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\output\result; defined('MOODLE_INTERNAL') || die(); use mod_h5pactivity\output\result; /** * Class to display H5P matching result. * * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class matching extends result { /** * Return the options data structure. * * @return array|null of options */ protected function export_options(): ?array { // Suppose H5P choices have only list of valid answers. $correctpattern = reset($this->correctpattern); $additionals = $this->additionals; // Get sources (options). if (isset($additionals->source)) { $sources = $this->get_descriptions($additionals->source); } else { $sources = []; } // Get targets. if (isset($additionals->target)) { $targets = $this->get_descriptions($additionals->target); } else { $targets = []; } // Create original options array. $options = array_map(function ($source) { $cloneddraggable = clone $source; $cloneddraggable->correctanswers = []; return $cloneddraggable; }, $sources); // Fill options with correct answers flags if they exist. foreach ($correctpattern as $pattern) { if (!is_array($pattern) || count($pattern) != 2) { continue; } // We assume here that the activity is following the convention sets in: // https://github.com/h5p/h5p-php-report/blob/master/type-processors/matching-processor.class.php // i.e. source is index 1 and dropzone is index 0. if (isset($sources[$pattern[1]]) && isset($targets[$pattern[0]])) { $target = $targets[$pattern[0]]; $source = $sources[$pattern[1]]; $currentoption = $options[$source->id]; $currentoption->correctanswers[$target->id] = $target->description; } } // Fill in user responses. foreach ($this->response as $response) { if (!is_array($response) || count($response) != 2) { continue; } if (isset($sources[$response[1]]) && isset($targets[$response[0]])) { $source = $sources[$response[1]]; $target = $targets[$response[0]]; $answer = $response[0]; $option = $options[$source->id] ?? null; if ($option) { if (isset($option->correctanswers[$answer])) { $state = parent::CORRECT; } else { $state = parent::INCORRECT; } $option->useranswer = $this->get_answer($state, $target->description); } } } // Fill in unchecked options. foreach ($options as $option) { if (!isset($option->useranswer)) { if (!empty($option->correctanswers)) { $option->useranswer = $this->get_answer(parent::INCORRECT, get_string('answer_noanswer', 'mod_h5pactivity')); } else { $option->useranswer = $this->get_answer(parent::CORRECT, get_string('answer_noanswer', 'mod_h5pactivity')); } } } // Now flattern correct answers. foreach ($options as $option) { $option->correctanswer = $this->get_answer( parent::TEXT, join(', ', $option->correctanswers)); unset($option->correctanswers); } return array_values($options); } /** * Return a label for result user options/choices * * Specific result types can override this method to customize * the result options table header. * * @return string to use in options table */ protected function get_optionslabel(): string { return get_string('result_matching', 'mod_h5pactivity'); } } PK1B\bvv classes/output/result/fillin.phpnu[. /** * Contains class mod_h5pactivity\output\result\fillin * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\output\result; defined('MOODLE_INTERNAL') || die(); use mod_h5pactivity\output\result; use renderer_base; use stdClass; /** * Class to display H5P fill-in result. * * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class fillin extends result { /** * Return the options data structure. * * @return array of options */ protected function export_options(): ?array { $correctpatterns = $this->correctpattern; $additionals = $this->additionals; $extensions = (array) $additionals->extensions ?? []; // There are two way in which H5P could force case sensitivity, with extensions // or using options in the correctpatterns. By default it is case sensible. $casesensitive = $extensions['https://h5p.org/x-api/case-sensitivity'] ?? true; if ((!empty($this->result->correctpattern) && strpos($this->result->correctpattern, '{case_matters=false}') !== false)) { $casesensitive = false; } $values = []; // Add all possibilities from $additionals. if (isset($extensions['https://h5p.org/x-api/alternatives'])) { foreach ($extensions['https://h5p.org/x-api/alternatives'] as $key => $value) { if (!is_array($value)) { $value = [$value]; } $values[$key] = ($casesensitive) ? $value : array_change_key_case($value); } } // Add possibilities from correctpattern. foreach ($correctpatterns as $correctpattern) { foreach ($correctpattern as $key => $pattern) { // The xAPI admits more params a part form values. // For now this extra information is not used in reporting // but it is posible future H5P types need them. $value = preg_replace('/\{.+=.*\}/', '', $pattern); $value = ($casesensitive) ? $value : strtolower($value); if (!isset($values[$key])) { $values[$key] = []; } if (!in_array($value, $values[$key])) { array_unshift($values[$key], $value); } } } // Generate options. $options = []; $num = 1; foreach ($values as $key => $value) { $option = (object)[ 'id' => $key, 'description' => get_string('result_fill-in_gap', 'mod_h5pactivity', $num), ]; $gapresponse = $this->response[$key] ?? null; $gapresponse = ($casesensitive) ? $gapresponse : strtolower($gapresponse); if ($gapresponse !== null && in_array($gapresponse, $value)) { $state = parent::CORRECT; } else { $state = parent::INCORRECT; } $option->useranswer = $this->get_answer($state, $this->response[$key]); $option->correctanswer = $this->get_answer(parent::TEXT, implode(' / ', $value)); $options[] = $option; $num++; } return $options; } /** * Return a label for result user options/choices * * Specific result types can override this method to customize * the result options table header. * * @return string to use in options table */ protected function get_optionslabel(): string { return get_string('result_matching', 'mod_h5pactivity'); } } PK1B\LV classes/output/result/choice.phpnu[. /** * 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; } } PK1B\(u$classes/output/result/longfillin.phpnu[. /** * Contains class mod_h5pactivity\output\result\longfillin * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\output\result; defined('MOODLE_INTERNAL') || die(); use mod_h5pactivity\output\result; use renderer_base; use stdClass; /** * Class to display H5P long fill in result. * * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class longfillin extends result { /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { $data = parent::export_for_template($output); $userresponse = reset($this->response); $data->content = format_text($userresponse, FORMAT_PLAIN); $data->track = true; // Long fill-in is used for Essay type exercices. H5P adds // extra characters to the description in all fill-in interactions // but in the essay questions is unnecesary. $data->description = preg_replace('/__________$/', '', $data->description); return $data; } } PK1B\^ ^ $classes/output/result/sequencing.phpnu[. /** * Contains class mod_h5pactivity\output\result\sequencing * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\output\result; defined('MOODLE_INTERNAL') || die(); use mod_h5pactivity\output\result; use renderer_base; /** * Class to display H5P sequencing result. * * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class sequencing extends result { /** * Return the options data structure. * * @return array of options */ protected function export_options(): ?array { $correctpattern = reset($this->correctpattern); $additionals = $this->additionals; $response = $this->response; if (isset($additionals->choices)) { $choices = $this->get_descriptions($additionals->choices); } else { $choices = []; } $options = []; $num = 1; foreach ($correctpattern as $key => $pattern) { if (!isset($choices[$pattern])) { continue; } $option = (object)[ 'id' => 'true', 'description' => get_string('result_sequencing_position', 'mod_h5pactivity', $num), 'correctanswer' => $this->get_answer(parent::TEXT, $choices[$pattern]->description), 'correctanswerid' => $key, ]; if (isset($response[$key])) { $responseid = str_replace('item_', '', $response[$key]); $answerstate = ($responseid == $option->correctanswerid) ? parent::PASS : parent::FAIL; } else { $answerstate = parent::FAIL; } $option->useranswer = $this->get_answer($answerstate); $options[$key] = $option; $num ++; } return $options; } /** * Return a label for result user options/choices. * * @return string to use in options table */ protected function get_optionslabel(): string { return get_string('result_sequencing_choice', 'mod_h5pactivity'); } /** * Return a label for result user correct answer. * * @return string to use in options table */ protected function get_correctlabel(): string { return get_string('result_sequencing_answer', 'mod_h5pactivity'); } } PK1B\ԋclasses/output/result/other.phpnu[. /** * 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; } } PK1B\ QF$$classes/output/result.phpnu[. /** * Contains class mod_h5pactivity\output\result * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\output; defined('MOODLE_INTERNAL') || die(); use renderable; use templatable; use renderer_base; use stdClass; /** * Class to display an attempt tesult in mod_h5pactivity. * * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class result implements renderable, templatable { /** Correct answer state. */ const CORRECT = 1; /** Incorrect answer state. */ const INCORRECT = 2; /** Checked answer state. */ const CHECKED = 3; /** Unchecked answer state. */ const UNCHECKED = 4; /** Pass answer state. */ const PASS = 5; /** Pass answer state. */ const FAIL = 6; /** Unknown answer state. */ const UNKNOWN = 7; /** Text answer state. */ const TEXT = 8; /** @var stdClass result record */ protected $result; /** @var mixed additional decoded data */ protected $additionals; /** @var mixed response decoded data */ protected $response; /** @var mixed correctpattern decoded data */ protected $correctpattern = []; /** * Constructor. * * @param stdClass $result a h5pactivity_attempts_results record */ protected function __construct(stdClass $result) { $this->result = $result; if (empty($result->additionals)) { $this->additionals = new stdClass(); } else { $this->additionals = json_decode($result->additionals); } $this->response = $this->decode_response($result->response); if (!empty($result->correctpattern)) { $correctpattern = json_decode($result->correctpattern); foreach ($correctpattern as $pattern) { $this->correctpattern[] = $this->decode_response($pattern); } } } /** * return the correct result output depending on the interactiontype * * @param stdClass $result h5pactivity_attempts_results record * @return result|null the result output class if any */ public static function create_from_record(stdClass $result): ?self { // Compound result track is omitted from the report. if ($result->interactiontype == 'compound') { return null; } $classname = "mod_h5pactivity\\output\\result\\{$result->interactiontype}"; $classname = str_replace('-', '', $classname); if (class_exists($classname)) { return new $classname($result); } return new self($result); } /** * Return a decoded response structure. * * @param string $value the current response structure * @return array an array of reponses */ private function decode_response(string $value): array { // If [,] means a list of elements. $list = explode('[,]', $value); // Inside a list element [.] means sublist (pair) and [:] a range. foreach ($list as $key => $item) { if (strpos($item, '[.]') !== false) { $list[$key] = explode('[.]', $item); } else if (strpos($item, '[:]') !== false) { $list[$key] = explode('[:]', $item); } } return $list; } /** * Export this data so it can be used as the context for a mustache template. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output): stdClass { $result = $this->result; $data = (object)[ 'id' => $result->id, 'attemptid' => $result->attemptid, 'subcontent' => $result->subcontent, 'timecreated' => $result->timecreated, 'interactiontype' => $result->interactiontype, 'description' => strip_tags($result->description), 'rawscore' => $result->rawscore, 'maxscore' => $result->maxscore, 'duration' => $result->duration, 'completion' => $result->completion, 'success' => $result->success, ]; $result; $options = $this->export_options(); if (!empty($options)) { $data->hasoptions = true; $data->optionslabel = $this->get_optionslabel(); $data->correctlabel = $this->get_correctlabel(); $data->answerlabel = $this->get_answerlabel(); $data->options = array_values($options); $data->track = true; } if (!empty($result->maxscore)) { $data->score = get_string('score_out_of', 'mod_h5pactivity', $result); } return $data; } /** * Return the options data structure. * * Result types have to override this method generate a specific options report. * * An option is an object with: * - id: the option ID * - description: option description text * - useranswer (optional): what the user answer (see get_answer method) * - correctanswer (optional): the correct answer (see get_answer method) * * @return array of options */ protected function export_options(): ?array { return []; } /** * Return a label for result user options/choices. * * Specific result types can override this method to customize * the result options table header. * * @return string to use in options table */ protected function get_optionslabel(): string { return get_string('choice', 'mod_h5pactivity'); } /** * Return a label for result user correct answer. * * Specific result types can override this method to customize * the result options table header. * * @return string to use in options table */ protected function get_correctlabel(): string { return get_string('correct_answer', 'mod_h5pactivity'); } /** * Return a label for result user attempt answer. * * Specific result types can override this method to customize * the result options table header. * * @return string to use in options table */ protected function get_answerlabel(): string { return get_string('attempt_answer', 'mod_h5pactivity'); } /** * Extract descriptions from array. * * @param array $data additional attribute to parse * @return string[] the resulting strings */ protected function get_descriptions(array $data): array { $result = []; foreach ($data as $key => $value) { $description = $this->get_description($value); $index = $value->id ?? $key; $index = trim($index); if (is_numeric($index)) { $index = intval($index); } $result[$index] = (object)['description' => $description, 'id' => $index]; } ksort($result); return $result; } /** * Extract description from data element. * * @param stdClass $data additional attribute to parse * @return string the resulting string */ protected function get_description(stdClass $data): string { if (!isset($data->description)) { return ''; } $translations = (array) $data->description; if (empty($translations)) { return ''; } // By default, H5P packages only send "en-US" descriptions. $result = $translations['en-US'] ?? array_shift($translations); return trim($result); } /** * Return an answer data to show results. * * @param int $state the answer state * @param string $answer the extra text to display (default null) * @return stdClass with "answer" text and the state attribute to be displayed */ protected function get_answer(int $state, ?string $answer = null): stdClass { $states = [ self::CORRECT => 'correct', self::INCORRECT => 'incorrect', self::CHECKED => 'checked', self::UNCHECKED => 'unchecked', self::PASS => 'pass', self::FAIL => 'fail', self::UNKNOWN => 'unknown', self::TEXT => 'text', ]; $state = $states[$state] ?? self::UNKNOWN; if ($answer === null) { $answer = get_string('answer_'.$state, 'mod_h5pactivity'); } $result = (object)[ 'answer' => $answer, $state => true, ]; return $result; } } PK1B\;d  &classes/event/course_module_viewed.phpnu[. /** * H5P activity 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 course_module_viewed event class. * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_viewed extends \core\event\course_module_viewed { /** * Init method. * * @return void */ protected function init(): void { $this->data['objecttable'] = 'h5pactivity'; $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * This is used when restoring course logs where it is required that we * map the objectid to it's new value in the new course. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'h5pactivity', 'restore' => 'h5pactivity']; } } PK1B\r\classes/event/report_viewed.phpnu[. /** * 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; } } PK1B\_\s* * $classes/event/statement_received.phpnu[. /** * H5P activity send an xAPI tracking statement. * * @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 statement_received event class. * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class statement_received extends \core\event\base { /** * Init method. * * @return void */ protected function init(): void { $this->data['objecttable'] = 'h5pactivity'; $this->data['crud'] = 'u'; $this->data['edulevel'] = self::LEVEL_PARTICIPATING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('statement_received', 'mod_h5pactivity'); } /** * Returns non-localised description of what happened. * * @return string */ public function get_description() { return "The user with the id '$this->userid' send a tracking statement " . "for a H5P activity with the course module id '$this->contextinstanceid'."; } /** * Get URL related to the action * * @return \moodle_url */ public function get_url() { return new \moodle_url('/mod/h5pactivity/grade.php', ['id' => $this->contextinstanceid, 'user' => $this->userid]); } /** * This is used when restoring course logs where it is required that we * map the objectid to it's new value in the new course. * * @return array */ public static function get_objectid_mapping() { return ['db' => 'h5pactivity', 'restore' => 'h5pactivity']; } } PK1B\[;  4classes/event/course_module_instance_list_viewed.phpnu[. /** * H5P Activity list viewed event. * * @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 course_module_instance_list_viewed event class. * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class course_module_instance_list_viewed extends \core\event\course_module_instance_list_viewed { } PK1B\:f templates/reportresults.mustachenu[{{! 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}} PK1B\q]< templates/attempt.mustachenu[{{! 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}}
PK1B\ ??'templates/local/result/options.mustachenu[{{! 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/local/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/local/result/answer}}{{/correctanswer}} {{#useranswer}}{{>mod_h5pactivity/local/result/answer}}{{/useranswer}}
{{#str}}score, mod_h5pactivity{{/str}}: {{score}} {{#str}}score, mod_h5pactivity{{/str}}: {{score}}
PK1B\"qww&templates/local/result/header.mustachenu[{{! 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/local/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}}}

PK1B\@@&templates/local/result/answer.mustachenu[{{! 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/local/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}} PK1B\z<<templates/attempts.mustachenu[{{! 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}}
PK1B\{aatemplates/reportlink.mustachenu[{{! 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)" } }} PK1B\HP!templates/reviewattempts.mustachenu[{{! 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/reviewattempts This template will render the H5P activity inside a recent activity report. Variables requirer for this template: * userpicture - User fullname and picture * grade - User grade * userdate - Submission date Example context (json): { "userpicture": " ", "grade": "30.77 / 100.00", "userdate": "Wednesday, 27 January 2021, 9:54 AM", "userurl": "http://localhost/m/stable_master/user/view.php?id=3&course=6", "fullname": "Lukáš Svoboda" } }}
{{{userpicture}}}
{{#modinfo}} {{modicon}}{{modname}} {{/modinfo}}
{{#grade}}
{{grade}}
{{/grade}}
{{fullname}} - {{#userdate}} {{submissiontimestamp}}, {{#str}} strftimedatetime {{/str}} {{/userdate}}
PK1B\%aa!templates/reportattempts.mustachenu[{{! 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}} PK1B\W7/tttemplates/result.mustachenu[{{! 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/local/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/local/result/header}} {{{content}}} {{#hasoptions}} {{>mod_h5pactivity/local/result/options}} {{/hasoptions}} {{^track}} {{/track}}
PK1B\E [[ upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This files describes API changes in /mod/h5pactivity/*, information provided here is intended especially for developers. === 4.2 === * The WebService mod_h5pactivity_get_h5pactivities_by_courses now returns a `h5pglobalsettings` property that includes the global settings for H5P activities. === 4.1 === * The following methods now accept an optional `$currentgroup` parameter in order to better support groups: - `\mod_h5pactivity\local\manager::get_active_users_join` - `\mod_h5pactivity\local\manager::get_report` - `\mod_h5pactivity\local\report\participants` constructor === 3.11 === * Added mod_h5pactivity\local\manager::get_active_users_join method to query all active users from a specific activity, even in a freeze context. PK1B\'$ report.phpnu[. /** * 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); $currentgroup = groups_get_activity_group($cm, true); $manager = manager::create_from_coursemodule($cm); $report = $manager->get_report($userid, $attemptid, $currentgroup); if (!$report) { throw new \moodle_exception('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)); $PAGE->activityheader->disable(); $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(); groups_print_activity_menu($cm, $PAGE->url); echo html_writer::start_div('mt-4'); echo $report->print(); echo html_writer::end_div(); echo $OUTPUT->footer(); PK1B\ eh h index.phpnu[. /** * Display information about all the mod_h5pactivity modules in the requested course. * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../config.php'); require_once(__DIR__.'/lib.php'); $id = required_param('id', PARAM_INT); $course = $DB->get_record('course', ['id' => $id], '*', MUST_EXIST); require_course_login($course); $coursecontext = context_course::instance($course->id); $event = \mod_h5pactivity\event\course_module_instance_list_viewed::create(['context' => $coursecontext]); $event->add_record_snapshot('course', $course); $event->trigger(); $PAGE->set_url('/mod/h5pactivity/index.php', ['id' => $id]); $PAGE->set_title(format_string($course->fullname)); $PAGE->set_heading(format_string($course->fullname)); $PAGE->set_context($coursecontext); echo $OUTPUT->header(); $modulenameplural = get_string('modulenameplural', 'mod_h5pactivity'); echo $OUTPUT->heading($modulenameplural); $h5pactivities = get_all_instances_in_course('h5pactivity', $course); if (empty($h5pactivities)) { notice(get_string('thereareno', 'moodle'), new moodle_url('/course/view.php', ['id' => $course->id])); exit; } $table = new html_table(); $table->attributes['class'] = 'generaltable mod_index table table-hover'; $align = ['center', 'left']; if ($course->format == 'weeks' || $course->format == 'topics') { $table->head = [get_string('section'), get_string('name')]; $table->align = ['center', 'left']; } else { $table->head = [get_string('name')]; $table->align = ['left']; } foreach ($h5pactivities as $h5pactivity) { $attributes = []; if (!$h5pactivity->visible) { $attributes['class'] = 'dimmed'; } $link = html_writer::link( new moodle_url('/mod/h5pactivity/view.php', ['id' => $h5pactivity->coursemodule]), format_string($h5pactivity->name, true), $attributes); if ($course->format == 'weeks' or $course->format == 'topics') { $table->data[] = [$h5pactivity->section, $link]; } else { $table->data[] = [$link]; } } echo html_writer::table($table); echo $OUTPUT->footer(); PK1B\ UPGRADING.mdnu[# mod_h5pactivity Upgrade notes ## 5.0 ### Changed - The external function get_user_attempts now returns the total number of attempts. For more information see [MDL-82775](https://tracker.moodle.org/browse/MDL-82775) PK1B\­)zXzXtests/generator/lib.phpnu[. /** * Data generator. * * @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; defined('MOODLE_INTERNAL') || die(); /** * h5pactivity module data generator class. * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_h5pactivity_generator extends testing_module_generator { /** * Creates new h5pactivity module instance. By default it contains a short * text file. * * @param array|stdClass $record data for module being generated. Requires 'course' key * (an id or the full object). Also can have any fields from add module form. * @param null|array $options general options for course module. Since 2.6 it is * possible to omit this argument by merging options into $record * @return stdClass record from module-defined table with additional field * cmid (corresponding id in course_modules table) */ public function create_instance($record = null, ?array $options = null): stdClass { global $CFG, $USER; // Ensure the record can be modified without affecting calling code. $record = (object)(array)$record; // Fill in optional values if not specified. if (!isset($record->packagefilepath)) { $record->packagefilepath = $CFG->dirroot.'/h5p/tests/fixtures/ipsums.h5p'; } else if (strpos($record->packagefilepath, $CFG->dirroot) !== 0) { $record->packagefilepath = "{$CFG->dirroot}/{$record->packagefilepath}"; } if (!isset($record->grade)) { $record->grade = 100; } if (!isset($record->displayoptions)) { $factory = new \core_h5p\factory(); $core = $factory->get_core(); $config = \core_h5p\helper::decode_display_options($core); $record->displayoptions = \core_h5p\helper::get_display_options($core, $config); } if (!isset($record->enabletracking)) { $record->enabletracking = 1; } if (!isset($record->grademethod)) { $record->grademethod = manager::GRADEHIGHESTATTEMPT; } if (!isset($record->reviewmode)) { $record->reviewmode = manager::REVIEWCOMPLETION; } $globaluser = $USER; if (!empty($record->username)) { $user = core_user::get_user_by_username($record->username); $this->set_user($user); } // The 'packagefile' value corresponds to the draft file area ID. If not specified, create from packagefilepath. if (empty($record->packagefile)) { if (!isloggedin() || isguestuser()) { throw new coding_exception('H5P activity generator requires a current user'); } if (!file_exists($record->packagefilepath)) { throw new coding_exception("File {$record->packagefilepath} does not exist"); } $usercontext = context_user::instance($USER->id); // Pick a random context id for specified user. $record->packagefile = file_get_unused_draft_itemid(); // Add actual file there. $filerecord = [ 'component' => 'user', 'filearea' => 'draft', 'contextid' => $usercontext->id, 'itemid' => $record->packagefile, 'filename' => basename($record->packagefilepath), 'filepath' => '/', 'userid' => $USER->id, ]; $fs = get_file_storage(); $fs->create_file_from_pathname($filerecord, $record->packagefilepath); } $instance = parent::create_instance($record, (array)$options); $this->set_user($globaluser); return $instance; } /** * Creata a fake attempt * @param stdClass $instance object returned from create_instance() call * @param stdClass|array $record * @return stdClass generated object * @throws coding_exception if function is not implemented by module */ public function create_content($instance, $record = []) { global $DB, $USER; $currenttime = time(); $cmid = $record['cmid']; $userid = $record['userid'] ?? $USER->id; $conditions = ['h5pactivityid' => $instance->id, 'userid' => $userid]; $attemptnum = $DB->count_records('h5pactivity_attempts', $conditions) + 1; $attempt = (object)[ 'h5pactivityid' => $instance->id, 'userid' => $userid, 'timecreated' => $currenttime, 'timemodified' => $currenttime, 'attempt' => $attemptnum, 'rawscore' => 3, 'maxscore' => 5, 'completion' => 1, 'success' => 1, 'scaled' => 0.6, ]; $attempt->id = $DB->insert_record('h5pactivity_attempts', $attempt); // Create 3 diferent tracking results. $result = (object)[ 'attemptid' => $attempt->id, 'subcontent' => '', 'timecreated' => $currenttime, 'interactiontype' => 'compound', 'description' => 'description for '.$userid, 'correctpattern' => '', 'response' => '', 'additionals' => '{"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":'. $cmid.'},"contextExtensions":{}}', 'rawscore' => 3, 'maxscore' => 5, 'completion' => 1, 'success' => 1, 'scaled' => 0.6, ]; $DB->insert_record('h5pactivity_attempts_results', $result); $result->subcontent = 'bd03477a-90a1-486d-890b-0657d6e80ffd'; $result->interactiontype = 'compound'; $result->response = '0[,]5[,]2[,]3'; $result->additionals = '{"choices":[{"id":"0","description":{"en-US":"Blueberry\n"}},'. '{"id":"1","description":{"en-US":"Raspberry\n"}},{"id":"5","description":'. '{"en-US":"Strawberry\n"}},{"id":"2","description":{"en-US":"Cloudberry\n"}},'. '{"id":"3","description":{"en-US":"Halle Berry\n"}},'. '{"id":"4","description":{"en-US":"Cocktail cherry\n"}}],'. '"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":'.$cmid. ',"http:\/\/h5p.org\/x-api\/h5p-subContentId":"'.$result->interactiontype. '"},"contextExtensions":{}}'; $result->rawscore = 1; $result->scaled = 0.2; $DB->insert_record('h5pactivity_attempts_results', $result); $result->subcontent = '14fcc986-728b-47f3-915b-'.$userid; $result->interactiontype = 'matching'; $result->correctpattern = '["0[.]1[,]1[.]0[,]2[.]2"]'; $result->response = '1[.]0[,]0[.]1[,]2[.]2'; $result->additionals = '{"source":[{"id":"0","description":{"en-US":"A berry"}}'. ',{"id":"1","description":{"en-US":"An orange berry"}},'. '{"id":"2","description":{"en-US":"A red berry"}}],'. '"target":[{"id":"0","description":{"en-US":"Cloudberry"}},'. '{"id":"1","description":{"en-US":"Blueberry"}},'. '{"id":"2","description":{"en-US":"Redcurrant\n"}}],'. '"contextExtensions":{}}'; $result->rawscore = 2; $result->scaled = 0.4; $DB->insert_record('h5pactivity_attempts_results', $result); return $attempt; } /** * Create a H5P attempt. * * This method is user by behat generator. * * @param array $data the attempts data array */ public function create_attempt(array $data): void { global $DB; if (!isset($data['h5pactivityid'])) { throw new coding_exception('Must specify h5pactivityid when creating a H5P attempt.'); } if (!isset($data['userid'])) { throw new coding_exception('Must specify userid when creating a H5P attempt.'); } // Defaults. $data['attempt'] = $data['attempt'] ?? 1; $data['rawscore'] = $data['rawscore'] ?? 0; $data['maxscore'] = $data['maxscore'] ?? 0; $data['duration'] = $data['duration'] ?? 0; $data['completion'] = $data['completion'] ?? 1; $data['success'] = $data['success'] ?? 0; $data['attemptid'] = $this->get_attempt_object($data); // Check interaction type and create a valid record for it. $data['interactiontype'] = $data['interactiontype'] ?? 'compound'; $method = 'get_attempt_result_' . str_replace('-', '', $data['interactiontype']); if (!method_exists($this, $method)) { throw new Exception("Cannot create a {$data['interactiontype']} interaction statement"); } $this->insert_statement($data, $this->$method($data)); // If the activity has tracking enabled, try to recalculate grades. $activity = $DB->get_record('h5pactivity', ['id' => $data['h5pactivityid']]); if ($activity->enabletracking) { h5pactivity_update_grades($activity, $data['userid']); } } /** * Get or create an H5P attempt using the data array. * * @param array $attemptinfo the generator provided data * @return int the attempt id */ private function get_attempt_object($attemptinfo): int { global $DB; $result = $DB->get_record('h5pactivity_attempts', [ 'userid' => $attemptinfo['userid'], 'h5pactivityid' => $attemptinfo['h5pactivityid'], 'attempt' => $attemptinfo['attempt'], ]); if ($result) { return $result->id; } return $this->new_user_attempt($attemptinfo); } /** * Creates a user attempt. * * @param array $attemptinfo the current attempt information. * @return int the h5pactivity_attempt ID */ private function new_user_attempt(array $attemptinfo): int { global $DB; $record = (object)[ 'h5pactivityid' => $attemptinfo['h5pactivityid'], 'userid' => $attemptinfo['userid'], 'timecreated' => time(), 'timemodified' => time(), 'attempt' => $attemptinfo['attempt'], 'rawscore' => $attemptinfo['rawscore'], 'maxscore' => $attemptinfo['maxscore'], 'duration' => $attemptinfo['duration'], 'completion' => $attemptinfo['completion'], 'success' => $attemptinfo['success'], ]; if (empty($record->maxscore)) { $record->scaled = 0; } else { $record->scaled = $record->rawscore / $record->maxscore; } return $DB->insert_record('h5pactivity_attempts', $record); } /** * Insert a new statement into an attempt. * * If the interaction type is "compound" it will also update the attempt general result. * * @param array $attemptinfo the current attempt information * @param array $statement the statement tracking information * @return int the h5pactivity_attempt_result ID */ private function insert_statement(array $attemptinfo, array $statement): int { global $DB; $record = $statement + [ 'attemptid' => $attemptinfo['attemptid'], 'interactiontype' => $attemptinfo['interactiontype'] ?? 'compound', 'timecreated' => time(), 'rawscore' => $attemptinfo['rawscore'], 'maxscore' => $attemptinfo['maxscore'], 'duration' => $attemptinfo['duration'], 'completion' => $attemptinfo['completion'], 'success' => $attemptinfo['success'], ]; $result = $DB->insert_record('h5pactivity_attempts_results', $record); if ($record['interactiontype'] == 'compound') { $attempt = (object)[ 'id' => $attemptinfo['attemptid'], 'rawscore' => $record['rawscore'], 'maxscore' => $record['maxscore'], 'duration' => $record['duration'], 'completion' => $record['completion'], 'success' => $record['success'], ]; $DB->update_record('h5pactivity_attempts', $attempt); } return $result; } /** * Generates a valid compound tracking result. * * @param array $attemptinfo the current attempt information. * @return array with the required statement data */ private function get_attempt_result_compound(array $attemptinfo): array { $additionals = (object)[ "extensions" => (object)[ "http://h5p.org/x-api/h5p-local-content-id" => 1, ], "contextExtensions" => (object)[], ]; return [ 'subcontent' => '', 'description' => '', 'correctpattern' => '', 'response' => '', 'additionals' => json_encode($additionals), ]; } /** * Generates a valid choice tracking result. * * @param array $attemptinfo the current attempt information. * @return array with the required statement data */ private function get_attempt_result_choice(array $attemptinfo): array { $response = ($attemptinfo['rawscore']) ? '1[,]0' : '2[,]3'; $additionals = (object)[ "choices" => [ (object)[ "id" => "3", "description" => (object)[ "en-US" => "Another wrong answer\n", ], ], (object)[ "id" => "2", "description" => (object)[ "en-US" => "Wrong answer\n", ], ], (object)[ "id" => "1", "description" => (object)[ "en-US" => "This is also a correct answer\n", ], ], (object)[ "id" => "0", "description" => (object)[ "en-US" => "This is a correct answer\n", ], ], ], "extensions" => (object)[ "http://h5p.org/x-api/h5p-local-content-id" => 1, "http://h5p.org/x-api/h5p-subContentId" => "4367a919-ec47-43c9-b521-c22d9c0c0d8d", ], "contextExtensions" => (object)[], ]; return [ 'subcontent' => microtime(), 'description' => 'Select the correct answers', 'correctpattern' => '["1[,]0"]', 'response' => $response, 'additionals' => json_encode($additionals), ]; } /** * Generates a valid matching tracking result. * * @param array $attemptinfo the current attempt information. * @return array with the required statement data */ private function get_attempt_result_matching(array $attemptinfo): array { $response = ($attemptinfo['rawscore']) ? '0[.]0[,]1[.]1' : '1[.]0[,]0[.]1'; $additionals = (object)[ "source" => [ (object)[ "id" => "0", "description" => (object)[ "en-US" => "Drop item A\n", ], ], (object)[ "id" => "1", "description" => (object)[ "en-US" => "Drop item B\n", ], ], ], "target" => [ (object)[ "id" => "0", "description" => (object)[ "en-US" => "Drop zone A\n", ], ], (object)[ "id" => "1", "description" => (object)[ "en-US" => "Drop zone B\n", ], ], ], "extensions" => [ "http://h5p.org/x-api/h5p-local-content-id" => 1, "http://h5p.org/x-api/h5p-subContentId" => "682f1c74-c819-4e9d-8c36-12d9dc5fcdbc", ], "contextExtensions" => (object)[], ]; return [ 'subcontent' => microtime(), 'description' => 'Drag and Drop example 1', 'correctpattern' => '["0[.]0[,]1[.]1"]', 'response' => $response, 'additionals' => json_encode($additionals), ]; } /** * Generates a valid fill-in tracking result. * * @param array $attemptinfo the current attempt information. * @return array with the required statement data */ private function get_attempt_result_fillin(array $attemptinfo): array { $response = ($attemptinfo['rawscore']) ? 'first[,]second' : 'something[,]else'; $additionals = (object)[ "extensions" => (object)[ "http://h5p.org/x-api/h5p-local-content-id" => 1, "http://h5p.org/x-api/h5p-subContentId" => "1a3febd5-7edc-4336-8112-12756b945b62", "https://h5p.org/x-api/case-sensitivity" => true, "https://h5p.org/x-api/alternatives" => [ ["first"], ["second"], ], ], "contextExtensions" => (object)[ "https://h5p.org/x-api/h5p-reporting-version" => "1.1.0", ], ]; return [ 'subcontent' => microtime(), 'description' => '

This an example of missing word text.

The first answer if "first": the first answer is __________.

The second is second is "second": the secons answer is __________

', 'correctpattern' => '["{case_matters=true}first[,]second"]', 'response' => $response, 'additionals' => json_encode($additionals), ]; } /** * Generates a valid true-false tracking result. * * @param array $attemptinfo the current attempt information. * @return array with the required statement data */ private function get_attempt_result_truefalse(array $attemptinfo): array { $response = ($attemptinfo['rawscore']) ? 'true' : 'false'; $additionals = (object)[ "extensions" => (object)[ "http://h5p.org/x-api/h5p-local-content-id" => 1, "http://h5p.org/x-api/h5p-subContentId" => "5de9fb1e-aa03-4c9a-8cf0-3870b3f012ca", ], "contextExtensions" => (object)[], ]; return [ 'subcontent' => microtime(), 'description' => 'The correct answer is true.', 'correctpattern' => '["true"]', 'response' => $response, 'additionals' => json_encode($additionals), ]; } /** * Generates a valid long-fill-in tracking result. * * @param array $attemptinfo the current attempt information. * @return array with the required statement data */ private function get_attempt_result_longfillin(array $attemptinfo): array { $response = ($attemptinfo['rawscore']) ? 'The Hobbit is book' : 'Who cares?'; $additionals = (object)[ "extensions" => (object)[ "http://h5p.org/x-api/h5p-local-content-id" => 1, "http://h5p.org/x-api/h5p-subContentId" => "5de9fb1e-aa03-4c9a-8cf0-3870b3f012ca", ], "contextExtensions" => (object)[], ]; return [ 'subcontent' => microtime(), 'description' => '

Please describe the novel The Hobbit', 'correctpattern' => '', 'response' => $response, 'additionals' => json_encode($additionals), ]; } /** * Generates a valid sequencing tracking result. * * @param array $attemptinfo the current attempt information. * @return array with the required statement data */ private function get_attempt_result_sequencing(array $attemptinfo): array { $response = ($attemptinfo['rawscore']) ? 'true' : 'false'; $additionals = (object)[ "extensions" => (object)[ "http://h5p.org/x-api/h5p-local-content-id" => 1, "http://h5p.org/x-api/h5p-subContentId" => "5de9fb1e-aa03-4c9a-8cf0-3870b3f012ca", ], "contextExtensions" => (object)[], ]; return [ 'subcontent' => microtime(), 'description' => 'The correct answer is true.', 'correctpattern' => '["{case_matters=true}first[,]second"]', 'response' => $response, 'additionals' => json_encode($additionals), ]; } /** * Generates a valid other tracking result. * * @param array $attemptinfo the current attempt information. * @return array with the required statement data */ private function get_attempt_result_other(array $attemptinfo): array { $additionals = (object)[ "extensions" => (object)[ "http://h5p.org/x-api/h5p-local-content-id" => 1, ], "contextExtensions" => (object)[], ]; return [ 'subcontent' => microtime(), 'description' => '', 'correctpattern' => '', 'response' => '', 'additionals' => json_encode($additionals), ]; } } PK1B\>3tests/generator/behat_mod_h5pactivity_generator.phpnu[. /** * H5P-related steps definitions. * * @package mod_h5pactivity * @category test * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_mod_h5pactivity_generator extends behat_generator_base { protected function get_creatable_entities(): array { return [ 'attempts' => [ 'singular' => 'attempt', 'datagenerator' => 'attempt', 'required' => ['h5pactivity', 'user'], 'switchids' => ['h5pactivity' => 'h5pactivityid', 'user' => 'userid'], ], ]; } /** * Look up the id of a h5p from its name. * * @param string $h5pname the activity name, for example 'Test h5p'. * @return int corresponding id */ protected function get_h5pactivity_id(string $h5pname): int { global $DB; if (!$id = $DB->get_field('h5pactivity', 'id', ['name' => $h5pname])) { throw new Exception('There is no h5p activity with name "' . $h5pname); } return $id; } } PK1B\ 5S5S$tests/external/get_attempts_test.phpnu[. /** * External function test for get_attempts. * * @package mod_h5pactivity * @category external * @since Moodle 3.9 * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use mod_h5pactivity\local\manager; use core_external\external_api; use externallib_advanced_testcase; /** * External function test for get_attempts. * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class get_attempts_test extends externallib_advanced_testcase { /** * Test the behaviour of get_attempts. * * @dataProvider execute_data * @param int $grademethod the activity grading method * @param string $loginuser the user which calls the webservice * @param string|null $participant the user to get the data * @param bool $createattempts if the student user has attempts created * @param int|null $count the expected number of attempts returned (null for exception) */ public function test_execute(int $grademethod, string $loginuser, ?string $participant, bool $createattempts, ?int $count): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course, 'enabletracking' => 1, 'grademethod' => $grademethod]); $manager = manager::create_from_instance($activity); $cm = $manager->get_coursemodule(); // Prepare users: 1 teacher, 2 students, 1 unenroled user. $users = [ 'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'), 'student' => $this->getDataGenerator()->create_and_enrol($course, 'student'), 'other' => $this->getDataGenerator()->create_and_enrol($course, 'student'), 'noenrolled' => $this->getDataGenerator()->create_user(), ]; $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); if ($createattempts) { $user = $users['student']; $params = ['cmid' => $cm->id, 'userid' => $user->id]; $generator->create_content($activity, $params); $generator->create_content($activity, $params); } // Create another user with 2 attempts to validate no cross attempts are returned. $user = $users['other']; $params = ['cmid' => $cm->id, 'userid' => $user->id]; $generator->create_content($activity, $params); $generator->create_content($activity, $params); // Execute external method. $this->setUser($users[$loginuser]); $userids = ($participant) ? [$users[$participant]->id] : []; $checkuserid = ($participant) ? $users[$participant]->id : $users[$loginuser]->id; $result = get_attempts::execute($activity->id, $userids); $result = external_api::clean_returnvalue( get_attempts::execute_returns(), $result ); // Validate general structure. $this->assertArrayHasKey('activityid', $result); $this->assertArrayHasKey('usersattempts', $result); $this->assertArrayHasKey('warnings', $result); $this->assertEquals($activity->id, $result['activityid']); if ($count === null) { $this->assertCount(1, $result['warnings']); $this->assertCount(0, $result['usersattempts']); return; } $this->assertCount(0, $result['warnings']); $this->assertCount(1, $result['usersattempts']); $userattempts = $result['usersattempts'][0]; $this->assertEquals($checkuserid, $userattempts['userid']); // Validate scored attempts. if ($grademethod == manager::GRADEMANUAL || $grademethod == manager::GRADEAVERAGEATTEMPT || $count == 0) { $this->assertArrayNotHasKey('scored', $userattempts); } else { $this->assertArrayHasKey('scored', $userattempts); list($dbgrademethod, $title) = $manager->get_selected_attempt(); $this->assertEquals($grademethod, $dbgrademethod); $this->assertEquals($grademethod, $userattempts['scored']['grademethod']); $this->assertEquals($title, $userattempts['scored']['title']); $this->assertCount(1, $userattempts['scored']['attempts']); } // Validate returned attempts. $this->assertCount($count, $userattempts['attempts']); foreach ($userattempts['attempts'] as $attempt) { $this->assertArrayHasKey('id', $attempt); $this->assertEquals($checkuserid, $attempt['userid']); $this->assertArrayHasKey('timecreated', $attempt); $this->assertArrayHasKey('timemodified', $attempt); $this->assertArrayHasKey('attempt', $attempt); $this->assertArrayHasKey('rawscore', $attempt); $this->assertArrayHasKey('maxscore', $attempt); $this->assertArrayHasKey('duration', $attempt); $this->assertArrayHasKey('completion', $attempt); $this->assertArrayHasKey('success', $attempt); $this->assertArrayHasKey('scaled', $attempt); } } /** * Data provider for the test_execute tests. * * @return array */ public static function execute_data(): array { return [ // Teacher checking a user with attempts. 'Manual grade, Teacher asking participant with attempts' => [ manager::GRADEMANUAL, 'editingteacher', 'student', true, 2 ], 'Highest grade, Teacher asking participant with attempts' => [ manager::GRADEHIGHESTATTEMPT, 'editingteacher', 'student', true, 2 ], 'Average grade, Teacher asking participant with attempts' => [ manager::GRADEAVERAGEATTEMPT, 'editingteacher', 'student', true, 2 ], 'Last grade, Teacher asking participant with attempts' => [ manager::GRADELASTATTEMPT, 'editingteacher', 'student', true, 2 ], 'First grade, Teacher asking participant with attempts' => [ manager::GRADEFIRSTATTEMPT, 'editingteacher', 'student', true, 2 ], // Teacher checking a user without attempts. 'Manual grade, Teacher asking participant without attempts' => [ manager::GRADEMANUAL, 'editingteacher', 'student', false, 0 ], 'Highest grade, Teacher asking participant without attempts' => [ manager::GRADEHIGHESTATTEMPT, 'editingteacher', 'student', false, 0 ], 'Average grade, Teacher asking participant without attempts' => [ manager::GRADEAVERAGEATTEMPT, 'editingteacher', 'student', false, 0 ], 'Last grade, Teacher asking participant without attempts' => [ manager::GRADELASTATTEMPT, 'editingteacher', 'student', false, 0 ], 'First grade, Teacher asking participant without attempts' => [ manager::GRADEFIRSTATTEMPT, 'editingteacher', 'student', false, 0 ], // Student checking own attempts specifying userid. 'Manual grade, check same user attempts report with attempts' => [ manager::GRADEMANUAL, 'student', 'student', true, 2 ], 'Highest grade, check same user attempts report with attempts' => [ manager::GRADEHIGHESTATTEMPT, 'student', 'student', true, 2 ], 'Average grade, check same user attempts report with attempts' => [ manager::GRADEAVERAGEATTEMPT, 'student', 'student', true, 2 ], 'Last grade, check same user attempts report with attempts' => [ manager::GRADELASTATTEMPT, 'student', 'student', true, 2 ], 'First grade, check same user attempts report with attempts' => [ manager::GRADEFIRSTATTEMPT, 'student', 'student', true, 2 ], // Student checking own attempts. 'Manual grade, check own attempts report with attempts' => [ manager::GRADEMANUAL, 'student', null, true, 2 ], 'Highest grade, check own attempts report with attempts' => [ manager::GRADEHIGHESTATTEMPT, 'student', null, true, 2 ], 'Average grade, check own attempts report with attempts' => [ manager::GRADEAVERAGEATTEMPT, 'student', null, true, 2 ], 'Last grade, check own attempts report with attempts' => [ manager::GRADELASTATTEMPT, 'student', null, true, 2 ], 'First grade, check own attempts report with attempts' => [ manager::GRADEFIRSTATTEMPT, 'student', null, true, 2 ], // Student checking own report without attempts. 'Manual grade, check own attempts report without attempts' => [ manager::GRADEMANUAL, 'student', 'student', false, 0 ], 'Highest grade, check own attempts report without attempts' => [ manager::GRADEHIGHESTATTEMPT, 'student', 'student', false, 0 ], 'Average grade, check own attempts report without attempts' => [ manager::GRADEAVERAGEATTEMPT, 'student', 'student', false, 0 ], 'Last grade, check own attempts report without attempts' => [ manager::GRADELASTATTEMPT, 'student', 'student', false, 0 ], 'First grade, check own attempts report without attempts' => [ manager::GRADEFIRSTATTEMPT, 'student', 'student', false, 0 ], // Student trying to get another user attempts. 'Manual grade, student trying to stalk another student' => [ manager::GRADEMANUAL, 'student', 'other', false, null ], 'Highest grade, student trying to stalk another student' => [ manager::GRADEHIGHESTATTEMPT, 'student', 'other', false, null ], 'Average grade, student trying to stalk another student' => [ manager::GRADEAVERAGEATTEMPT, 'student', 'other', false, null ], 'Last grade, student trying to stalk another student' => [ manager::GRADELASTATTEMPT, 'student', 'other', false, null ], 'First grade, student trying to stalk another student' => [ manager::GRADEFIRSTATTEMPT, 'student', 'other', false, null ], // Teacher trying to get a non enroled user attempts. 'Manual grade, teacher trying to get an non enrolled user attempts' => [ manager::GRADEMANUAL, 'editingteacher', 'noenrolled', false, null ], 'Highest grade, teacher trying to get an non enrolled user attempts' => [ manager::GRADEHIGHESTATTEMPT, 'editingteacher', 'noenrolled', false, null ], 'Average grade, teacher trying to get an non enrolled user attempts' => [ manager::GRADEAVERAGEATTEMPT, 'editingteacher', 'noenrolled', false, null ], 'Last grade, teacher trying to get an non enrolled user attempts' => [ manager::GRADELASTATTEMPT, 'editingteacher', 'noenrolled', false, null ], 'First grade, teacher trying to get an non enrolled user attempts' => [ manager::GRADEFIRSTATTEMPT, 'editingteacher', 'noenrolled', false, null ], // Student trying to get a non enroled user attempts. 'Manual grade, student trying to get an non enrolled user attempts' => [ manager::GRADEMANUAL, 'student', 'noenrolled', false, null ], 'Highest grade, student trying to get an non enrolled user attempts' => [ manager::GRADEHIGHESTATTEMPT, 'student', 'noenrolled', false, null ], 'Average grade, student trying to get an non enrolled user attempts' => [ manager::GRADEAVERAGEATTEMPT, 'student', 'noenrolled', false, null ], 'Last grade, student trying to get an non enrolled user attempts' => [ manager::GRADELASTATTEMPT, 'student', 'noenrolled', false, null ], 'First grade, student trying to get an non enrolled user attempts' => [ manager::GRADEFIRSTATTEMPT, 'student', 'noenrolled', false, null ], ]; } /** * Test the behaviour of get_attempts when tracking is not enabled. * */ public function test_execute_no_tracking(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course, 'enabletracking' => 0]); $manager = manager::create_from_instance($activity); $cm = $manager->get_coursemodule(); // Prepare users: 1 teacher, 1 student. $users = [ 'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'), 'student' => $this->getDataGenerator()->create_and_enrol($course, 'student'), ]; // Execute external method. $this->setUser($users['editingteacher']); $result = get_attempts::execute($activity->id, [$users['student']->id]); $result = external_api::clean_returnvalue( get_attempts::execute_returns(), $result ); $this->assertCount(1, $result['warnings']); $this->assertCount(0, $result['usersattempts']); } /** * Test the behaviour of get_attempts when own review is not allowed. * */ public function test_execute_no_own_review(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course, 'enabletracking' => 1, 'reviewmode' => manager::REVIEWNONE]); $manager = manager::create_from_instance($activity); $cm = $manager->get_coursemodule(); // Prepare users: 1 student. $users = [ 'student' => $this->getDataGenerator()->create_and_enrol($course, 'student'), ]; // Execute external method. $this->setUser($users['student']); $result = get_attempts::execute($activity->id); $result = external_api::clean_returnvalue( get_attempts::execute_returns(), $result ); $this->assertCount(1, $result['warnings']); $this->assertCount(0, $result['usersattempts']); } /** * Test the behaviour of get_attempts getting more than one user at once. * * @dataProvider execute_multipleusers_data * @param string $loginuser the user which calls the webservice * @param string[] $participants the users to get the data * @param string[] $warnings the expected users with warnings * @param string[] $resultusers expected users in the resultusers */ public function test_execute_multipleusers(string $loginuser, array $participants, array $warnings, array $resultusers): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $manager = manager::create_from_instance($activity); $cm = $manager->get_coursemodule(); // Prepare users: 1 teacher, 2 students with attempts, 1 student without, 1 no enrolled. $users = [ 'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'), 'student1' => $this->getDataGenerator()->create_and_enrol($course, 'student'), 'student2' => $this->getDataGenerator()->create_and_enrol($course, 'student'), 'noattempts' => $this->getDataGenerator()->create_and_enrol($course, 'student'), 'noenrolled' => $this->getDataGenerator()->create_user(), ]; // Generate attempts (student1 with 1 attempt, student2 with 2). $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); $user = $users['student1']; $params = ['cmid' => $cm->id, 'userid' => $user->id]; $generator->create_content($activity, $params); $user = $users['student2']; $params = ['cmid' => $cm->id, 'userid' => $user->id]; $generator->create_content($activity, $params); $generator->create_content($activity, $params); $countattempts = [ $users['editingteacher']->id => 0, $users['student1']->id => 1, $users['student2']->id => 2, $users['noattempts']->id => 0, $users['noenrolled']->id => 0, ]; // Execute external method. $this->setUser($users[$loginuser]); $userids = []; foreach ($participants as $participant) { $userids[] = $users[$participant]->id; } $result = get_attempts::execute($activity->id, $userids); $result = external_api::clean_returnvalue( get_attempts::execute_returns(), $result ); $this->assertCount(count($warnings), $result['warnings']); $this->assertCount(count($resultusers), $result['usersattempts']); $expectedwarnings = []; foreach ($warnings as $warninguser) { $id = $users[$warninguser]->id; $expectedwarnings[$id] = $warninguser; } foreach ($result['warnings'] as $warning) { $this->assertEquals('user', $warning['item']); $this->assertEquals(1, $warning['warningcode']); $this->assertArrayHasKey($warning['itemid'], $expectedwarnings); } $expectedusers = []; foreach ($resultusers as $resultuser) { $id = $users[$resultuser]->id; $expectedusers[$id] = $resultuser; } foreach ($result['usersattempts'] as $usersattempts) { $this->assertArrayHasKey('userid', $usersattempts); $userid = $usersattempts['userid']; $this->assertArrayHasKey($userid, $expectedusers); $this->assertCount($countattempts[$userid], $usersattempts['attempts']); if ($countattempts[$userid]) { $this->assertArrayHasKey('scored', $usersattempts); } } } /** * Data provider for the test_execute_multipleusers. * * @return array */ public static function execute_multipleusers_data(): array { return [ // Teacher checks. 'Teacher checking students with attempts' => [ 'editingteacher', ['student1', 'student2'], [], ['student1', 'student2'] ], 'Teacher checking one student with atempts and one not' => [ 'editingteacher', ['student1', 'noattempts'], [], ['student1', 'noattempts'] ], 'Teacher checking no students' => [ 'editingteacher', [], [], ['editingteacher'] ], 'Teacher checking one student and a no enrolled user' => [ 'editingteacher', ['student1', 'noenrolled'], ['noenrolled'], ['student1'] ], // Student checks. 'Student checking self attempts and another user' => [ 'student1', ['student1', 'student2'], ['student2'], ['student1'] ], 'Student checking no students' => [ 'student1', [], [], ['student1'] ], 'Student checking self attempts and a no enrolled user' => [ 'student1', ['student1', 'noenrolled'], ['noenrolled'], ['student1'] ], ]; } } PK1B\J99)tests/external/log_report_viewed_test.phpnu[. namespace mod_h5pactivity\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use mod_h5pactivity\local\manager; use core_external\external_api; use externallib_advanced_testcase; /** * External function test for log_report_viewed. * * @package mod_h5pactivity * @category external * @covers \mod_h5pactivity\external\log_report_viewed * @since Moodle 3.11 * @copyright 2021 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class log_report_viewed_test extends externallib_advanced_testcase { /** * Test the behaviour of log_report_viewed. * * @dataProvider execute_data * @param int $enabletracking the activity tracking enable * @param int $reviewmode the activity review mode * @param string $loginuser the user which calls the webservice * @param string|null $participant the user to log the data */ public function test_execute(int $enabletracking, int $reviewmode, string $loginuser, ?string $participant): void { $this->resetAfterTest(); $this->setAdminUser(); // Create a course. $course = $this->getDataGenerator()->create_course(); // Enrol users: 1 teacher, 1 student. $users = [ 'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'), 'student' => $this->getDataGenerator()->create_and_enrol($course, 'student'), ]; // Add h5p activity. $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course, 'enabletracking' => $enabletracking, 'reviewmode' => $reviewmode]); // Create attempt for h5p activity. $attempts = []; $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); $user = $users['student']; $manager = manager::create_from_instance($activity); $cm = $manager->get_coursemodule(); $params = ['cmid' => $cm->id, 'userid' => $user->id]; $attempts['student'] = $generator->create_content($activity, $params); // Redirect events to the sink, so we can recover them later. $sink = $this->redirectEvents(); // Execute external method. $this->setUser($users[$loginuser]); $attemptid = $attempts[$participant]->id ?? 0; $result = log_report_viewed::execute($activity->id, $user->id, $attemptid); $result = external_api::clean_returnvalue( log_report_viewed::execute_returns(), $result ); // Validate general structure. $this->assertArrayHasKey('status', $result); $events = $sink->get_events(); $event = end($events); // Check the event details are correct. $this->assertInstanceOf('mod_h5pactivity\event\report_viewed', $event); $this->assertEquals(\context_module::instance($cm->id), $event->get_context()); $this->assertEquals($cm->instance, $event->other['instanceid']); $this->assertEquals($user->id, $event->other['userid']); $this->assertEquals($attemptid, $event->other['attemptid']); } /** * Data provider for the test_execute tests. * * @return array */ public static function execute_data(): array { return [ 'Student reviewing own attempt' => [ 1, manager::REVIEWCOMPLETION, 'student', 'student' ], 'Teacher reviewing student attempts' => [ 1, manager::REVIEWCOMPLETION, 'editingteacher', 'student' ], ]; } } PK1B\((4tests/external/get_h5pactivities_by_courses_test.phpnu[. /** * External function test for get_h5pactivities_by_courses. * * @package mod_h5pactivity * @category external * @since Moodle 3.9 * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use core_external\external_api; use externallib_advanced_testcase; use context_module; /** * External function test for get_h5pactivities_by_courses. * * @package mod_h5pactivity * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class get_h5pactivities_by_courses_test extends externallib_advanced_testcase { /** * Test test_get_h5pactivities_by_courses user student. */ public function test_get_h5pactivities_by_courses(): void { global $CFG, $DB; $this->resetAfterTest(); $this->setAdminUser(); // Create 2 courses. // Course 1 -> 2 activities with H5P files package without deploy. // Course 2 -> 1 activity with H5P file package deployed. $course1 = $this->getDataGenerator()->create_course(); $params = [ 'course' => $course1->id, 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/filltheblanks.h5p', 'introformat' => 1 ]; $activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params); // Add filename and contextid to make easier the asserts. $activities[0]->filename = 'filltheblanks.h5p'; $context = context_module::instance($activities[0]->cmid); $activities[0]->contextid = $context->id; $params = [ 'course' => $course1->id, 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/greeting-card.h5p', 'introformat' => 1 ]; $activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params); // Add filename and contextid to make easier the asserts. $activities[1]->filename = 'greeting-card.h5p'; $context = context_module::instance($activities[1]->cmid); $activities[1]->contextid = $context->id; $course2 = $this->getDataGenerator()->create_course(); $params = [ 'course' => $course2->id, 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/guess-the-answer.h5p', 'introformat' => 1 ]; $activities[] = $this->getDataGenerator()->create_module('h5pactivity', $params); $activities[2]->filename = 'guess-the-answer.h5p'; $context = context_module::instance($activities[2]->cmid); $activities[2]->contextid = $context->id; // Create a fake deploy H5P file. $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); $deployedfile = $generator->create_export_file($activities[2]->filename, $context->id, 'mod_h5pactivity', 'package'); // Create a user and enrol as student in both courses. $user = $this->getDataGenerator()->create_user(); $studentrole = $DB->get_record('role', ['shortname' => 'student']); $maninstance1 = $DB->get_record('enrol', ['courseid' => $course1->id, 'enrol' => 'manual'], '*', MUST_EXIST); $maninstance2 = $DB->get_record('enrol', ['courseid' => $course2->id, 'enrol' => 'manual'], '*', MUST_EXIST); $manual = enrol_get_plugin('manual'); $manual->enrol_user($maninstance1, $user->id, $studentrole->id); $manual->enrol_user($maninstance2, $user->id, $studentrole->id); // Set admin settings. set_config('enablesavestate', 1, 'mod_h5pactivity'); set_config('savestatefreq', 120, 'mod_h5pactivity'); // Check the activities returned by the first course. $this->setUser($user); $courseids = [$course1->id]; $result = get_h5pactivities_by_courses::execute($courseids); $result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(2, $result['h5pactivities']); $this->assert_activities($activities, $result); $this->assertNotContains('deployedfile', $result['h5pactivities'][0]); $this->assertNotContains('deployedfile', $result['h5pactivities'][1]); $this->assertEquals(1, $result['h5pglobalsettings']['enablesavestate']); $this->assertEquals(120, $result['h5pglobalsettings']['savestatefreq']); // Call the external function without passing course id. // Expected result, all the courses, course1 and course2. $result = get_h5pactivities_by_courses::execute([]); $result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(3, $result['h5pactivities']); // We need to sort the $result by id. // Because we are not sure how it is ordered with more than one course. array_multisort(array_map(function($element) { return $element['id']; }, $result['h5pactivities']), SORT_ASC, $result['h5pactivities']); $this->assert_activities($activities, $result); $this->assertNotContains('deployedfile', $result['h5pactivities'][0]); $this->assertNotContains('deployedfile', $result['h5pactivities'][1]); // Only the activity from the second course has been deployed. $this->assertEquals($deployedfile['filename'], $result['h5pactivities'][2]['deployedfile']['filename']); $this->assertEquals($deployedfile['filepath'], $result['h5pactivities'][2]['deployedfile']['filepath']); $this->assertEquals($deployedfile['filesize'], $result['h5pactivities'][2]['deployedfile']['filesize']); $this->assertEquals($deployedfile['timemodified'], $result['h5pactivities'][2]['deployedfile']['timemodified']); $this->assertEquals($deployedfile['mimetype'], $result['h5pactivities'][2]['deployedfile']['mimetype']); $this->assertEquals($deployedfile['fileurl'], $result['h5pactivities'][2]['deployedfile']['fileurl']); $this->assertEquals(1, $result['h5pglobalsettings']['enablesavestate']); $this->assertEquals(120, $result['h5pglobalsettings']['savestatefreq']); // Unenrol user from second course. $manual->unenrol_user($maninstance2, $user->id); // Remove the last activity from the array. array_pop($activities); // Disable save state. set_config('enablesavestate', 0, 'mod_h5pactivity'); // Call the external function without passing course id. $result = get_h5pactivities_by_courses::execute([]); $result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result); $this->assertCount(0, $result['warnings']); $this->assertCount(2, $result['h5pactivities']); $this->assert_activities($activities, $result); $this->assertEquals(0, $result['h5pglobalsettings']['enablesavestate']); $this->assertNotContains('savestatefreq', $result['h5pglobalsettings']); // Call for the second course we unenrolled the user from, expected warning. $result = get_h5pactivities_by_courses::execute([$course2->id]); $result = external_api::clean_returnvalue(get_h5pactivities_by_courses::execute_returns(), $result); $this->assertCount(1, $result['warnings']); $this->assertEquals('1', $result['warnings'][0]['warningcode']); $this->assertEquals($course2->id, $result['warnings'][0]['itemid']); } /** * Create a scenario to use into the tests. * * @param array $activities list of H5P activities. * @param array $result list of H5P activities by WS. * @return void */ protected function assert_activities(array $activities, array $result): void { $total = count($result['h5pactivities']); for ($i = 0; $i < $total; $i++) { $this->assertEquals($activities[$i]->id, $result['h5pactivities'][$i]['id']); $this->assertEquals($activities[$i]->course, $result['h5pactivities'][$i]['course']); $this->assertEquals($activities[$i]->name, $result['h5pactivities'][$i]['name']); $this->assertEquals($activities[$i]->timecreated, $result['h5pactivities'][$i]['timecreated']); $this->assertEquals($activities[$i]->timemodified, $result['h5pactivities'][$i]['timemodified']); $this->assertEquals($activities[$i]->intro, $result['h5pactivities'][$i]['intro']); $this->assertEquals($activities[$i]->introformat, $result['h5pactivities'][$i]['introformat']); $this->assertEquals([], $result['h5pactivities'][$i]['introfiles']); $this->assertEquals($activities[$i]->grade, $result['h5pactivities'][$i]['grade']); $this->assertEquals($activities[$i]->displayoptions, $result['h5pactivities'][$i]['displayoptions']); $this->assertEquals($activities[$i]->enabletracking, $result['h5pactivities'][$i]['enabletracking']); $this->assertEquals($activities[$i]->grademethod, $result['h5pactivities'][$i]['grademethod']); $this->assertEquals($activities[$i]->cmid, $result['h5pactivities'][$i]['coursemodule']); $this->assertEquals($activities[$i]->contextid, $result['h5pactivities'][$i]['context']); $this->assertEquals($activities[$i]->filename, $result['h5pactivities'][$i]['package'][0]['filename']); } } } PK1B\`t  (tests/external/view_h5pactivity_test.phpnu[. /** * External function test for view_h5pactivity. * * @package mod_h5pactivity * @category external * @since Moodle 3.9 * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use core_external\external_api; use externallib_advanced_testcase; use stdClass; use context_module; use course_modinfo; /** * External function test for view_h5pactivity. * * @package mod_h5pactivity * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class view_h5pactivity_test extends externallib_advanced_testcase { /** * Test test_view_h5pactivity invalid id. */ public function test_view_h5pactivity_invalid_id(): void { $this->resetAfterTest(); $this->setAdminUser(); $this->expectException('moodle_exception'); $result = view_h5pactivity::execute(0); } /** * Test test_view_h5pactivity user not enrolled. */ public function test_view_h5pactivity_user_not_enrolled(): void { $this->resetAfterTest(); $this->setAdminUser(); // Setup scenario. $scenario = $this->setup_scenario(); // Test not-enrolled user. $usernotenrolled = self::getDataGenerator()->create_user(); $this->setUser($usernotenrolled); $this->expectException('moodle_exception'); $result = view_h5pactivity::execute($scenario->h5pactivity->id); } /** * Test test_view_h5pactivity user student. */ public function test_view_h5pactivity_user_student(): void { $this->resetAfterTest(); $this->setAdminUser(); // Setup scenario. $scenario = $this->setup_scenario(); $cm = get_coursemodule_from_instance('h5pactivity', $scenario->h5pactivity->id); $this->setUser($scenario->student); // Trigger and capture the event. $sink = $this->redirectEvents(); $result = view_h5pactivity::execute($scenario->h5pactivity->id); $result = external_api::clean_returnvalue(view_h5pactivity::execute_returns(), $result); $this->assertTrue($result['status']); $events = $sink->get_events(); $this->assertCount(1, $events); $event = array_shift($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_h5pactivity\event\course_module_viewed', $event); $this->assertEquals($scenario->contextmodule, $event->get_context()); $h5pactivity = new \moodle_url('/mod/h5pactivity/view.php', array('id' => $cm->id)); $this->assertEquals($h5pactivity, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); } /** * Test test_view_h5pactivity user missing capabilities. */ public function test_view_h5pactivity_user_missing_capabilities(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); // Setup scenario. $scenario = $this->setup_scenario(); $studentrole = $DB->get_record('role', array('shortname' => 'student')); // Test user with no capabilities. // We need a explicit prohibit since this capability is only defined in authenticated user and guest roles. assign_capability('mod/h5pactivity:view', CAP_PROHIBIT, $studentrole->id, $scenario->contextmodule->id); // Empty all the caches that may be affected by this change. accesslib_clear_all_caches_for_unit_testing(); course_modinfo::clear_instance_cache(); $this->setUser($scenario->student); $this->expectException('moodle_exception'); $result = view_h5pactivity::execute($scenario->h5pactivity->id); } /** * Create a scenario to use into the tests. * * @return stdClass $scenario */ protected function setup_scenario() { $course = $this->getDataGenerator()->create_course(); $h5pactivity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $contextmodule = context_module::instance($h5pactivity->cmid); $scenario = new stdClass(); $scenario->contextmodule = $contextmodule; $scenario->student = $student; $scenario->h5pactivity = $h5pactivity; return $scenario; } } PK1B\}}:tests/external/get_h5pactivity_access_information_test.phpnu[. /** * External function test for get_h5pactivity_access_information. * * @package mod_h5pactivity * @category external * @since Moodle 3.9 * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use dml_missing_record_exception; use core_external\external_api; use externallib_advanced_testcase; /** * External function test for get_h5pactivity_access_information. * * @package mod_h5pactivity * @copyright 2020 Carlos Escobedo * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class get_h5pactivity_access_information_test extends externallib_advanced_testcase { /** * Test the behaviour of get_h5pactivity_access_information(). * * @dataProvider get_h5pactivity_access_information_data * @param string $role user role in course * @param int $enabletracking if tracking is enabled * @param array $enabledcaps capabilities enabled */ public function test_get_h5pactivity_access_information(string $role, int $enabletracking, array $enabledcaps): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', [ 'course' => $course, 'enabletracking' => $enabletracking ] ); if ($role) { $user = $this->getDataGenerator()->create_and_enrol($course, $role); $this->setUser($user); } // Check the access information. $result = get_h5pactivity_access_information::execute($activity->id); $result = external_api::clean_returnvalue(get_h5pactivity_access_information::execute_returns(), $result); $this->assertCount(0, $result['warnings']); unset($result['warnings']); // Check the values for capabilities. foreach ($result as $capname => $capvalue) { if (in_array($capname, $enabledcaps)) { $this->assertTrue($capvalue); } else { $this->assertFalse($capvalue); } } } /** * Data provider for get_h5pactivity_access_information. * * @return array */ public static function get_h5pactivity_access_information_data(): array { return [ 'Admin, tracking enabled' => [ '', 1, ['canview', 'canreviewattempts', 'canaddinstance'] ], 'Admin, tracking disabled' => [ '', 0, ['canview', 'canreviewattempts', 'canaddinstance'] ], 'Student, tracking enabled' => [ 'student', 1, ['canview', 'cansubmit'] ], 'Student, tracking disabled' => [ 'student', 0, ['canview'] ], 'Teacher, tracking enabled' => [ 'editingteacher', 1, [ 'canview', 'canreviewattempts', 'canaddinstance' ] ], 'Teacher, tracking disabled' => [ 'editingteacher', 0, [ 'canview', 'canreviewattempts', 'canaddinstance' ] ], ]; } /** * Test dml_missing_record_exception in get_h5pactivity_access_information. */ public function test_dml_missing_record_exception(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); // Call the WS using an unexisting h5pactivityid. $this->expectException(dml_missing_record_exception::class); $result = get_h5pactivity_access_information::execute(1); } }PK1B\m%m%)tests/external/get_user_attempts_test.phpnu[. /** * External function test for get_user_attempts. * * @package mod_h5pactivity * @category external * @since Moodle 3.11 * @copyright 2020 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use mod_h5pactivity\local\manager; use core_external\external_api; use externallib_advanced_testcase; /** * External function test for get_user_attempts. * * @package mod_h5pactivity * @copyright 2020 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class get_user_attempts_test extends externallib_advanced_testcase { /** * Test the behaviour of get_user_attempts getting more than one user at once. * * @dataProvider execute_multipleusers_data * @param string $loginuser the user which calls the webservice * @param string[] $participants the users to get the data * @param string[] $warnings the expected users with warnings * @param string[] $resultusers expected users in the resultusers */ public function test_execute_multipleusers(string $loginuser, array $participants, array $warnings, array $resultusers): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $manager = manager::create_from_instance($activity); $cm = $manager->get_coursemodule(); $users = ['editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher')]; // Prepare users. foreach ($participants as $participant) { if ($participant == 'noenrolled') { $users[$participant] = $this->getDataGenerator()->create_user(); } else { $users[$participant] = $this->getDataGenerator()->create_and_enrol($course, 'student'); } } // Generate attempts (student1 with 1 attempt, student2 with 2 etc). $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); $attemptcount = 1; $totalattempts = 0; foreach ($users as $key => $user) { if (($key == 'noattempts') || ($key == 'noenrolled') || ($key == 'editingteacher')) { $countattempts[$user->id] = 0; } else { $params = ['cmid' => $cm->id, 'userid' => $user->id]; for ($i = 1; $i <= $attemptcount; $i++) { $generator->create_content($activity, $params); $totalattempts++; } $countattempts[$user->id] = $attemptcount; $attemptcount++; } } // Execute external method. $this->setUser($users[$loginuser]); if ($loginuser == 'student1') { $this->expectException('moodle_exception'); $this->expectExceptionMessage('h5pactivity:reviewattempts required view attempts' . ' of all enrolled users'); } $result = get_user_attempts::execute($activity->id); $result = external_api::clean_returnvalue( get_user_attempts::execute_returns(), $result ); $this->assertEquals($totalattempts, $result['totalattempts']); $this->assertCount(count($warnings), $result['warnings']); // Teacher is excluded. $this->assertCount(count($resultusers), $result['usersattempts']); $expectedwarnings = []; foreach ($warnings as $warninguser) { $id = $users[$warninguser]->id; $expectedwarnings[$id] = $warninguser; } foreach ($result['warnings'] as $warning) { $this->assertEquals('user', $warning['item']); $this->assertEquals(1, $warning['warningcode']); $this->assertArrayHasKey($warning['itemid'], $expectedwarnings); } $expectedusers = []; foreach ($resultusers as $resultuser) { $id = $users[$resultuser]->id; $expectedusers[$id] = $resultuser; } foreach ($result['usersattempts'] as $usersattempts) { $this->assertArrayHasKey('userid', $usersattempts); $userid = $usersattempts['userid']; $this->assertArrayHasKey($userid, $expectedusers); $this->assertCount($countattempts[$userid], $usersattempts['attempts']); if ($countattempts[$userid]) { $this->assertArrayHasKey('scored', $usersattempts); } } } /** * Data provider for the test_execute_multipleusers. * * @return array */ public static function execute_multipleusers_data(): array { return [ // Teacher checks. 'Teacher checking students with attempts' => [ 'editingteacher', ['student1', 'student2', 'student3', 'student4', 'student5'], [], ['student1', 'student2', 'student3', 'student4', 'student5'], ], 'Teacher checking 2 students with atempts and one not' => [ 'editingteacher', ['student1', 'student2', 'noattempts'], [], ['student1', 'student2', 'noattempts'], ], 'Teacher checking no students' => [ 'editingteacher', [], [], [], ], 'Teacher checking one student and a no enrolled user' => [ 'editingteacher', ['student1', 'noenrolled'], [], ['student1'], ], // Permission check. 'Student checking attempts and another user' => [ 'student1', ['student1', 'student2'], ['student2'], ['student1'], ], ]; } /** * Data provider for {@see test_execute_with_sortorder} * * @return array[] */ public static function execute_with_sortorder(): array { return [ 'Sort by id' => ['id', ['user01', 'user02']], 'Sort by id desc' => ['id desc', ['user02', 'user01']], 'Sort by id asc' => ['id asc', ['user01', 'user02']], 'Sort by firstname' => ['firstname', ['user01', 'user02']], 'Sort by firstname desc' => ['firstname desc', ['user02', 'user01']], 'Sort by firstname asc' => ['firstname asc', ['user01', 'user02']], 'Sort by lastname' => ['lastname', ['user02', 'user01']], 'Sort by lastname desc' => ['lastname desc', ['user01', 'user02']], 'Sort by lastname asc' => ['lastname asc', ['user02', 'user01']], // Edge cases (should fall back to default). 'Sort by empty string' => ['', ['user01', 'user02']], 'Sort by invalid field' => ['invalid', ['user01', 'user02']], ]; } /** * Test external execute method with sortorder * * @param string $sortorder * @param string[] $expectedorder * * @dataProvider execute_with_sortorder */ public function test_execute_with_sortorder(string $sortorder, array $expectedorder): void { $this->resetAfterTest(); $this->setAdminUser(); // Create course, module. $course = $this->getDataGenerator()->create_course(); $module = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); // Couple of enrolled users in the course. $users['user01'] = $this->getDataGenerator()->create_and_enrol($course, 'student', [ 'username' => 'user01', 'firstname' => 'Adam', 'lastname' => 'Zebra', ]); $users['user02'] = $this->getDataGenerator()->create_and_enrol($course, 'student', [ 'username' => 'user02', 'firstname' => 'Zoe', 'lastname' => 'Apples', ]); $result = external_api::clean_returnvalue( get_user_attempts::execute_returns(), get_user_attempts::execute($module->id, $sortorder) ); // Map expected order of usernames to user IDs. $expectedorderbyuserid = array_map(static function(string $username) use ($users): int { return $users[$username]->id; }, $expectedorder); // The order should match the ordering of user attempt user IDs. $this->assertEquals($expectedorderbyuserid, array_column($result['usersattempts'], 'userid')); } } PK1B\AA#tests/external/get_results_test.phpnu[. /** * External function test for get_results. * * @package mod_h5pactivity * @category external * @since Moodle 3.9 * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\external; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot . '/webservice/tests/helpers.php'); use mod_h5pactivity\local\manager; use core_external\external_api; use externallib_advanced_testcase; use dml_missing_record_exception; /** * External function test for get_results. * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class get_results_test extends externallib_advanced_testcase { /** * Test the behaviour of get_results. * * @dataProvider execute_data * @param int $enabletracking the activity tracking enable * @param int $reviewmode the activity review mode * @param string $loginuser the user which calls the webservice * @param string|null $participant the user to get the data * @param bool $createattempts if the student user has attempts created * @param int|null $count the expected number of attempts returned (null for exception) */ public function test_execute(int $enabletracking, int $reviewmode, string $loginuser, ?string $participant, bool $createattempts, ?int $count): void { $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 = $manager->get_coursemodule(); // Prepare users: 1 teacher, 1 student and 1 unenroled user. $users = [ 'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'), 'student' => $this->getDataGenerator()->create_and_enrol($course, 'student'), 'other' => $this->getDataGenerator()->create_and_enrol($course, 'student'), ]; $attempts = []; $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); if ($createattempts) { $user = $users['student']; $params = ['cmid' => $cm->id, 'userid' => $user->id]; $attempts['student'] = $generator->create_content($activity, $params); } // Create another 2 attempts for the user "other" to validate no cross attempts are returned. $user = $users['other']; $params = ['cmid' => $cm->id, 'userid' => $user->id]; $attempts['other'] = $generator->create_content($activity, $params); // Execute external method. $this->setUser($users[$loginuser]); $attemptid = $attempts[$participant]->id ?? 0; $result = get_results::execute($activity->id, [$attemptid]); $result = external_api::clean_returnvalue( get_results::execute_returns(), $result ); // Validate general structure. $this->assertArrayHasKey('activityid', $result); $this->assertArrayHasKey('attempts', $result); $this->assertArrayHasKey('warnings', $result); $this->assertEquals($activity->id, $result['activityid']); if ($count === null) { $this->assertCount(1, $result['warnings']); $this->assertCount(0, $result['attempts']); return; } $this->assertCount(0, $result['warnings']); $this->assertCount(1, $result['attempts']); // Validate attempt. $attempt = $result['attempts'][0]; $this->assertEquals($attemptid, $attempt['id']); // Validate results. $this->assertArrayHasKey('results', $attempt); $this->assertCount($count, $attempt['results']); foreach ($attempt['results'] as $value) { $this->assertEquals($attemptid, $value['attemptid']); $this->assertArrayHasKey('subcontent', $value); $this->assertArrayHasKey('rawscore', $value); $this->assertArrayHasKey('maxscore', $value); $this->assertArrayHasKey('duration', $value); $this->assertArrayHasKey('track', $value); if (isset($value['options'])) { foreach ($value['options'] as $option) { $this->assertArrayHasKey('description', $option); $this->assertArrayHasKey('id', $option); } } } } /** * Data provider for the test_execute tests. * * @return array */ public static function execute_data(): array { return [ 'Teacher reviewing an attempt' => [ 1, manager::REVIEWCOMPLETION, 'editingteacher', 'student', true, 1 ], 'Teacher try to review an inexistent attempt' => [ 1, manager::REVIEWCOMPLETION, 'editingteacher', 'student', false, null ], 'Teacher reviewing attempt with student review mode off' => [ 1, manager::REVIEWNONE, 'editingteacher', 'student', true, 1 ], 'Student reviewing own attempt' => [ 1, manager::REVIEWCOMPLETION, 'student', 'student', true, 1 ], 'Student reviewing an inexistent attempt' => [ 1, manager::REVIEWCOMPLETION, 'student', 'student', false, null ], 'Student reviewing own attempt with review mode off' => [ 1, manager::REVIEWNONE, 'student', 'student', true, null ], 'Student try to stalk other student attempt' => [ 1, manager::REVIEWCOMPLETION, 'student', 'other', false, null ], 'Teacher trying to review an attempt without tracking enabled' => [ 0, manager::REVIEWNONE, 'editingteacher', 'student', true, null ], 'Student trying to review an attempt without tracking enabled' => [ 0, manager::REVIEWNONE, 'editingteacher', 'student', true, null ], 'Student trying to stalk another student attempt without tracking enabled' => [ 0, manager::REVIEWNONE, 'editingteacher', 'student', true, null ], ]; } /** * Test the behaviour of get_results. * * @dataProvider execute_multipleattempts_data * @param string $loginuser the user which calls the webservice * @param array $getattempts the attempts to get the data * @param array $warnings warnigns expected * @param array $reports data expected * */ public function test_execute_multipleattempts(string $loginuser, array $getattempts, array $warnings, array $reports): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $manager = manager::create_from_instance($activity); $cm = $manager->get_coursemodule(); // Prepare users: 1 teacher, 2 student. $users = [ 'editingteacher' => $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'), 'student1' => $this->getDataGenerator()->create_and_enrol($course, 'student'), 'student2' => $this->getDataGenerator()->create_and_enrol($course, 'student'), ]; $attempts = []; // Generate attempts for student 1 and 2. $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); $user = $users['student1']; $params = ['cmid' => $cm->id, 'userid' => $user->id]; $attempts['student1_1'] = $generator->create_content($activity, $params); $attempts['student1_2'] = $generator->create_content($activity, $params); $user = $users['student2']; $params = ['cmid' => $cm->id, 'userid' => $user->id]; $attempts['student2_1'] = $generator->create_content($activity, $params); $attempts['student2_2'] = $generator->create_content($activity, $params); // Execute external method. $this->setUser($users[$loginuser]); $attemptids = []; foreach ($getattempts as $getattempt) { $attemptids[] = $attempts[$getattempt]->id ?? 0; } $result = get_results::execute($activity->id, $attemptids); $result = external_api::clean_returnvalue( get_results::execute_returns(), $result ); // Validate general structure. $this->assertArrayHasKey('activityid', $result); $this->assertArrayHasKey('attempts', $result); $this->assertArrayHasKey('warnings', $result); $this->assertEquals($activity->id, $result['activityid']); $this->assertCount(count($warnings), $result['warnings']); $this->assertCount(count($reports), $result['attempts']); // Validate warnings. $expectedwarnings = []; foreach ($warnings as $warningattempt) { $id = $attempts[$warningattempt]->id ?? 0; $expectedwarnings[$id] = $warningattempt; } foreach ($result['warnings'] as $warning) { $this->assertEquals('h5pactivity_attempts', $warning['item']); $this->assertEquals(1, $warning['warningcode']); $this->assertArrayHasKey($warning['itemid'], $expectedwarnings); } // Validate attempts. $expectedattempts = []; foreach ($reports as $expectedattempt) { $id = $attempts[$expectedattempt]->id; $expectedattempts[$id] = $expectedattempt; } foreach ($result['attempts'] as $value) { $this->assertArrayHasKey($value['id'], $expectedattempts); } } /** * Data provider for the test_execute_multipleattempts tests. * * @return array */ public static function execute_multipleattempts_data(): array { return [ // Teacher cases. 'Teacher reviewing students attempts' => [ 'editingteacher', ['student1_1', 'student2_1'], [], ['student1_1', 'student2_1'] ], 'Teacher reviewing invalid attempt' => [ 'editingteacher', ['student1_1', 'invalid'], ['invalid'], ['student1_1'] ], 'Teacher reviewing empty attempts list' => [ 'editingteacher', [], [], [] ], // Student cases. 'Student reviewing own students attempts' => [ 'student1', ['student1_1', 'student1_2'], [], ['student1_1', 'student1_2'] ], 'Student reviewing invalid attempt' => [ 'student1', ['student1_1', 'invalid'], ['invalid'], ['student1_1'] ], 'Student reviewing trying to access another user attempts' => [ 'student1', ['student1_1', 'student2_1'], ['student2_1'], ['student1_1'] ], 'Student reviewing empty attempts list' => [ 'student1', [], [], ['student1_1', 'student1_2'] ], ]; } /** * Test the behaviour of get_results using mixed activityid. * * @dataProvider execute_mixactivities_data * @param string $activityname the activity name to use * @param string $attemptname the attempt name to use * @param string $expectedwarnings expected warning attempt * @param string $expectedattempt expected result attempt * */ public function test_execute_mixactivities(string $activityname, string $attemptname, string $expectedwarnings, string $expectedattempt): void { $this->resetAfterTest(); $this->setAdminUser(); // Create 2 courses. $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); // Prepare users: 1 teacher, 1 student. $user = $this->getDataGenerator()->create_and_enrol($course1, 'student'); $this->getDataGenerator()->enrol_user($user->id, $course2->id, 'student'); // Create our base activity. $activity11 = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course1]); $manager11 = manager::create_from_instance($activity11); $cm11 = $manager11->get_coursemodule(); // Create a second activity in the same course to check if the retuned attempt is the correct one. $activity12 = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course1]); $manager12 = manager::create_from_instance($activity12); $cm12 = $manager12->get_coursemodule(); // Create a second activity on a different course. $activity21 = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course2]); $manager21 = manager::create_from_instance($activity21); $cm21 = $manager21->get_coursemodule(); $activities = [ '11' => $activity11->id, '12' => $activity12->id, '21' => $activity21->id, 'inexistent' => 0, ]; // Generate attempts. $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); $params = ['cmid' => $cm11->id, 'userid' => $user->id]; $attempt11 = $generator->create_content($activity11, $params); $params = ['cmid' => $cm12->id, 'userid' => $user->id]; $attempt12 = $generator->create_content($activity12, $params); $params = ['cmid' => $cm21->id, 'userid' => $user->id]; $attempt21 = $generator->create_content($activity21, $params); $attempts = [ '11' => $attempt11->id, '12' => $attempt12->id, '21' => $attempt21->id, 'inexistent' => 0, ]; if ($activityname == 'inexistent') { $this->expectException(dml_missing_record_exception::class); } // Execute external method. $this->setUser($user); $attemptid = $attempts[$attemptname]; $result = get_results::execute($activities[$activityname], [$attemptid]); $result = external_api::clean_returnvalue( get_results::execute_returns(), $result ); // Validate general structure. $this->assertArrayHasKey('activityid', $result); $this->assertArrayHasKey('attempts', $result); $this->assertArrayHasKey('warnings', $result); if (empty($expectedwarnings)) { $this->assertEmpty($result['warnings']); } else { $this->assertEquals('h5pactivity_attempts', $result['warnings'][0]['item']); $this->assertEquals(1, $result['warnings'][0]['warningcode']); $this->assertEquals($attempts[$expectedwarnings], $result['warnings'][0]['itemid']); } if (empty($expectedattempt)) { $this->assertEmpty($result['attempts']); } else { $this->assertEquals($attempts[$expectedattempt], $result['attempts'][0]['id']); } } /** * Data provider for the test_execute_multipleattempts tests. * * @return array */ public static function execute_mixactivities_data(): array { return [ // Teacher cases. 'Correct activity id' => [ '11', '11', '', '11' ], 'Wrong activity id' => [ '21', '11', '11', '' ], 'Inexistent activity id' => [ 'inexistent', '11', '', '' ], 'Inexistent attempt id' => [ '11', 'inexistent', 'inexistent', '' ], ]; } } PK1B\!Σ|:|:tests/privacy/provider_test.phpnu[. /** * mod_h5pactivity privacy tests * * @package mod_h5pactivity * @category test * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\privacy; use \core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\approved_userlist; use \core_privacy\local\request\writer; use \core_privacy\tests\provider_testcase; use core_xapi\local\statement\item_activity; use core_xapi\test_helper; use stdClass; /** * Privacy tests class for mod_h5pactivity. * * @package mod_h5pactivity * @category test * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \mod_h5pactivity\privacy\provider */ final class provider_test extends provider_testcase { /** @var stdClass User without any attempt. */ protected $student0; /** @var stdClass User with some attempt. */ protected $student1; /** @var stdClass User with some attempt. */ protected $student2; /** @var stdClass User with some attempt. */ protected $student3; /** @var \context context_module of the H5P activity. */ protected $context; /** * Test getting the context for the user ID related to this plugin. */ public function test_get_contexts_for_userid(): void { $this->resetAfterTest(true); $this->setAdminUser(); $this->h5pactivity_setup_test_scenario_data(); // The student0 hasn't any attempt. $contextlist = provider::get_contexts_for_userid($this->student0->id); $this->assertCount(0, (array) $contextlist->get_contextids()); // The student1 has data in the mod_h5pactivity context. $contextlist = provider::get_contexts_for_userid($this->student1->id); $this->assertCount(1, (array) $contextlist->get_contextids()); $this->assertContainsEquals($this->context->id, $contextlist->get_contextids()); } /** * Test getting the user IDs for the context related to this plugin. */ public function test_get_users_in_context(): void { $this->resetAfterTest(true); $this->setAdminUser(); $this->h5pactivity_setup_test_scenario_data(); $component = 'mod_h5pactivity'; $userlist = new \core_privacy\local\request\userlist($this->context, $component); provider::get_users_in_context($userlist); // Students 1 and 2 have attempts in the H5P context, student 0 does not. $this->assertCount(2, $userlist); $expected = [$this->student1->id, $this->student2->id]; $actual = $userlist->get_userids(); sort($expected); sort($actual); $this->assertEquals($expected, $actual); } /** * Test that data is exported correctly for this plugin. */ public function test_export_user_data(): void { $this->resetAfterTest(true); $this->setAdminUser(); $this->h5pactivity_setup_test_scenario_data(); $component = 'mod_h5pactivity'; // Validate exported data for student0 (without any attempt). $this->setUser($this->student0); $writer = writer::with_context($this->context); $this->export_context_data_for_user($this->student0->id, $this->context, $component); $subcontextattempt1 = [ get_string('myattempts', 'mod_h5pactivity'), get_string('attempt', 'mod_h5pactivity'). " 1" ]; $data = $writer->get_data($subcontextattempt1); $this->assertEmpty($data); // Validate exported data for student1. writer::reset(); $this->setUser($this->student1); $writer = writer::with_context($this->context); $this->assertFalse($writer->has_any_data()); $this->export_context_data_for_user($this->student1->id, $this->context, $component); $data = $writer->get_data([]); $this->assertEquals('H5P 1', $data->name); $data = (array)$writer->get_data($subcontextattempt1); $this->assertCount(1, $data); $this->assertCount(3, (array) reset($data)); $subcontextattempt2 = [ get_string('myattempts', 'mod_h5pactivity'), get_string('attempt', 'mod_h5pactivity'). " 2" ]; $data = (array)$writer->get_data($subcontextattempt2); $this->assertCount(3, (array) reset($data)); // The student1 has only 1 tracked attempts. $subcontextattempt3 = [ get_string('myattempts', 'mod_h5pactivity'), get_string('attempt', 'mod_h5pactivity'). " 3" ]; $data = (array)$writer->get_data($subcontextattempt3); $this->assertEmpty($data); } /** * Test for provider::delete_data_for_all_users_in_context(). */ public function test_delete_data_for_all_users_in_context(): void { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $this->h5pactivity_setup_test_scenario_data(true); // Check data before deletion. $this->assertEquals(6, $DB->count_records('h5pactivity_attempts')); $this->assertEquals(18, $DB->count_records('h5pactivity_attempts_results')); $this->assertEquals(2, $DB->count_records('xapi_states')); // Delete data based on the context. provider::delete_data_for_all_users_in_context($this->context); // After deletion, the attempts entries should have been deleted. $this->assertEquals(0, $DB->count_records('h5pactivity_attempts')); // After deletion, the results entries should have been deleted. $this->assertEquals(0, $DB->count_records('h5pactivity_attempts_results')); // After deletion, the xapi states should have been deleted. $this->assertEquals(0, $DB->count_records('xapi_states')); } /** * Test for provider::delete_data_for_user(). */ public function test_delete_data_for_user(): void { global $DB; $this->resetAfterTest(true); $this->setAdminUser(); $this->h5pactivity_setup_test_scenario_data(true); $params = ['userid' => $this->student1->id]; // Check data before deletion. $this->assertEquals(6, $DB->count_records('h5pactivity_attempts')); $this->assertEquals(18, $DB->count_records('h5pactivity_attempts_results')); $this->assertEquals(2, $DB->count_records('xapi_states')); // Save student1 attempts ids. $attemptsids = $DB->get_records_menu('h5pactivity_attempts', $params, '', 'attempt, id'); list($resultselect, $attemptids) = $DB->get_in_or_equal($attemptsids); $resultselect = 'id ' . $resultselect; $approvedcontextlist = new approved_contextlist($this->student1, 'h5pactivity', [$this->context->id]); provider::delete_data_for_user($approvedcontextlist); // After deletion, the h5pactivity_attempts entries for the first student should have been deleted. $this->assertEquals(0, $DB->count_records('h5pactivity_attempts', $params)); $this->assertEquals(4, $DB->count_records('h5pactivity_attempts')); // After deletion, the results entries for the first student should have been deleted. $count = $DB->count_records_select('h5pactivity_attempts_results', $resultselect, $attemptids); $this->assertEquals(0, $count); $this->assertEquals(12, $DB->count_records('h5pactivity_attempts_results')); // After deletion, the results entries for the first student should have been deleted. $this->assertEquals(0, $DB->count_records('xapi_states', $params)); $this->assertEquals(1, $DB->count_records('xapi_states')); // Confirm that the h5pactivity hasn't been removed. $h5pactivitycount = $DB->get_records('h5pactivity'); $this->assertCount(1, (array) $h5pactivitycount); // Delete track for student0 (nothing has to be removed). $approvedcontextlist = new approved_contextlist($this->student0, 'h5pactivity', [$this->context->id]); provider::delete_data_for_user($approvedcontextlist); $this->assertEquals(4, $DB->count_records('h5pactivity_attempts')); $this->assertEquals(12, $DB->count_records('h5pactivity_attempts_results')); $this->assertEquals(1, $DB->count_records('xapi_states')); } /** * Test for provider::delete_data_for_users(). */ public function test_delete_data_for_users(): void { global $DB; $component = 'mod_h5pactivity'; $this->resetAfterTest(true); $this->setAdminUser(); // In this scenario we need a 3rd user to test batch deletion. // Create student2 with 2 attempts. $this->h5pactivity_setup_test_scenario_data(true); // Check data before deletion. $this->assertEquals(6, $DB->count_records('h5pactivity_attempts')); $this->assertEquals(18, $DB->count_records('h5pactivity_attempts_results')); $this->assertEquals(2, $DB->count_records('xapi_states')); // Save student1 and student2 attempts ids. $params1 = ['userid' => $this->student1->id]; $attempts1ids = $DB->get_records_menu('h5pactivity_attempts', $params1, '', 'attempt, id'); $params2 = ['userid' => $this->student2->id]; $attempts2ids = $DB->get_records_menu('h5pactivity_attempts', $params2, '', 'attempt, id'); list($resultselect, $attemptids) = $DB->get_in_or_equal(array_merge($attempts1ids, $attempts2ids)); $resultselect = 'id ' . $resultselect; // Delete student 1 ans 2 data, retain student 3 data. $approveduserids = [$this->student1->id, $this->student2->id]; $approvedlist = new approved_userlist($this->context, $component, $approveduserids); provider::delete_data_for_users($approvedlist); // After deletion, the h5pactivity_attempts entries for student1 and student2 should have been deleted. $this->assertEquals(0, $DB->count_records('h5pactivity_attempts', $params1)); $this->assertEquals(0, $DB->count_records('h5pactivity_attempts', $params2)); $this->assertEquals(0, $DB->count_records('xapi_states', $params1)); $this->assertEquals(0, $DB->count_records('xapi_states', $params2)); $this->assertEquals(2, $DB->count_records('h5pactivity_attempts')); // After deletion, the results entries for the first and second student should have been deleted. $count = $DB->count_records_select('h5pactivity_attempts_results', $resultselect, $attemptids); $this->assertEquals(0, $count); $this->assertEquals(6, $DB->count_records('h5pactivity_attempts_results')); $this->assertEquals(1, $DB->count_records('xapi_states')); // Confirm that the h5pactivity hasn't been removed. $h5pactivitycount = $DB->get_records('h5pactivity'); $this->assertCount(1, (array) $h5pactivitycount); // Delete results track for student0 (nothing has to be removed). $approveduserids = [$this->student0->id]; $approvedlist = new approved_userlist($this->context, $component, $approveduserids); provider::delete_data_for_users($approvedlist); $this->assertEquals(2, $DB->count_records('h5pactivity_attempts')); $this->assertEquals(6, $DB->count_records('h5pactivity_attempts_results')); $this->assertEquals(1, $DB->count_records('xapi_states')); } /** * Helper function to setup 3 users and 2 H5P attempts for student1 and student2. * $this->student0 is always created without any attempt. * * @param bool $extrauser generate a 3rd user (default false). */ protected function h5pactivity_setup_test_scenario_data(bool $extrauser = false): void { global $CFG, $USER; require_once($CFG->dirroot.'/lib/xapi/tests/helper.php'); $generator = $this->getDataGenerator(); $course = $this->getDataGenerator()->create_course(); $params = ['course' => $course]; $activity = $this->getDataGenerator()->create_module('h5pactivity', $params); $cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST); $this->context = \context_module::instance($activity->cmid); /** @var \mod_h5pactivity_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); // Create student0 without any attempt. $this->student0 = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create student1 with 2 attempts and 1 xapi state. $this->student1 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $params = ['cmid' => $cm->id, 'userid' => $this->student1->id]; $generator->create_content($activity, $params); $generator->create_content($activity, $params); $currentuser = $USER; $this->setUser($this->student1); test_helper::create_state([ 'activity' => item_activity::create_from_id($this->context->id), 'component' => 'mod_h5pactivity', ], true); $this->setUser($currentuser); // Create student2 with 2 attempts. $this->student2 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $params = ['cmid' => $cm->id, 'userid' => $this->student2->id]; $generator->create_content($activity, $params); $generator->create_content($activity, $params); if ($extrauser) { $this->student3 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $params = ['cmid' => $cm->id, 'userid' => $this->student3->id]; $generator->create_content($activity, $params); $generator->create_content($activity, $params); // Add 1 xapi state. $currentuser = $USER; $this->setUser($this->student3); test_helper::create_state([ 'activity' => item_activity::create_from_id($this->context->id), 'component' => 'mod_h5pactivity', ], true); $this->setUser($currentuser); } } } PK1B\7D.44tests/local/grader_test.phpnu[. /** * mod_h5pactivity grader tests * * @package mod_h5pactivity * @category test * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\local; use grade_item; use stdClass; /** * Grader tests class for mod_h5pactivity. * * @package mod_h5pactivity * @category test * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class grader_test extends \advanced_testcase { /** * Setup to ensure that fixtures are loaded. */ public static function setupBeforeClass(): void { global $CFG; require_once($CFG->libdir.'/gradelib.php'); } /** * Test for grade item delete. */ public function test_grade_item_delete(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); $grader = new grader($activity); // Force a user grade. $this->generate_fake_attempt($activity, $user, 5, 10); $grader->update_grades($user->id); $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id); $this->assertNotEquals(0, count($gradeinfo->items)); $this->assertArrayHasKey($user->id, $gradeinfo->items[0]->grades); $grader->grade_item_delete(); $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id); $this->assertEquals(0, count($gradeinfo->items)); } /** * Test for grade item update. * * @dataProvider grade_item_update_data * @param int $newgrade new activity grade * @param bool $reset if has to reset grades * @param string $idnumber the new idnumber */ public function test_grade_item_update(int $newgrade, bool $reset, string $idnumber): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Force a user initial grade. $grader = new grader($activity); $this->generate_fake_attempt($activity, $user, 5, 10); $grader->update_grades($user->id); $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id); $this->assertNotEquals(0, count($gradeinfo->items)); $item = array_shift($gradeinfo->items); $this->assertArrayHasKey($user->id, $item->grades); $this->assertEquals(50, round($item->grades[$user->id]->grade)); // Module grade value determine the way gradebook acts. That means that the expected // result depends on this value. // - Grade > 0: regular max grade value. // - Grade = 0: no grading is used (but grademax remains the same). // - Grade < 0: a scaleid is used (value = -scaleid). if ($newgrade > 0) { $grademax = $newgrade; $scaleid = null; $usergrade = ($newgrade > 50) ? 50 : $newgrade; } else if ($newgrade == 0) { $grademax = 100; $scaleid = null; $usergrade = null; // No user grades expected. } else if ($newgrade < 0) { $scale = $this->getDataGenerator()->create_scale(array("scale" => "value1, value2, value3")); $newgrade = -1 * $scale->id; $grademax = 3; $scaleid = $scale->id; $usergrade = 3; // 50 value will ve converted to "value 3" on scale. } // Update grade item. $activity->grade = $newgrade; // In case a reset is need, usergrade will be empty. if ($reset) { $param = 'reset'; $usergrade = null; } else { // Individual user gradings will be tested as a subcall of update_grades. $param = null; } $grader = new grader($activity, $idnumber); $grader->grade_item_update($param); // Check new grade item and grades. grade_regrade_final_grades($course->id); $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, $user->id); $item = array_shift($gradeinfo->items); $this->assertEquals($scaleid, $item->scaleid); $this->assertEquals($grademax, $item->grademax); $this->assertArrayHasKey($user->id, $item->grades); if ($usergrade) { $this->assertEquals($usergrade, round($item->grades[$user->id]->grade)); } else { $this->assertEmpty($item->grades[$user->id]->grade); } if (!empty($idnumber)) { $gradeitem = grade_item::fetch(['idnumber' => $idnumber, 'courseid' => $course->id]); $this->assertInstanceOf('grade_item', $gradeitem); } } /** * Data provider for test_grade_item_update. * * @return array */ public static function grade_item_update_data(): array { return [ 'Change idnumber' => [ 100, false, 'newidnumber' ], 'Increase max grade to 110' => [ 110, false, '' ], 'Decrease max grade to 80' => [ 40, false, '' ], 'Decrease max grade to 40 (less than actual grades)' => [ 40, false, '' ], 'Reset grades' => [ 100, true, '' ], 'Disable grades' => [ 0, false, '' ], 'Use scales' => [ -1, false, '' ], 'Use scales with reset' => [ -1, true, '' ], ]; } /** * Test for grade update. * * @dataProvider update_grades_data * @param int $newgrade the new activity grade * @param bool $all if has to be applied to all students or just to one * @param int $completion 1 all student have the activity completed, 0 one have incompleted * @param array $results expected results (user1 grade, user2 grade) */ public function test_update_grades(int $newgrade, bool $all, int $completion, array $results): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $user1 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $user2 = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Force a user initial grade. $grader = new grader($activity); $this->generate_fake_attempt($activity, $user1, 5, 10); $this->generate_fake_attempt($activity, $user2, 3, 12, $completion); $grader->update_grades(); $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, [$user1->id, $user2->id]); $this->assertNotEquals(0, count($gradeinfo->items)); $item = array_shift($gradeinfo->items); $this->assertArrayHasKey($user1->id, $item->grades); $this->assertArrayHasKey($user2->id, $item->grades); $this->assertEquals(50, $item->grades[$user1->id]->grade); // Uncompleted attempts does not generate grades. if ($completion) { $this->assertEquals(25, $item->grades[$user2->id]->grade); } else { $this->assertNull($item->grades[$user2->id]->grade); } // Module grade value determine the way gradebook acts. That means that the expected // result depends on this value. // - Grade > 0: regular max grade value. // - Grade <= 0: no grade calculation is used (scale and no grading). if ($newgrade < 0) { $scale = $this->getDataGenerator()->create_scale(array("scale" => "value1, value2, value3")); $activity->grade = -1 * $scale->id; } else { $activity->grade = $newgrade; } $userid = ($all) ? 0 : $user1->id; $grader = new grader($activity); $grader->update_grades($userid); // Check new grade item and grades. grade_regrade_final_grades($course->id); $gradeinfo = grade_get_grades($course->id, 'mod', 'h5pactivity', $activity->id, [$user1->id, $user2->id]); $item = array_shift($gradeinfo->items); $this->assertArrayHasKey($user1->id, $item->grades); $this->assertArrayHasKey($user2->id, $item->grades); $this->assertEquals($results[0], $item->grades[$user1->id]->grade); $this->assertEquals($results[1], $item->grades[$user2->id]->grade); } /** * Data provider for test_grade_item_update. * * @return array */ public static function update_grades_data(): array { return [ // Quantitative grade, all attempts completed. 'Same grademax, all users, all completed' => [ 100, true, 1, [50, 25] ], 'Same grademax, one user, all completed' => [ 100, false, 1, [50, 25] ], 'Increade max, all users, all completed' => [ 200, true, 1, [100, 50] ], 'Increade max, one user, all completed' => [ 200, false, 1, [100, 25] ], 'Decrease max, all users, all completed' => [ 50, true, 1, [25, 12.5] ], 'Decrease max, one user, all completed' => [ 50, false, 1, [25, 25] ], // Quantitative grade, some attempts not completed. 'Same grademax, all users, not completed' => [ 100, true, 0, [50, null] ], 'Same grademax, one user, not completed' => [ 100, false, 0, [50, null] ], 'Increade max, all users, not completed' => [ 200, true, 0, [100, null] ], 'Increade max, one user, not completed' => [ 200, false, 0, [100, null] ], 'Decrease max, all users, not completed' => [ 50, true, 0, [25, null] ], 'Decrease max, one user, not completed' => [ 50, false, 0, [25, null] ], // No grade (no grade will be used). 'No grade, all users, all completed' => [ 0, true, 1, [null, null] ], 'No grade, one user, all completed' => [ 0, false, 1, [null, null] ], 'No grade, all users, not completed' => [ 0, true, 0, [null, null] ], 'No grade, one user, not completed' => [ 0, false, 0, [null, null] ], // Scale (grate item will updated but without regrading). 'Scale, all users, all completed' => [ -1, true, 1, [3, 3] ], 'Scale, one user, all completed' => [ -1, false, 1, [3, 3] ], 'Scale, all users, not completed' => [ -1, true, 0, [3, null] ], 'Scale, one user, not completed' => [ -1, false, 0, [3, null] ], ]; } /** * Create a fake attempt for a specific user. * * @param stdClass $activity activity instance record. * @param stdClass $user user record * @param int $rawscore score obtained * @param int $maxscore attempt max score * @param int $completion 1 for activity completed, 0 for not completed yet * @return stdClass the attempt record */ private function generate_fake_attempt(stdClass $activity, stdClass $user, int $rawscore, int $maxscore, int $completion = 1): stdClass { global $DB; $attempt = (object)[ 'h5pactivityid' => $activity->id, 'userid' => $user->id, 'timecreated' => 10, 'timemodified' => 20, 'attempt' => 1, 'rawscore' => $rawscore, 'maxscore' => $maxscore, 'duration' => 2, 'completion' => $completion, 'success' => 0, ]; $attempt->scaled = $attempt->rawscore / $attempt->maxscore; $attempt->id = $DB->insert_record('h5pactivity_attempts', $attempt); return $attempt; } } PK1B\G I Itests/local/attempt_test.phpnu[. /** * mod_h5pactivity attempt tests * * @package mod_h5pactivity * @category test * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity\local; use \core_xapi\local\statement; use \core_xapi\local\statement\item; use \core_xapi\local\statement\item_agent; use \core_xapi\local\statement\item_activity; use \core_xapi\local\statement\item_definition; use \core_xapi\local\statement\item_verb; use \core_xapi\local\statement\item_result; use core_xapi\test_helper; use stdClass; /** * Attempt tests class for mod_h5pactivity. * * @package mod_h5pactivity * @category test * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class attempt_test extends \advanced_testcase { /** * Generate a scenario to run all tests. * @return array course_modules, user record, course record */ private function generate_testing_scenario(): array { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); return [$cm, $student, $course]; } /** * Test for create_attempt method. */ public function test_create_attempt(): void { global $CFG, $DB; require_once($CFG->dirroot.'/lib/xapi/tests/helper.php'); [$cm, $student, $course] = $this->generate_testing_scenario(); $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Save the current state for this activity for student1 and student2 (before creating the first attempt). $manager = manager::create_from_coursemodule($cm); $this->setUser($student2); test_helper::create_state([ 'activity' => item_activity::create_from_id($manager->get_context()->id), 'component' => 'mod_h5pactivity', ], true); $this->setUser($student); test_helper::create_state([ 'activity' => item_activity::create_from_id($manager->get_context()->id), 'component' => 'mod_h5pactivity', ], true); $this->assertEquals(2, $DB->count_records('xapi_states')); // Create first attempt. $attempt = attempt::new_attempt($student, $cm); $this->assertEquals($student->id, $attempt->get_userid()); $this->assertEquals($cm->instance, $attempt->get_h5pactivityid()); $this->assertEquals(1, $attempt->get_attempt()); $this->assertEquals(1, $DB->count_records('xapi_states')); $this->assertEquals(0, $DB->count_records('xapi_states', ['userid' => $student->id])); // Create a second attempt. $attempt = attempt::new_attempt($student, $cm); $this->assertEquals($student->id, $attempt->get_userid()); $this->assertEquals($cm->instance, $attempt->get_h5pactivityid()); $this->assertEquals(2, $attempt->get_attempt()); } /** * Test for last_attempt method */ public function test_last_attempt(): void { list($cm, $student) = $this->generate_testing_scenario(); // Create first attempt. $attempt = attempt::last_attempt($student, $cm); $this->assertEquals($student->id, $attempt->get_userid()); $this->assertEquals($cm->instance, $attempt->get_h5pactivityid()); $this->assertEquals(1, $attempt->get_attempt()); $lastid = $attempt->get_id(); // Get last attempt. $attempt = attempt::last_attempt($student, $cm); $this->assertEquals($student->id, $attempt->get_userid()); $this->assertEquals($cm->instance, $attempt->get_h5pactivityid()); $this->assertEquals(1, $attempt->get_attempt()); $this->assertEquals($lastid, $attempt->get_id()); // Now force a new attempt. $attempt = attempt::new_attempt($student, $cm); $this->assertEquals($student->id, $attempt->get_userid()); $this->assertEquals($cm->instance, $attempt->get_h5pactivityid()); $this->assertEquals(2, $attempt->get_attempt()); $lastid = $attempt->get_id(); // Get last attempt. $attempt = attempt::last_attempt($student, $cm); $this->assertEquals($student->id, $attempt->get_userid()); $this->assertEquals($cm->instance, $attempt->get_h5pactivityid()); $this->assertEquals(2, $attempt->get_attempt()); $this->assertEquals($lastid, $attempt->get_id()); } /** * Test saving statements. * * @dataProvider save_statement_data * @param string $subcontent subcontent identifier * @param bool $hasdefinition generate definition * @param bool $hasresult generate result * @param array $results 0 => insert ok, 1 => maxscore, 2 => rawscore, 3 => count */ public function test_save_statement(string $subcontent, bool $hasdefinition, bool $hasresult, array $results): void { list($cm, $student) = $this->generate_testing_scenario(); $attempt = attempt::new_attempt($student, $cm); $this->assertEquals(0, $attempt->get_maxscore()); $this->assertEquals(0, $attempt->get_rawscore()); $this->assertEquals(0, $attempt->count_results()); $this->assertEquals(0, $attempt->get_duration()); $this->assertNull($attempt->get_completion()); $this->assertNull($attempt->get_success()); $this->assertFalse($attempt->get_scoreupdated()); $statement = $this->generate_statement($hasdefinition, $hasresult); $result = $attempt->save_statement($statement, $subcontent); $this->assertEquals($results[0], $result); $this->assertEquals($results[1], $attempt->get_maxscore()); $this->assertEquals($results[2], $attempt->get_rawscore()); $this->assertEquals($results[3], $attempt->count_results()); $this->assertEquals($results[4], $attempt->get_duration()); $this->assertEquals($results[5], $attempt->get_completion()); $this->assertEquals($results[6], $attempt->get_success()); if ($results[5]) { $this->assertTrue($attempt->get_scoreupdated()); } else { $this->assertFalse($attempt->get_scoreupdated()); } } /** * Data provider for data request creation tests. * * @return array */ public static function save_statement_data(): array { return [ 'Statement without definition and result' => [ '', false, false, [false, 0, 0, 0, 0, null, null] ], 'Statement with definition but no result' => [ '', true, false, [false, 0, 0, 0, 0, null, null] ], 'Statement with result but no definition' => [ '', true, false, [false, 0, 0, 0, 0, null, null] ], 'Statement subcontent without definition and result' => [ '111-222-333', false, false, [false, 0, 0, 0, 0, null, null] ], 'Statement subcontent with definition but no result' => [ '111-222-333', true, false, [false, 0, 0, 0, 0, null, null] ], 'Statement subcontent with result but no definition' => [ '111-222-333', true, false, [false, 0, 0, 0, 0, null, null] ], 'Statement with definition, result but no subcontent' => [ '', true, true, [true, 2, 2, 1, 25, 1, 1] ], 'Statement with definition, result and subcontent' => [ '111-222-333', true, true, [true, 0, 0, 1, 0, null, null] ], ]; } /** * Test delete results from attempt. */ public function test_delete_results(): void { list($cm, $student) = $this->generate_testing_scenario(); $attempt = $this->generate_full_attempt($student, $cm); $attempt->delete_results(); $this->assertEquals(0, $attempt->count_results()); } /** * Test delete attempt. */ public function test_delete_attempt(): void { global $DB; list($cm, $student) = $this->generate_testing_scenario(); // Check no previous attempts are created. $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(0, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(0, $count); // Generate one attempt. $attempt1 = $this->generate_full_attempt($student, $cm); $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(1, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(2, $count); // Generate a second attempt. $attempt2 = $this->generate_full_attempt($student, $cm); $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(2, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(4, $count); // Delete the first attempt. attempt::delete_attempt($attempt1); $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(1, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(2, $count); $this->assertEquals(2, $attempt2->count_results()); } /** * Test delete all attempts. * * @dataProvider delete_all_attempts_data * @param bool $hasstudent if user is specificed * @param int[] 0-3 => statements count results, 4-5 => totals */ public function test_delete_all_attempts(bool $hasstudent, array $results): void { global $DB; list($cm, $student, $course) = $this->generate_testing_scenario(); // For this test we need extra activity and student. $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $cm2 = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST); $student2 = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Check no previous attempts are created. $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(0, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(0, $count); // Generate some attempts attempt on both activities and students. $attempts = []; $attempts[] = $this->generate_full_attempt($student, $cm); $attempts[] = $this->generate_full_attempt($student2, $cm); $attempts[] = $this->generate_full_attempt($student, $cm2); $attempts[] = $this->generate_full_attempt($student2, $cm2); $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(4, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(8, $count); // Delete all specified attempts. $user = ($hasstudent) ? $student : null; attempt::delete_all_attempts($cm, $user); // Check data. for ($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); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals($results[5], $count); } /** * Data provider for data request creation tests. * * @return array */ public static function delete_all_attempts_data(): array { return [ 'Delete all attempts from activity' => [ false, [0, 0, 2, 2, 2, 4] ], 'Delete all attempts from user' => [ true, [0, 2, 2, 2, 3, 6] ], ]; } /** * Test set_score method. * */ public function test_set_score(): void { global $DB; list($cm, $student, $course) = $this->generate_testing_scenario(); // Generate one attempt. $attempt = $this->generate_full_attempt($student, $cm); $dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]); $this->assertEquals($dbattempt->rawscore, $attempt->get_rawscore()); $this->assertEquals(2, $dbattempt->rawscore); $this->assertEquals($dbattempt->maxscore, $attempt->get_maxscore()); $this->assertEquals(2, $dbattempt->maxscore); $this->assertEquals(1, $dbattempt->scaled); // Set attempt score. $attempt->set_score(5, 10); $this->assertEquals(5, $attempt->get_rawscore()); $this->assertEquals(10, $attempt->get_maxscore()); $this->assertTrue($attempt->get_scoreupdated()); // Save new score into DB. $attempt->save(); $dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]); $this->assertEquals($dbattempt->rawscore, $attempt->get_rawscore()); $this->assertEquals(5, $dbattempt->rawscore); $this->assertEquals($dbattempt->maxscore, $attempt->get_maxscore()); $this->assertEquals(10, $dbattempt->maxscore); $this->assertEquals(0.5, $dbattempt->scaled); } /** * Test set_duration method. * * @dataProvider basic_setters_data * @param string $attribute the stribute to test * @param int $oldvalue attribute old value * @param int $newvalue attribute new expected value */ public function test_basic_setters(string $attribute, int $oldvalue, int $newvalue): void { global $DB; list($cm, $student, $course) = $this->generate_testing_scenario(); // Generate one attempt. $attempt = $this->generate_full_attempt($student, $cm); $setmethod = 'set_'.$attribute; $getmethod = 'get_'.$attribute; $dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]); $this->assertEquals($dbattempt->$attribute, $attempt->$getmethod()); $this->assertEquals($oldvalue, $dbattempt->$attribute); // Set attempt attribute. $attempt->$setmethod($newvalue); $this->assertEquals($newvalue, $attempt->$getmethod()); // Save new score into DB. $attempt->save(); $dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]); $this->assertEquals($dbattempt->$attribute, $attempt->$getmethod()); $this->assertEquals($newvalue, $dbattempt->$attribute); // Set null $attribute. $attempt->$setmethod(null); $this->assertNull($attempt->$getmethod()); // Save new score into DB. $attempt->save(); $dbattempt = $DB->get_record('h5pactivity_attempts', ['id' => $attempt->get_id()]); $this->assertEquals($dbattempt->$attribute, $attempt->$getmethod()); $this->assertNull($dbattempt->$attribute); } /** * Data provider for testing basic setters. * * @return array */ public static function basic_setters_data(): array { return [ 'Set attempt duration' => [ 'duration', 25, 35 ], 'Set attempt completion' => [ 'completion', 1, 0 ], 'Set attempt success' => [ 'success', 1, 0 ], ]; } /** * Generate a fake attempt with two results. * * @param stdClass $student a user record * @param stdClass $cm a course_module record * @return attempt */ private function generate_full_attempt($student, $cm): attempt { $attempt = attempt::new_attempt($student, $cm); $this->assertEquals(0, $attempt->get_maxscore()); $this->assertEquals(0, $attempt->get_rawscore()); $this->assertEquals(0, $attempt->count_results()); $statement = $this->generate_statement(true, true); $saveok = $attempt->save_statement($statement, ''); $this->assertTrue($saveok); $saveok = $attempt->save_statement($statement, '111-222-333'); $this->assertTrue($saveok); $this->assertEquals(2, $attempt->count_results()); return $attempt; } /** * Return a xAPI partial statement with object defined. * @param bool $hasdefinition if has to include definition * @param bool $hasresult if has to include results * @return statement */ private function generate_statement(bool $hasdefinition, bool $hasresult): statement { global $USER; $statement = new statement(); $statement->set_actor(item_agent::create_from_user($USER)); $statement->set_verb(item_verb::create_from_id('http://adlnet.gov/expapi/verbs/completed')); $definition = null; if ($hasdefinition) { $definition = item_definition::create_from_data((object)[ 'interactionType' => 'compound', 'correctResponsesPattern' => '1', ]); } $statement->set_object(item_activity::create_from_id('something', $definition)); if ($hasresult) { $statement->set_result(item_result::create_from_data((object)[ 'completion' => true, 'success' => true, 'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1], 'duration' => 'PT25S', ])); } return $statement; } } PK1B\G tests/local/manager_test.phpnu[. namespace mod_h5pactivity\local; use context_module; use stdClass; /** * Manager tests class for mod_h5pactivity. * * @package mod_h5pactivity * @covers \mod_h5pactivity\local\manager * @category test * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class manager_test extends \advanced_testcase { /** * Test for static create methods. */ public function test_create(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); $manager = manager::create_from_instance($activity); $manageractivity = $manager->get_instance(); $this->assertEquals($activity->id, $manageractivity->id); $managercm = $manager->get_coursemodule(); $this->assertEquals($cm->id, $managercm->id); $managercontext = $manager->get_context(); $this->assertEquals($context->id, $managercontext->id); $manager = manager::create_from_coursemodule($cm); $manageractivity = $manager->get_instance(); $this->assertEquals($activity->id, $manageractivity->id); $managercm = $manager->get_coursemodule(); $this->assertEquals($cm->id, $managercm->id); $managercontext = $manager->get_context(); $this->assertEquals($context->id, $managercontext->id); } /** * Test for is_tracking_enabled and can_submit methods. * * @covers ::is_tracking_enabled * @covers ::can_submit * @dataProvider is_tracking_enabled_data * @param bool $login if the user is logged in * @param string $role user role in course * @param int $enabletracking if tracking is enabled * @param bool $expectedtracking expected result for is_tracking_enabled() * @param bool $expectedsubmit expected result for can_submit() */ public function test_is_tracking_enabled_and_can_submit(bool $login, string $role, int $enabletracking, bool $expectedtracking, bool $expectedsubmit): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course, 'enabletracking' => $enabletracking]); $user = $this->getDataGenerator()->create_and_enrol($course, $role); if ($login) { $this->setUser($user); $param = null; } else { $param = $user; } $manager = manager::create_from_instance($activity); $this->assertEquals($expectedtracking, $manager->is_tracking_enabled()); $this->assertEquals($expectedsubmit, $manager->can_submit($param)); } /** * Data provider for test_is_tracking_enabled_and_can_submit. * * @return array */ public static function is_tracking_enabled_data(): array { return [ 'Logged student, tracking enabled' => [ true, 'student', 1, true, true, ], 'Logged student, tracking disabled' => [ true, 'student', 0, false, true, ], 'Logged teacher, tracking enabled' => [ true, 'editingteacher', 1, true, false, ], 'Logged teacher, tracking disabled' => [ true, 'editingteacher', 0, false, false, ], 'No logged student, tracking enabled' => [ true, 'student', 1, true, true, ], 'No logged student, tracking disabled' => [ true, 'student', 0, false, true, ], 'No logged teacher, tracking enabled' => [ true, 'editingteacher', 1, true, false, ], 'No logged teacher, tracking disabled' => [ true, 'editingteacher', 0, false, false, ], ]; } /** * Test for get_users_scaled_score. * * @dataProvider get_users_scaled_score_data * @param int $enabletracking if tracking is enabled * @param int $gradingmethod new grading method * @param array $result1 student 1 results (scaled, timemodified, attempt number) * @param array $result2 student 2 results (scaled, timemodified, attempt number) */ public function test_get_users_scaled_score(int $enabletracking, int $gradingmethod, array $result1, array $result2): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course, 'enabletracking' => $enabletracking, 'grademethod' => $gradingmethod]); // Generate two users with 4 attempts each. $user1 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->generate_fake_attempts($activity, $user1, 1); $user2 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->generate_fake_attempts($activity, $user2, 2); $manager = manager::create_from_instance($activity); // Get all users scaled scores. $scaleds = $manager->get_users_scaled_score(); // No results will be returned if tracking is dsabled or manual grading method is defined. if (empty($result1)) { $this->assertNull($scaleds); return; } $this->assertCount(2, $scaleds); // Check expected user1 scaled score. $scaled = $scaleds[$user1->id]; $this->assertEquals($user1->id, $scaled->userid); $this->assertEquals($result1[0], $scaled->scaled); $this->assertEquals($result1[1], $scaled->timemodified); if ($result1[2]) { $attempt = $DB->get_record('h5pactivity_attempts', ['id' => $scaled->attemptid]); $this->assertEquals($attempt->h5pactivityid, $activity->id); $this->assertEquals($attempt->userid, $scaled->userid); $this->assertEquals($attempt->scaled, round($scaled->scaled, 5)); $this->assertEquals($attempt->timemodified, $scaled->timemodified); $this->assertEquals($result1[2], $attempt->attempt); } else { $this->assertEquals(0, $scaled->attemptid); } // Check expected user2 scaled score. $scaled = $scaleds[$user2->id]; $this->assertEquals($user2->id, $scaled->userid); $this->assertEquals($result2[0], round($scaled->scaled, 5)); $this->assertEquals($result2[1], $scaled->timemodified); if ($result2[2]) { $attempt = $DB->get_record('h5pactivity_attempts', ['id' => $scaled->attemptid]); $this->assertEquals($attempt->h5pactivityid, $activity->id); $this->assertEquals($attempt->userid, $scaled->userid); $this->assertEquals($attempt->scaled, $scaled->scaled); $this->assertEquals($attempt->timemodified, $scaled->timemodified); $this->assertEquals($result2[2], $attempt->attempt); } else { $this->assertEquals(0, $scaled->attemptid); } // Now check a single user record. $scaleds = $manager->get_users_scaled_score($user2->id); $this->assertCount(1, $scaleds); $scaled2 = $scaleds[$user2->id]; $this->assertEquals($scaled->userid, $scaled2->userid); $this->assertEquals($scaled->scaled, $scaled2->scaled); $this->assertEquals($scaled->attemptid, $scaled2->attemptid); $this->assertEquals($scaled->timemodified, $scaled2->timemodified); } /** * Data provider for get_users_scaled_score. * * @return array */ public static function get_users_scaled_score_data(): array { return [ 'Tracking with max attempt method' => [ 1, manager::GRADEHIGHESTATTEMPT, [1.00000, 31, 2], [0.66667, 32, 2] ], 'Tracking with average attempt method' => [ 1, manager::GRADEAVERAGEATTEMPT, [0.61111, 51, 0], [0.52222, 52, 0] ], 'Tracking with last attempt method' => [ 1, manager::GRADELASTATTEMPT, [0.33333, 51, 3], [0.40000, 52, 3] ], 'Tracking with first attempt method' => [ 1, manager::GRADEFIRSTATTEMPT, [0.50000, 11, 1], [0.50000, 12, 1] ], 'Tracking with manual attempt grading' => [ 1, manager::GRADEMANUAL, [], [] ], 'No tracking with max attempt method' => [ 0, manager::GRADEHIGHESTATTEMPT, [], [] ], 'No tracking with average attempt method' => [ 0, manager::GRADEAVERAGEATTEMPT, [], [] ], 'No tracking with last attempt method' => [ 0, manager::GRADELASTATTEMPT, [], [] ], 'No tracking with first attempt method' => [ 0, manager::GRADEFIRSTATTEMPT, [], [] ], 'No tracking with manual attempt grading' => [ 0, manager::GRADEMANUAL, [], [] ], ]; } /** * Test static get_grading_methods. */ public function test_get_grading_methods(): void { $methods = manager::get_grading_methods(); $this->assertCount(5, $methods); $this->assertNotEmpty($methods[manager::GRADEHIGHESTATTEMPT]); $this->assertNotEmpty($methods[manager::GRADEAVERAGEATTEMPT]); $this->assertNotEmpty($methods[manager::GRADELASTATTEMPT]); $this->assertNotEmpty($methods[manager::GRADEFIRSTATTEMPT]); $this->assertNotEmpty($methods[manager::GRADEMANUAL]); } /** * Test 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): void { $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 static 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(): void { $methods = manager::get_review_modes(); $this->assertCount(2, $methods); $this->assertNotEmpty($methods[manager::REVIEWCOMPLETION]); $this->assertNotEmpty($methods[manager::REVIEWNONE]); } /** * Test get_grader method. */ public function test_get_grader(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); $manager = manager::create_from_instance($activity); $grader = $manager->get_grader(); $this->assertInstanceOf('mod_h5pactivity\local\grader', $grader); } /** * 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): void { 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 static 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): void { $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 static 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 of one user. */ public function test_count_attempts(): void { $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 of all active participants. * * @dataProvider count_attempts_all_data * @param bool $canview if the student role has mod_h5pactivity/view capability * @param bool $cansubmit if the student role has mod_h5pactivity/submit capability * @param bool $extrarole if an extra role without submit capability is required * @param int $result the expected result */ public function test_count_attempts_all(bool $canview, bool $cansubmit, bool $extrarole, int $result): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module( 'h5pactivity', ['course' => $course] ); $manager = manager::create_from_instance($activity); $roleid = $DB->get_field('role', 'id', ['shortname' => 'student']); $newcap = ($canview) ? CAP_ALLOW : CAP_PROHIBIT; role_change_permission($roleid, $manager->get_context(), 'mod/h5pactivity:view', $newcap); $newcap = ($cansubmit) ? CAP_ALLOW : CAP_PROHIBIT; role_change_permission($roleid, $manager->get_context(), 'mod/h5pactivity:submit', $newcap); // Teacher with review capability and attempts (should not be listed). if ($extrarole) { $user1 = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $this->generate_fake_attempts($activity, $user1, 1); } // Student with attempts. $user2 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->generate_fake_attempts($activity, $user2, 1); // Another student with attempts. $user3 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->generate_fake_attempts($activity, $user3, 1); $this->assertEquals($result, $manager->count_attempts()); } /** * Data provider for test_count_attempts_all. * * @return array */ public static function count_attempts_all_data(): array { return [ 'Students with both view and submit capability' => [true, true, false, 6], 'Students without view but with submit capability' => [false, true, false, 0], 'Students with view but without submit capability' => [true, false, false, 6], 'Students without both view and submit capability' => [false, false, false, 0], 'Students with both view and submit capability and extra role' => [true, true, true, 6], 'Students without view but with submit capability and extra role' => [false, true, true, 0], 'Students with view but without submit capability and extra role' => [true, false, true, 6], 'Students without both view and submit capability and extra role' => [false, false, true, 0], ]; } /** * Test static test_get_active_users_join of all active participants. * * Most method scenarios are tested in test_count_attempts_all so we only * need to test the with $allpotentialusers true and false. * * @dataProvider get_active_users_join_data * @param bool $allpotentialusers if the join should return all potential users or only the submitted ones. * @param int $result the expected result */ public function test_get_active_users_join(bool $allpotentialusers, int $result): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module( 'h5pactivity', ['course' => $course] ); $manager = manager::create_from_instance($activity); $user1 = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $this->generate_fake_attempts($activity, $user1, 1); // Student with attempts. $user2 = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->generate_fake_attempts($activity, $user2, 1); // 2 more students without attempts. $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->getDataGenerator()->create_and_enrol($course, 'student'); $usersjoin = $manager->get_active_users_join($allpotentialusers); // Final SQL. $num = $DB->count_records_sql( "SELECT COUNT(DISTINCT u.id) FROM {user} u $usersjoin->joins WHERE $usersjoin->wheres", array_merge($usersjoin->params) ); $this->assertEquals($result, $num); } /** * Data provider for test_get_active_users_join. * * @return array */ public static function get_active_users_join_data(): array { return [ 'All potential users' => [ 'allpotentialusers' => true, 'result' => 3, ], 'Users with attempts' => [ 'allpotentialusers' => false, 'result' => 1, ], ]; } /** * Test active users joins returns appropriate results for groups */ public function test_get_active_users_join_groupmode(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1]); // Teacher/user one in group one. $teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $userone = $this->getDataGenerator()->create_and_enrol($course, 'student'); $groupone = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $teacher->id]); $this->getDataGenerator()->create_group_member(['groupid' => $groupone->id, 'userid' => $userone->id]); // User two in group two. $usertwo = $this->getDataGenerator()->create_and_enrol($course, 'student'); $grouptwo = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $this->getDataGenerator()->create_group_member(['groupid' => $grouptwo->id, 'userid' => $usertwo->id]); // User three in no group. $userthree = $this->getDataGenerator()->create_and_enrol($course, 'student'); // User four in a non-participation group. $userfour = $this->getDataGenerator()->create_and_enrol($course, 'student'); $groupthree = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'participation' => 0]); $this->getDataGenerator()->create_group_member(['groupid' => $groupthree->id, 'userid' => $userfour->id]); // Editing teacher in no group. $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $manager = manager::create_from_instance($activity); // Admin user can view all participants (any group and none). $usersjoin = $manager->get_active_users_join(true, 0); $users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}", $usersjoin->params); $this->assertEqualsCanonicalizing( [$userone->username, $usertwo->username, $userthree->username, $userfour->username], $users); // Switch to teacher, who cannot view all participants. $this->setUser($teacher); $usersjoin = $manager->get_active_users_join(true, 0); $users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}", $usersjoin->params); $this->assertEmpty($users); // Teacher can view participants inside group. $usersjoin = $manager->get_active_users_join(true, $groupone->id); $users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}", $usersjoin->params); $this->assertEqualsCanonicalizing([$userone->username], $users); // Switch to editing teacher, who can view all participants. $this->setUser($editingteacher); $usersjoin = $manager->get_active_users_join(true, 0); $users = $DB->get_fieldset_sql("SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}", $usersjoin->params); $this->assertEqualsCanonicalizing( [$userone->username, $usertwo->username, $userthree->username, $userfour->username], $users); } /** * Test getting active users join where there are no roles with 'mod/h5pactivity:reviewattempts' capability */ public function test_get_active_users_join_no_reviewers(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); $manager = manager::create_from_instance($activity); // By default manager and editingteacher can review attempts, prohibit both. $rolemanager = $DB->get_field('role', 'id', ['shortname' => 'manager']); role_change_permission($rolemanager, $manager->get_context(), 'mod/h5pactivity:reviewattempts', CAP_PROHIBIT); $roleeditingteacher = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']); role_change_permission($roleeditingteacher, $manager->get_context(), 'mod/h5pactivity:reviewattempts', CAP_PROHIBIT); // Generate users join SQL to find matching users. $usersjoin = $manager->get_active_users_join(true); $usernames = $DB->get_fieldset_sql( "SELECT u.username FROM {user} u {$usersjoin->joins} WHERE {$usersjoin->wheres}", $usersjoin->params ); $this->assertEquals([$user->username], $usernames); } /** * Test static count_attempts. */ public function test_count_users_attempts(): void { $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): void { $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 static 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'] ], ]; } /** * Test teacher access to student reports (get_report) when course groupmode is SEPARATEGROUPS. * @covers ::get_report() * @dataProvider get_report_data_groupmode * * @param bool $activitygroupmode Course or activity groupmode */ public function test_get_report_groupmode(bool $activitygroupmode): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); if ($activitygroupmode) { $course = $this->getDataGenerator()->create_course(['groupmode' => NOGROUPS, 'groupmodeforce' => 0]); $activitysettings = ['course' => $course, 'groupmode' => SEPARATEGROUPS]; } else { $course = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS, 'groupmodeforce' => 1]); $activitysettings = ['course' => $course]; } $activity = $this->getDataGenerator()->create_module('h5pactivity', $activitysettings); // Grant mod/h5pactivity:reviewattempts to non-editing teacher. // At the time of writing this is not set by default (see MDL-80028). $teacherrole = $DB->get_record('role', ['shortname' => 'teacher']); role_change_permission($teacherrole->id, \context_course::instance($course->id), 'mod/h5pactivity:reviewattempts', CAP_ALLOW); $manager = manager::create_from_instance($activity); $editingteacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $teacher1 = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $teacher2 = $this->getDataGenerator()->create_and_enrol($course, 'teacher'); $student1 = $this->getDataGenerator()->create_and_enrol($course); $student2 = $this->getDataGenerator()->create_and_enrol($course); $student3 = $this->getDataGenerator()->create_and_enrol($course); $group1 = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $this->getDataGenerator()->create_group_member(['groupid' => $group1->id, 'userid' => $teacher1->id]); $this->getDataGenerator()->create_group_member(['groupid' => $group1->id, 'userid' => $student1->id]); $group2 = $this->getDataGenerator()->create_group(['courseid' => $course->id]); $this->getDataGenerator()->create_group_member(['groupid' => $group2->id, 'userid' => $student2->id]); // Check reports. // Editing teachers can view all users, those in any group or no group. $this->setUser($editingteacher); $report = $manager->get_report($student1->id); $this->assertNotNull($report); $report = $manager->get_report($student3->id); $this->assertNotNull($report); // Non-editing teacher can view student, both members of same group. $this->setUser($teacher1); $report = $manager->get_report($student1->id); $this->assertNotNull($report); // Non-editing teacher cannot view student in no group. $report = $manager->get_report($student3->id); $this->assertNull($report); // Non-editing teacher cannot view student in different group. $report = $manager->get_report($student2->id); $this->assertNull($report); // Non-editing teacher in no group can view no one. $this->setUser($teacher2); $report = $manager->get_report($student1->id); $this->assertNull($report); $report = $manager->get_report($student3->id); $this->assertNull($report); } /** * Data provider for test_get_report_groupmode. * * @return array */ public static function get_report_data_groupmode(): array { return [ // No tracking scenarios. 'course groupmode is SEPARATEGROUPS' => [false], 'course groupmode is NOGROUPS, activity groupmode is SEPARATEGROUPS' => [true], ]; } /** * Test get_attempt method. * * @dataProvider get_attempt_data * @param string $attemptname the attempt to use * @param string|null $result the expected attempt ID or null for none */ public function test_get_attempt(string $attemptname, ?string $result): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST); $otheractivity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $othercm = get_coursemodule_from_id('h5pactivity', $otheractivity->cmid, 0, false, MUST_EXIST); $manager = manager::create_from_instance($activity); $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); $attempts = ['inexistent' => 0]; $this->generate_fake_attempts($activity, $user, 1); $attempt = attempt::last_attempt($user, $cm); $attempts['current'] = $attempt->get_id(); $this->generate_fake_attempts($otheractivity, $user, 1); $attempt = attempt::last_attempt($user, $othercm); $attempts['other'] = $attempt->get_id(); $attempt = $manager->get_attempt($attempts[$attemptname]); if ($result === null) { $this->assertNull($attempt); } else { $this->assertEquals($attempts[$attemptname], $attempt->get_id()); $this->assertEquals($activity->id, $attempt->get_h5pactivityid()); $this->assertEquals($user->id, $attempt->get_userid()); $this->assertEquals(4, $attempt->get_attempt()); } } /** * Data provider for test_get_attempt. * * @return array */ public static function get_attempt_data(): array { return [ 'Get the current activity attempt' => [ 'current', 'current' ], 'Try to get another activity attempt' => [ 'other', null ], 'Try to get an inexistent attempt' => [ 'inexistent', null ], ]; } /** * Insert fake attempt data into h5pactiviyt_attempts. * * This function insert 4 attempts. 3 of them finished with different gradings * and timestamps and 1 unfinished. * * @param stdClass $activity the activity record * @param stdClass $user user record * @param int $basescore a score to be used to generate all attempts */ private function generate_fake_attempts(stdClass $activity, stdClass $user, int $basescore) { global $DB; $attempt = (object)[ 'h5pactivityid' => $activity->id, 'userid' => $user->id, 'timecreated' => $basescore, 'timemodified' => ($basescore + 10), 'attempt' => 1, 'rawscore' => $basescore, 'maxscore' => ($basescore + $basescore), 'duration' => $basescore, 'completion' => 1, 'success' => 1, ]; $attempt->scaled = $attempt->rawscore / $attempt->maxscore; $DB->insert_record('h5pactivity_attempts', $attempt); $attempt = (object)[ 'h5pactivityid' => $activity->id, 'userid' => $user->id, 'timecreated' => ($basescore + 20), 'timemodified' => ($basescore + 30), 'attempt' => 2, 'rawscore' => $basescore, 'maxscore' => ($basescore + $basescore - 1), 'duration' => $basescore, 'completion' => 1, 'success' => 1, ]; $attempt->scaled = $attempt->rawscore / $attempt->maxscore; $DB->insert_record('h5pactivity_attempts', $attempt); $attempt = (object)[ 'h5pactivityid' => $activity->id, 'userid' => $user->id, 'timecreated' => ($basescore + 40), 'timemodified' => ($basescore + 50), 'attempt' => 3, 'rawscore' => $basescore, 'maxscore' => ($basescore + $basescore + 1), 'duration' => $basescore, 'completion' => 1, 'success' => 0, ]; $attempt->scaled = $attempt->rawscore / $attempt->maxscore; $DB->insert_record('h5pactivity_attempts', $attempt); // Unfinished attempt. $attempt = (object)[ 'h5pactivityid' => $activity->id, 'userid' => $user->id, 'timecreated' => ($basescore + 60), 'timemodified' => ($basescore + 60), 'attempt' => 4, 'rawscore' => $basescore, 'maxscore' => $basescore, 'duration' => $basescore, ]; $DB->insert_record('h5pactivity_attempts', $attempt); } } PK1B\#sGGtests/lib_test.phpnu[. namespace mod_h5pactivity; use advanced_testcase; use mod_h5pactivity\local\manager; /** * Unit tests for (some of) mod/h5pactivity/lib.php. * * @package mod_h5pactivity * @copyright 2021 Ilya Tregubov * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class lib_test extends advanced_testcase { /** * Load required test libraries */ public static function setUpBeforeClass(): void { global $CFG; require_once("{$CFG->dirroot}/mod/h5pactivity/lib.php"); parent::setUpBeforeClass(); } /** * Test that h5pactivity_delete_instance removes data. * * @covers ::h5pactivity_delete_instance */ public function test_h5pactivity_delete_instance(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $this->setUser($user); /** @var \mod_h5pactivity_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); /** @var \core_h5p_generator $h5pgenerator */ $h5pgenerator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); // Add an attempt to the H5P activity. $attemptinfo = [ 'userid' => $user->id, 'h5pactivityid' => $activity->id, 'attempt' => 1, 'interactiontype' => 'compound', 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0, ]; $generator->create_attempt($attemptinfo); // Add also a xAPI state to the H5P activity. $filerecord = [ 'contextid' => \context_module::instance($activity->cmid)->id, 'component' => 'mod_h5pactivity', 'filearea' => 'package', 'itemid' => 0, 'filepath' => '/', 'filename' => 'dummy.h5p', 'addxapistate' => true, ]; $h5pgenerator->generate_h5p_data(false, $filerecord); // Check the H5P activity exists and the attempt has been created. $this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id])); $this->assertEquals(2, $DB->count_records('grade_items')); $this->assertEquals(2, $DB->count_records('grade_grades')); $this->assertEquals(1, $DB->count_records('xapi_states')); // Check nothing happens when given activity id doesn't exist. h5pactivity_delete_instance($activity->id + 1); $this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id])); $this->assertEquals(2, $DB->count_records('grade_items')); $this->assertEquals(2, $DB->count_records('grade_grades')); $this->assertEquals(1, $DB->count_records('xapi_states')); // Check the H5P instance and its associated data is removed. h5pactivity_delete_instance($activity->id); $this->assertEmpty($DB->get_record('h5pactivity', ['id' => $activity->id])); $this->assertEquals(1, $DB->count_records('grade_items')); $this->assertEquals(1, $DB->count_records('grade_grades')); $this->assertEquals(0, $DB->count_records('xapi_states')); } /** * Test that assign_print_recent_activity shows ungraded submitted assignments. * * @covers ::h5pactivity_print_recent_activity */ public function test_print_recent_activity(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course, 'enabletracking' => 1, 'grademethod' => manager::GRADEHIGHESTATTEMPT]); $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); $manager = manager::create_from_instance($activity); $cm = $manager->get_coursemodule(); $user = $student; $params = ['cmid' => $cm->id, 'userid' => $user->id]; $generator->create_content($activity, $params); $this->setUser($student); $this->expectOutputRegex('/submitted:/'); h5pactivity_print_recent_activity($course, true, time() - 3600); } /** * Test that h5pactivity_print_recent_activity does not display any warnings when a custom fullname has been configured. * * @covers ::h5pactivity_print_recent_activity */ public function test_print_recent_activity_fullname(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course, 'enabletracking' => 1, 'grademethod' => manager::GRADEHIGHESTATTEMPT]); $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); $manager = manager::create_from_instance($activity); $cm = $manager->get_coursemodule(); $user = $student; $params = ['cmid' => $cm->id, 'userid' => $user->id]; $generator->create_content($activity, $params); $this->setUser($teacher); $this->expectOutputRegex('/submitted:/'); set_config('fullnamedisplay', 'firstname, lastnamephonetic'); h5pactivity_print_recent_activity($course, false, time() - 3600); } /** * Test that h5pactivity_get_recent_mod_activity fetches the h5pactivity correctly. * * @covers ::h5pactivity_get_recent_mod_activity */ public function test_h5pactivity_get_recent_mod_activity(): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course, 'enabletracking' => 1, 'grademethod' => manager::GRADEHIGHESTATTEMPT]); $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); $manager = manager::create_from_instance($activity); $cm = $manager->get_coursemodule(); $user = $student; $params = ['cmid' => $cm->id, 'userid' => $user->id]; $generator->create_content($activity, $params); $index = 1; $activities = [ $index => (object) [ 'type' => 'h5pactivity', 'cmid' => $cm->id, ], ]; $this->setUser($teacher); h5pactivity_get_recent_mod_activity($activities, $index, time() - HOURSECS, $course->id, $cm->id); $activity = $activities[1]; $this->assertEquals("h5pactivity", $activity->type); $this->assertEquals($student->id, $activity->user->id); } /** * Test that h5pactivity_get_recent_mod_activity fetches activity correctly. * * @covers ::h5pactivity_fetch_recent_activity */ public function test_h5pactivity_fetch_recent_activity(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); // Create users and groups. $students = array(); $groups = array(); $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); for ($i = 1; $i < 6; $i++) { $students[$i] = $this->getDataGenerator()->create_and_enrol($course, 'student'); $groups[$i] = $this->getDataGenerator()->create_group(array('courseid' => $course->id)); } $groups[$i] = $this->getDataGenerator()->create_group(array('courseid' => $course->id, 'participation' => 0)); // Update the course set the groupmode SEPARATEGROUPS and forced. update_course((object)array('id' => $course->id, 'groupmode' => SEPARATEGROUPS, 'groupmodeforce' => true)); // Student 1 is in groups 1 and 3. groups_add_member($groups[1], $students[1]); groups_add_member($groups[3], $students[1]); // Student 2 is in groups 1 and 2. groups_add_member($groups[1], $students[2]); groups_add_member($groups[2], $students[2]); // Student 3 is only in group 3. groups_add_member($groups[3], $students[3]); // Student 4 is only in group 5 (non-participation). groups_add_member($groups[6], $students[4]); // Student 5 is not in any groups. // Grader is only in group 3. groups_add_member($groups[3], $teacher); $timestart = time() - 1; // Create h5pactivity. $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course->id, 'enabletracking' => 1, 'grademethod' => manager::GRADEHIGHESTATTEMPT, 'groupmode' => SEPARATEGROUPS]); $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher')); $cmcontext = \context_module::instance($activity->cmid); assign_capability('moodle/site:accessallgroups', CAP_PROHIBIT, $teacherrole->id, $cmcontext, true); $manager = manager::create_from_instance($activity); $cm = $manager->get_coursemodule(); // Create attempts. $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); foreach ($students as $student) { $params = ['cmid' => $cm->id, 'userid' => $student->id]; $generator->create_content($activity, $params); } // Get all attempts. $dbparams = [$timestart, $course->id, 'h5pactivity']; $userfieldsapi = \core_user\fields::for_userpic(); $namefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects;; $sql = "SELECT h5pa.id, h5pa.timemodified, cm.id as cmid, $namefields FROM {h5pactivity_attempts} h5pa JOIN {h5pactivity} h5p ON h5p.id = h5pa.h5pactivityid JOIN {course_modules} cm ON cm.instance = h5p.id JOIN {modules} md ON md.id = cm.module JOIN {user} u ON u.id = h5pa.userid WHERE h5pa.timemodified > ? AND h5p.course = ? AND md.name = ? ORDER BY h5pa.timemodified ASC"; $submissions = $DB->get_records_sql($sql, $dbparams); $this->assertCount(count($students), $submissions); // Fetch activity for student (only his own). $this->setUser($students[1]); $recentactivity = h5pactivity_fetch_recent_activity($submissions, $course->id); $this->assertCount(1, $recentactivity); $this->assertEquals($students[1]->id, $recentactivity[$students[1]->id]->userid); // Fetch users group info for grader. $this->setUser($teacher); $recentactivity = h5pactivity_fetch_recent_activity($submissions, $course->id); $this->assertCount(2, $recentactivity); // Grader, Student 1 and 3 are in Group 3. $this->assertEquals($students[1]->id, $recentactivity[$students[1]->id]->userid); $this->assertEquals($students[3]->id, $recentactivity[$students[3]->id]->userid); // Grader is in Group 2. groups_remove_member($groups[3], $teacher); groups_add_member($groups[2], $teacher); get_fast_modinfo($course->id, 0, true); $recentactivity = h5pactivity_fetch_recent_activity($submissions, $course->id); $this->assertCount(1, $recentactivity); // Grader, Student 2 are in Group 2. $this->assertEquals($students[2]->id, $recentactivity[$students[2]->id]->userid); // Grader is in Group 1. groups_remove_member($groups[2], $teacher); groups_add_member($groups[1], $teacher); get_fast_modinfo($course->id, 0, true); $recentactivity = h5pactivity_fetch_recent_activity($submissions, $course->id); $this->assertCount(2, $recentactivity); // Grader, Student 1 and 2 are in Group 1. $this->assertEquals($students[1]->id, $recentactivity[$students[1]->id]->userid); $this->assertEquals($students[2]->id, $recentactivity[$students[2]->id]->userid); // Grader is in no group. groups_remove_member($groups[1], $teacher); get_fast_modinfo($course->id, 0, true); $recentactivity = h5pactivity_fetch_recent_activity($submissions, $course->id); // Student 4 and Student 5 have submissions, but they are not in a participation group, so they do not show up in recent // activity for separate groups mode. $this->assertCount(0, $recentactivity); } /** * Test that h5pactivity_reset_userdata reset user data. * * @covers ::h5pactivity_reset_userdata */ public function test_h5pactivity_reset_userdata(): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $this->setUser($user); /** @var \mod_h5pactivity_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); /** @var \core_h5p_generator $h5pgenerator */ $h5pgenerator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); // Add an attempt to the H5P activity. $attemptinfo = [ 'userid' => $user->id, 'h5pactivityid' => $activity->id, 'attempt' => 1, 'interactiontype' => 'compound', 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0, ]; $generator->create_attempt($attemptinfo); // Add also a xAPI state to the H5P activity. $filerecord = [ 'contextid' => \context_module::instance($activity->cmid)->id, 'component' => 'mod_h5pactivity', 'filearea' => 'package', 'itemid' => 0, 'filepath' => '/', 'filename' => 'dummy.h5p', 'addxapistate' => true, ]; $h5pgenerator->generate_h5p_data(false, $filerecord); // Check the H5P activity exists and the attempt has been created with the expected data. $this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id])); $this->assertEquals(2, $DB->count_records('grade_items')); $this->assertEquals(2, $DB->count_records('grade_grades')); $this->assertEquals(1, $DB->count_records('xapi_states')); // Check nothing happens when reset_h5pactivity is not set. $data = new \stdClass(); h5pactivity_reset_userdata($data); $this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id])); $this->assertEquals(2, $DB->count_records('grade_items')); $this->assertEquals(2, $DB->count_records('grade_grades')); $this->assertEquals(1, $DB->count_records('xapi_states')); $this->assertEquals(1, $DB->count_records('xapi_states')); // Check nothing happens when reset_h5pactivity is not set. $data = (object) [ 'courseid' => $course->id, ]; h5pactivity_reset_userdata($data); $this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id])); $this->assertEquals(2, $DB->count_records('grade_items')); $this->assertEquals(2, $DB->count_records('grade_grades')); $this->assertEquals(1, $DB->count_records('xapi_states')); $this->assertEquals(1, $DB->count_records('xapi_states')); // Check nothing happens when the given course doesn't exist. $data = (object) [ 'reset_h5pactivity' => true, 'courseid' => $course->id + 1, ]; h5pactivity_reset_userdata($data); $this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id])); $this->assertEquals(2, $DB->count_records('grade_items')); $this->assertEquals(2, $DB->count_records('grade_grades')); $this->assertEquals(1, $DB->count_records('xapi_states')); $this->assertEquals(1, $DB->count_records('xapi_states')); // Check the H5P instance and its associated data is reset. $data = (object) [ 'reset_h5pactivity' => true, 'courseid' => $course->id, ]; h5pactivity_reset_userdata($data); $this->assertNotEmpty($DB->get_record('h5pactivity', ['id' => $activity->id])); $this->assertEquals(2, $DB->count_records('grade_items')); $this->assertEquals(1, $DB->count_records('grade_grades')); $this->assertEquals(0, $DB->count_records('xapi_states')); } } PK1B\X`&&tests/restore_test.phpnu[. /** * mod_h5pactivity generator tests * * @package mod_h5pactivity * @category test * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace mod_h5pactivity; use advanced_testcase; use backup; use backup_controller; use backup_setting; use restore_controller; use restore_dbops; use stdClass; /** * Genarator tests class for mod_h5pactivity. * * @package mod_h5pactivity * @category test * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class restore_test extends advanced_testcase { /** * Setup to ensure that fixtures are loaded. */ public static function setupBeforeClass(): void { global $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); } /** * Test on H5P activity backup and restore. * * @dataProvider backup_restore_data * @param bool $content if has to create attempts * @param bool $userdata if backup have userdata * @param array $result1 data to check on original course * @param array $result2 data to check on resotred course */ public function test_backup_restore(bool $content, bool $userdata, array $result1, array $result2): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); // Create one activity. $this->assertFalse($DB->record_exists('h5pactivity', ['course' => $course->id])); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $cm = get_coursemodule_from_id('h5pactivity', $activity->cmid, 0, false, MUST_EXIST); if ($content) { $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); $params = ['cmid' => $cm->id, 'userid' => $user->id]; $generator->create_content($activity, $params); } $this->assertEquals($result1[0], $DB->count_records('h5pactivity', ['course' => $course->id])); $this->assertEquals($result1[1], $DB->count_records('h5pactivity_attempts', ['h5pactivityid' => $activity->id])); $attemptid = $DB->get_field('h5pactivity_attempts', 'id', ['h5pactivityid' => $activity->id]); $this->assertEquals($result1[2], $DB->count_records('h5pactivity_attempts_results', ['attemptid' => $attemptid])); // Execute course backup and restore. $newcourseid = $this->backup_and_restore($course, $userdata); // Check original activity. $this->assertEquals($result1[0], $DB->count_records('h5pactivity', ['course' => $course->id])); $this->assertEquals($result1[1], $DB->count_records('h5pactivity_attempts', ['h5pactivityid' => $activity->id])); $attempt = $DB->get_record('h5pactivity_attempts', ['h5pactivityid' => $activity->id]); $attemptid = $attempt->id ?? 0; $this->assertEquals($result1[2], $DB->count_records('h5pactivity_attempts_results', ['attemptid' => $attemptid])); // Check original activity. $this->assertEquals($result2[0], $DB->count_records('h5pactivity', ['course' => $newcourseid])); $activity2 = $DB->get_record('h5pactivity', ['course' => $newcourseid]); $this->assertEquals($result2[1], $DB->count_records('h5pactivity_attempts', ['h5pactivityid' => $activity2->id])); $attempt2 = $DB->get_record('h5pactivity_attempts', ['h5pactivityid' => $activity2->id]); $attempt2id = $attempt2->id ?? 0; $this->assertEquals($result2[2], $DB->count_records('h5pactivity_attempts_results', ['attemptid' => $attempt2id])); // Compare activities. $this->assertEquals($newcourseid, $activity2->course); $this->assertEquals($activity->name, $activity2->name); $this->assertEquals($activity->intro, $activity2->intro); $this->assertEquals($activity->introformat, $activity2->introformat); $this->assertEquals($activity->grade, $activity2->grade); $this->assertEquals($activity->displayoptions, $activity2->displayoptions); $this->assertEquals($activity->enabletracking, $activity2->enabletracking); $this->assertEquals($activity->grademethod, $activity2->grademethod); // Compare attempts. if ($content && $userdata) { $this->assertEquals($activity2->id, $attempt2->h5pactivityid); $this->assertEquals($attempt->userid, $attempt2->userid); $this->assertEquals($attempt->timecreated, $attempt2->timecreated); $this->assertEquals($attempt->timemodified, $attempt2->timemodified); $this->assertEquals($attempt->attempt, $attempt2->attempt); $this->assertEquals($attempt->rawscore, $attempt2->rawscore); $this->assertEquals($attempt->maxscore, $attempt2->maxscore); $this->assertEquals($attempt->duration, $attempt2->duration); $this->assertEquals($attempt->completion, $attempt2->completion); $this->assertEquals($attempt->success, $attempt2->success); // Compare results. $results = $DB->get_records('h5pactivity_attempts_results', ['attemptid' => $attempt->id]); foreach ($results as $result) { $result2 = $DB->get_record('h5pactivity_attempts_results', [ 'subcontent' => $result->subcontent, 'attemptid' => $attempt2->id ]); $this->assertNotFalse($result2); $this->assertEquals($result->timecreated, $result2->timecreated); $this->assertEquals($result->interactiontype, $result2->interactiontype); $this->assertEquals($result->description, $result2->description); $this->assertEquals($result->correctpattern, $result2->correctpattern); $this->assertEquals($result->response, $result2->response); $this->assertEquals($result->additionals, $result2->additionals); $this->assertEquals($result->rawscore, $result2->rawscore); $this->assertEquals($result->maxscore, $result2->maxscore); $this->assertEquals($result->duration, $result2->duration); $this->assertEquals($result->completion, $result2->completion); $this->assertEquals($result->success, $result2->success); } } } /** * Data provider for test_backup_restore. * * @return array */ public static function backup_restore_data(): array { return [ 'Activity attempts and restore with userdata' => [ true, true, [1, 1, 3], [1, 1, 3] ], 'No activity attempts and restore with userdata' => [ false, true, [1, 0, 0], [1, 0, 0] ], 'Activity attempts and restore with no userdata' => [ true, false, [1, 1, 3], [1, 0, 0] ], 'No activity attempts and restore with no userdata' => [ false, false, [1, 0, 0], [1, 0, 0] ], ]; } /** * Backs a course up and restores it. * * @param stdClass $srccourse Course object to backup * @param bool $userdata if the backup must be with user data * @return int ID of newly restored course */ private function backup_and_restore(stdClass $srccourse, bool $userdata): int { global $USER, $CFG; require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); // Turn off file logging, otherwise it can't delete the file (Windows). $CFG->backup_file_logger_level = backup::LOG_NONE; // Do backup with default settings. MODE_IMPORT means it will just // create the directory and not zip it. $bc = new backup_controller(backup::TYPE_1COURSE, $srccourse->id, backup::FORMAT_MOODLE, backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); $bc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED); $bc->get_plan()->get_setting('users')->set_value($userdata); $backupid = $bc->get_backupid(); $bc->execute_plan(); $bc->destroy(); // Do restore to new course with default settings. $newcourseid = restore_dbops::create_new_course( $srccourse->fullname, $srccourse->shortname . '_2', $srccourse->category ); $rc = new restore_controller($backupid, $newcourseid, backup::INTERACTIVE_NO, backup::MODE_GENERAL, $USER->id, backup::TARGET_NEW_COURSE); $rc->get_plan()->get_setting('users')->set_status(backup_setting::NOT_LOCKED); $rc->get_plan()->get_setting('users')->set_value($userdata); $this->assertTrue($rc->execute_precheck()); $rc->execute_plan(); $rc->destroy(); return $newcourseid; } } PK1B\۰ KKtests/xapi/handler_test.phpnu[. namespace mod_h5pactivity\xapi; use \core_xapi\local\statement; use \core_xapi\local\statement\item_agent; use \core_xapi\local\statement\item_activity; use \core_xapi\local\statement\item_definition; use \core_xapi\local\statement\item_verb; use \core_xapi\local\statement\item_result; use context_module; use core_xapi\test_helper; use stdClass; /** * Attempt tests class for mod_h5pactivity. * * @package mod_h5pactivity * @category test * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @covers \mod_h5pactivity\xapi\handler */ final class handler_test extends \advanced_testcase { /** * Setup to ensure that fixtures are loaded. */ public static function setUpBeforeClass(): void { global $CFG; require_once($CFG->dirroot.'/lib/xapi/tests/helper.php'); parent::setUpBeforeClass(); } /** * Generate a valid scenario for each tests. * * @return stdClass an object with all scenario data in it */ private function generate_testing_scenario(): stdClass { $this->resetAfterTest(); $this->setAdminUser(); $data = new stdClass(); $data->course = $this->getDataGenerator()->create_course(); // Generate 2 users, one enroled into course and one not. $data->student = $this->getDataGenerator()->create_and_enrol($data->course, 'student'); $data->otheruser = $this->getDataGenerator()->create_user(); // H5P activity. $data->activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $data->course]); $data->context = context_module::instance($data->activity->cmid); $data->xapihandler = handler::create('mod_h5pactivity'); $this->assertNotEmpty($data->xapihandler); $this->assertInstanceOf('\mod_h5pactivity\xapi\handler', $data->xapihandler); $this->setUser($data->student); return $data; } /** * Test for xapi_handler with valid statements. */ public function test_xapi_handler(): void { global $DB; $data = $this->generate_testing_scenario(); $xapihandler = $data->xapihandler; $context = $data->context; $student = $data->student; $otheruser = $data->otheruser; // Check we have 0 entries in the attempts tables. $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(0, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(0, $count); $statements = $this->generate_statements($context, $student); // Insert first statement. $event = $xapihandler->statement_to_event($statements[0]); $this->assertNotNull($event); $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(1, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(1, $count); // Insert second statement. $event = $xapihandler->statement_to_event($statements[1]); $this->assertNotNull($event); $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(1, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(2, $count); // Insert again first statement. $event = $xapihandler->statement_to_event($statements[0]); $this->assertNotNull($event); $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(2, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(3, $count); // Insert again second statement. $event = $xapihandler->statement_to_event($statements[1]); $this->assertNotNull($event); $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(2, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(4, $count); } /** * Testing wrong statements scenarios. * * @dataProvider xapi_handler_errors_data * @param bool $hasverb valid verb * @param bool $hasdefinition generate definition * @param bool $hasresult generate result * @param bool $hascontext valid context * @param bool $hasuser valid user * @param bool $generateattempt if generates an empty attempt */ public function test_xapi_handler_errors(bool $hasverb, bool $hasdefinition, bool $hasresult, bool $hascontext, bool $hasuser, bool $generateattempt): void { global $DB, $CFG; $data = $this->generate_testing_scenario(); $xapihandler = $data->xapihandler; $context = $data->context; $student = $data->student; $otheruser = $data->otheruser; // Check we have 0 entries in the attempts tables. $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(0, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(0, $count); $statement = new statement(); if ($hasverb) { $statement->set_verb(item_verb::create_from_id('http://adlnet.gov/expapi/verbs/completed')); } else { $statement->set_verb(item_verb::create_from_id('cook')); } $definition = null; if ($hasdefinition) { $definition = item_definition::create_from_data((object)[ 'interactionType' => 'compound', 'correctResponsesPattern' => '1', ]); } if ($hascontext) { $statement->set_object(item_activity::create_from_id($context->id, $definition)); } else { $statement->set_object(item_activity::create_from_id('paella', $definition)); } if ($hasresult) { $statement->set_result(item_result::create_from_data((object)[ 'completion' => true, 'success' => true, 'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1], ])); } if ($hasuser) { $statement->set_actor(item_agent::create_from_user($student)); } else { $statement->set_actor(item_agent::create_from_user($otheruser)); } $event = $xapihandler->statement_to_event($statement); $this->assertNull($event); // No enties should be generated. $count = $DB->count_records('h5pactivity_attempts'); $attempts = ($generateattempt) ? 1 : 0; $this->assertEquals($attempts, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(0, $count); } /** * Data provider for data request creation tests. * * @return array */ public static function xapi_handler_errors_data(): array { return [ // Invalid Definitions and results possibilities. 'Invalid definition and result' => [ true, false, false, true, true, false ], 'Invalid result' => [ true, true, false, true, true, false ], 'Invalid definition (generate empty attempt)' => [ true, false, true, true, true, true ], // Invalid verb possibilities. 'Invalid verb, definition and result' => [ false, false, false, true, true, false ], 'Invalid verb and result' => [ false, true, false, true, true, false ], 'Invalid verb and definition' => [ false, false, true, true, true, false ], // Invalid context possibilities. 'Invalid definition, result and context' => [ true, false, false, false, true, false ], 'Invalid result and context' => [ true, true, false, false, true, false ], 'Invalid definition and context' => [ true, false, true, false, true, false ], 'Invalid verb, definition result and context' => [ false, false, false, false, true, false ], 'Invalid verb, result and context' => [ false, true, false, false, true, false ], 'Invalid verb, definition and context' => [ false, false, true, false, true, false ], // Invalid user possibilities. 'Invalid definition, result and user' => [ true, false, false, true, false, false ], 'Invalid result and user' => [ true, true, false, true, false, false ], 'Invalid definition and user' => [ true, false, true, true, false, false ], 'Invalid verb, definition, result and user' => [ false, false, false, true, false, false ], 'Invalid verb, result and user' => [ false, true, false, true, false, false ], 'Invalid verb, definition and user' => [ false, false, true, true, false, false ], 'Invalid definition, result, context and user' => [ true, false, false, false, false, false ], 'Invalid result, context and user' => [ true, true, false, false, false, false ], 'Invalid definition, context and user' => [ true, false, true, false, false, false ], 'Invalid verb, definition, result, context and user' => [ false, false, false, false, false, false ], 'Invalid verb, result, context and user' => [ false, true, false, false, false, false ], 'Invalid verb, definition, context and user' => [ false, false, true, false, false, false ], ]; } /** * Test xapi_handler stored statements. */ public function test_stored_statements(): void { global $DB; $data = $this->generate_testing_scenario(); $xapihandler = $data->xapihandler; $context = $data->context; $student = $data->student; $otheruser = $data->otheruser; $activity = $data->activity; // Check we have 0 entries in the attempts tables. $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(0, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(0, $count); $statements = $this->generate_statements($context, $student); // Insert statements. $stored = $xapihandler->process_statements($statements); $this->assertCount(2, $stored); $this->assertEquals(true, $stored[0]); $this->assertEquals(true, $stored[1]); $count = $DB->count_records('h5pactivity_attempts'); $this->assertEquals(1, $count); $count = $DB->count_records('h5pactivity_attempts_results'); $this->assertEquals(2, $count); // Validate stored data. $attempts = $DB->get_records('h5pactivity_attempts'); $attempt = array_shift($attempts); $statement = $statements[0]; $data = $statement->get_result()->get_data(); $this->assertEquals(1, $attempt->attempt); $this->assertEquals($student->id, $attempt->userid); $this->assertEquals($activity->id, $attempt->h5pactivityid); $this->assertEquals($data->score->raw, $attempt->rawscore); $this->assertEquals($data->score->max, $attempt->maxscore); $this->assertEquals($statement->get_result()->get_duration(), $attempt->duration); $this->assertEquals($data->completion, $attempt->completion); $this->assertEquals($data->success, $attempt->success); $results = $DB->get_records('h5pactivity_attempts_results'); foreach ($results as $result) { $statement = (empty($result->subcontent)) ? $statements[0] : $statements[1]; $xapiresult = $statement->get_result()->get_data(); $xapiobject = $statement->get_object()->get_data(); $this->assertEquals($attempt->id, $result->attemptid); $this->assertEquals($xapiobject->definition->interactionType, $result->interactiontype); $this->assertEquals($xapiresult->score->raw, $result->rawscore); $this->assertEquals($xapiresult->score->max, $result->maxscore); $this->assertEquals($statement->get_result()->get_duration(), $result->duration); $this->assertEquals($xapiresult->completion, $result->completion); $this->assertEquals($xapiresult->success, $result->success); } } /** * Returns a basic xAPI statements simulating a H5P content. * * @param context_module $context activity context * @param stdClass $user user record * @return statement[] array of xAPI statements */ private function generate_statements(context_module $context, stdClass $user): array { $statements = []; $statement = new statement(); $statement->set_actor(item_agent::create_from_user($user)); $statement->set_verb(item_verb::create_from_id('http://adlnet.gov/expapi/verbs/completed')); $definition = item_definition::create_from_data((object)[ 'interactionType' => 'compound', 'correctResponsesPattern' => '1', ]); $statement->set_object(item_activity::create_from_id($context->id, $definition)); $statement->set_result(item_result::create_from_data((object)[ 'completion' => true, 'success' => true, 'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1], 'duration' => 'PT25S', ])); $statements[] = $statement; $statement = new statement(); $statement->set_actor(item_agent::create_from_user($user)); $statement->set_verb(item_verb::create_from_id('http://adlnet.gov/expapi/verbs/completed')); $definition = item_definition::create_from_data((object)[ 'interactionType' => 'matching', 'correctResponsesPattern' => '1', ]); $statement->set_object(item_activity::create_from_id($context->id.'?subContentId=111-222-333', $definition)); $statement->set_result(item_result::create_from_data((object)[ 'completion' => true, 'success' => true, 'score' => (object) ['min' => 0, 'max' => 1, 'raw' => 0, 'scaled' => 0], 'duration' => 'PT20S', ])); $statements[] = $statement; return $statements; } /** * Test validate_state method. */ public function test_validate_state(): void { global $DB; $this->resetAfterTest(); /** @var \core_h5p_generator $generator */ $generator = $this->getDataGenerator()->get_plugin_generator('core_h5p'); // Create a valid H5P activity with a valid xAPI state. $course = $this->getDataGenerator()->create_course(); $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->setUser($user); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $coursecontext = \context_course::instance($course->id); $activitycontext = \context_module::instance($activity->cmid); $component = 'mod_h5pactivity'; $filerecord = [ 'contextid' => $activitycontext->id, 'component' => $component, 'filearea' => 'package', 'itemid' => 0, 'filepath' => '/', 'filename' => 'dummy.h5p', 'addxapistate' => true, ]; $generator->generate_h5p_data(false, $filerecord); $handler = handler::create($component); // Change the method visibility for validate_state in order to test it. $method = new \ReflectionMethod(handler::class, 'validate_state'); // The activity id should be numeric. $state = test_helper::create_state(['activity' => item_activity::create_from_id('AA')]); $result = $method->invoke($handler, $state); $this->assertFalse($result); // The activity id should exist. $state = test_helper::create_state(); $result = $method->invoke($handler, $state); $this->assertFalse($result); // The given activity should be H5P activity. $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course]); $state = test_helper::create_state([ 'activity' => item_activity::create_from_id($forum->cmid), ]); $result = $method->invoke($handler, $state); $this->assertFalse($result); // Tracking should be enabled for the H5P activity. $state = test_helper::create_state([ 'activity' => item_activity::create_from_id($activitycontext->id), 'component' => $component, ]); $result = $method->invoke($handler, $state); $this->assertTrue($result); // So, when tracking is disabled, the state won't be considered valid. $activity2 = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course, 'enabletracking' => 0]); $activitycontext2 = \context_module::instance($activity2->cmid); $state = test_helper::create_state([ 'activity' => item_activity::create_from_id($activitycontext2->id), 'component' => $component, ]); $result = $method->invoke($handler, $state); $this->assertFalse($result); // The user should have permission to submit. $studentrole = $DB->get_record('role', array('shortname' => 'student')); assign_capability('mod/h5pactivity:submit', CAP_PROHIBIT, $studentrole->id, $coursecontext->id); // Empty all the caches that may be affected by this change. accesslib_clear_all_caches_for_unit_testing(); \course_modinfo::clear_instance_cache(); $state = test_helper::create_state([ 'activity' => item_activity::create_from_id($activitycontext->id), 'component' => $component, ]); $result = $method->invoke($handler, $state); $this->assertFalse($result); } } PK1B\EU 3 3#tests/output/result/result_test.phpnu[. namespace mod_h5pactivity\output\result; use core_xapi\local\statement\item_result; use mod_h5pactivity\local\manager; /** * Result test class for H5P activity. * * @package mod_h5pactivity * @covers \mod_h5pactivity\output\result * @category test * @copyright 2023 Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class result_test extends \advanced_testcase { /** * Data provider for result export_options test * * @return array[] */ public static function result_data_provider(): array { return [ 'fill-in with case sensitive' => [ 'providedresultdata' => [ 'interactiontype' => 'fill-in', 'description' => '

Fill in the missing words

Meow .... this is a __________

Bark... this is a __________

', 'correctpattern' => '["{case_matters=true}cat[,]dog"]', 'response' => 'Cat[,]dog', 'additionals' => '{"extensions":{"http:\\/\\/h5p.org\\/x-api\\/h5p-local-content-id":31,' . '"https:\\/\\/h5p.org\\/x-api\\/case-sensitivity":true,' . '"https:\\/\\/h5p.org\\/x-api\\/alternatives":[["cat"],["dog"]]},"contextExtensions":{}}', ], 'expecteduseranswers' => [ 'Gap #1 - cat' => (object) ['answer' => 'Cat', 'incorrect' => true], 'Gap #2 - dog' => (object) ['answer' => 'dog', 'correct' => true], ], ], 'fill-in with case insensitive' => [ 'providedresultdata' => [ 'interactiontype' => 'fill-in', 'description' => '

Fill in the missing words

Meow .... this is a __________

Bark... this is a __________

', 'correctpattern' => '["{case_matters=false}cat[,]dog"]', 'response' => 'Cat[,]dog', 'additionals' => '{"extensions":{"http:\\/\\/h5p.org\\/x-api\\/h5p-local-content-id":31,' . '"https:\\/\\/h5p.org\\/x-api\\/case-sensitivity":false,' . '"https:\\/\\/h5p.org\\/x-api\\/alternatives":[["cat"],["dog"]]},"contextExtensions":{}}', ], 'expecteduseranswers' => [ 'Gap #1 - cat' => (object) ['answer' => 'Cat', 'correct' => true], 'Gap #2 - dog' => (object) ['answer' => 'dog', 'correct' => true], ], ], 'drag and drop' => [ 'providedresultdata' => [ 'interactiontype' => 'matching', 'description' => 'Drag and Drop Test', 'correctpattern' => '["0[.]0[,]0[.]2[,]1[.]1[,]1[.]0"]', 'response' => '0[.]0[,]1[.]1[,]0[.]2[,]0[.]3', 'additionals' => '{"source":[{"id":"0","description":{"en-US":"Answer 1 (DZ1 and DZ2)\n"}},' . '{"id":"1","description":{"en-US":"Anwser 2 (DZ2)\n"}},' . '{"id":"2","description":{"en-US":"Anwser 3 (DZ1)\n"}},' . '{"id":"3","description":{"en-US":"Anwser 4 (neither)\n"}}],' . '"target":[{"id":"0","description":{"en-US":"Dropzone 1\n"}},' . '{"id":"1","description":{"en-US":"Dropzone 2\n"}}],' . '"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":41,' . '"http:\/\/h5p.org\/x-api\/h5p-subContentId":"59590246-f16e-4855-8dd6-c80e892ef96b"},' . '"contextExtensions":{}}', ], 'expecteduseranswers' => [ 'Answer 1 (DZ1 and DZ2) - Dropzone 1, Dropzone 2' => (object) ['answer' => 'Dropzone 1', 'correct' => true], 'Anwser 2 (DZ2) - Dropzone 2' => (object) ['answer' => 'Dropzone 2', 'correct' => true], 'Anwser 3 (DZ1) - Dropzone 1' => (object) ['answer' => 'Dropzone 1', 'correct' => true], 'Anwser 4 (neither) - ' => (object) ['answer' => 'Dropzone 1', 'incorrect' => true], ], ], 'drag and drop with no answer' => [ 'providedresultdata' => [ 'interactiontype' => 'matching', 'description' => 'Drag and Drop Test', 'correctpattern' => '["0[.]0[,]0[.]2[,]1[.]1[,]1[.]0"]', 'response' => '1[.]0[,]1[.]1', 'additionals' => '{"source":[{"id":"0","description":{"en-US":"Answer 1 (DZ1 and DZ2)\n"}},' . '{"id":"1","description":{"en-US":"Anwser 2 (DZ2)\n"}},' . '{"id":"2","description":{"en-US":"Anwser 3 (DZ1)\n"}},' . '{"id":"3","description":{"en-US":"Anwser 4 (neither)\n"}}],' . '"target":[{"id":"0","description":{"en-US":"Dropzone 1\n"}},' . '{"id":"1","description":{"en-US":"Dropzone 2\n"}}],' . '"extensions":{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":41,' . '"http:\/\/h5p.org\/x-api\/h5p-subContentId":"59590246-f16e-4855-8dd6-c80e892ef96b"},' . '"contextExtensions":{}}', ], 'expecteduseranswers' => [ 'Answer 1 (DZ1 and DZ2) - Dropzone 1, Dropzone 2' => (object) ['answer' => 'Dropzone 2', 'correct' => true], 'Anwser 2 (DZ2) - Dropzone 2' => (object) ['answer' => 'Dropzone 2', 'correct' => true], 'Anwser 3 (DZ1) - Dropzone 1' => (object) ['answer' => 'None', 'incorrect' => true], 'Anwser 4 (neither) - ' => (object) ['answer' => 'None', 'correct' => true], ], ], 'sort the paragraph text' => [ 'providedresultdata' => [ 'interactiontype' => 'sequencing', 'description' => 'SortParagraphs', 'correctpattern' => '["0[,]1[,]2[,]3[,]4"]', 'response' => '0[,]1[,]3[,]2[,]4', 'additionals' => '{"choices":[{"id":0,"description":{"en":"First I wake up at 7.30 am\\n",' . '"en-US":"First I wake up at 7.30 am\\n"}}' . ',{"id":1,"description":{"en":"Next I get dressed\\n","en-US":"Next I get dressed\\n"}},' . '{"id":2,"description":{"en":"Afterward I have breakfast\\n","en-US":"Afterward I have breakfast\\n"}},' . '{"id":3,"description":{"en":"I brush my teeth\\n","en-US":"I brush my teeth\\n"}},' . '{"id":4,"description":{"en":"Finally I go school\\n","en-US":"Finally I go school\\n"}}],' . '"extensions":{"http:\\/\\/h5p.org\\/x-api\\/h5p-local-content-id":39,' . '"https:\\/\\/h5p.org\\/x-api\\/duplicates-interchangeable":1},"contextExtensions":{}}', ], 'expecteduseranswers' => [ '#1 - First I wake up at 7.30 am' => (object) ['answer' => 'Correct answer', 'pass' => true], '#2 - Next I get dressed' => (object) ['answer' => 'Correct answer', 'pass' => true], '#3 - Afterward I have breakfast' => (object) ['answer' => 'Incorrect answer', 'fail' => true], '#4 - I brush my teeth' => (object) ['answer' => 'Incorrect answer', 'fail' => true], '#5 - Finally I go school' => (object) ['answer' => 'Correct answer', 'pass' => true], ], ], 'sequencing images' => [ 'providedresultdata' => [ 'interactiontype' => 'sequencing', 'description' => 'Order the planets from smallest to largest', 'correctpattern' => '["item_3[,]item_0[,]item_7[,]item_1[,]item_5[,]item_2[,]item_4[,]item_6"]', 'response' => 'item_0[,]item_1[,]item_3[,]item_2[,]item_5[,]item_4[,]item_6[,]item_7', 'additionals' => '{"choices":[{"id":"item_3","description":{"en-US":"Mercury"}},' . '{"id":"item_0","description":{"en-US":"Mars"}},{"id":"item_7","description":{"en-US":"Earth"}},' . '{"id":"item_1","description":{"en-US":"Venus"}},{"id":"item_5","description":{"en-US":"Uranus"}},' . '{"id":"item_2","description":{"en-US":"Neptune"}},{"id":"item_4","description":{"en-US":"Saturn"}},' . '{"id":"item_6","description":{"en-US":"Jupiter"}}],"extensions":' . '{"http:\/\/h5p.org\/x-api\/h5p-local-content-id":43},"contextExtensions":{}}', ], 'expecteduseranswers' => [ '#1 - Mercury' => (object) ['answer' => 'Correct answer', 'pass' => true], '#2 - Mars' => (object) ['answer' => 'Correct answer', 'pass' => true], '#3 - Earth' => (object) ['answer' => 'Incorrect answer', 'fail' => true], '#4 - Venus' => (object) ['answer' => 'Incorrect answer', 'fail' => true], '#5 - Uranus' => (object) ['answer' => 'Incorrect answer', 'fail' => true], '#6 - Neptune' => (object) ['answer' => 'Incorrect answer', 'fail' => true], '#7 - Saturn' => (object) ['answer' => 'Correct answer', 'pass' => true], '#8 - Jupiter' => (object) ['answer' => 'Correct answer', 'pass' => true], ], ], ]; } /** * Test result export_options * * @param array $providedresultdata * @param array $expecteduseranswers * @return void * @dataProvider result_data_provider * @covers \mod_h5pactivity\output\result::export_options */ public function test_result_options(array $providedresultdata, array $expecteduseranswers): void { $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $manager = manager::create_from_instance($activity); $cm = $manager->get_coursemodule(); $student = $this->getDataGenerator()->create_and_enrol($course, 'student'); $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); $params = ['cmid' => $cm->id, 'userid' => $student->id]; $attempt = $generator->create_content($activity, $params); $resultdata = [ 'attemptid' => $attempt->id, 'subcontent' => '', 'timecreated' => time(), 'completion' => true, 'success' => true, 'score' => (object) ['min' => 0, 'max' => 2, 'raw' => 2, 'scaled' => 1], 'duration' => 'PT25S', ]; $resultdata = array_merge($resultdata, $providedresultdata); $result = item_result::create_from_data((object) $resultdata); $classname = "mod_h5pactivity\\output\\result\\{$providedresultdata['interactiontype']}"; $classname = str_replace('-', '', $classname); $reflectionoutput = new \ReflectionClass($classname); $constructor = $reflectionoutput->getConstructor(); $resultoutput = $reflectionoutput->newInstanceWithoutConstructor(); $constructor->invoke($resultoutput, $result->get_data()); $exportoptions = $reflectionoutput->getMethod('export_options'); $data = $exportoptions->invoke($resultoutput); $useranswersdata = array_map(function($item) { return $item->useranswer ?? null; }, $data); $keys = array_map(function($item) { return $item->description . ' - ' . ($item->correctanswer->answer ?? ''); }, $data); $useranswersdata = array_combine($keys, $useranswersdata); $this->assertEquals($expecteduseranswers, $useranswersdata); } } PK1B\r"tests/event/report_viewed_test.phpnu[. /** * 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 */ final class report_viewed_test 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): void { $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 static 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 ], ]; } } PK1B\U L L 'tests/event/statement_received_test.phpnu[. /** * 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 context_module; /** * 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 */ final class statement_received_test extends advanced_testcase { /** * Test statement_recieved event. */ public function test_statement_received(): void { global $USER; $this->resetAfterTest(); $this->setAdminUser(); // Must be a non-guest user to create h5pactivities. $this->setAdminUser(); // There is no proper API to call to trigger this event, so what we are // doing here is simply making sure that the events returns the right information. $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course->id]); $params = [ 'context' => context_module::instance($activity->cmid), 'objectid' => $activity->id ]; $event = statement_received::create($params); // Triggering and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_h5pactivity\event\statement_received', $event); $this->assertEquals(context_module::instance($activity->cmid), $event->get_context()); $this->assertEquals($activity->id, $event->objectid); $this->assertEventContextNotUsed($event); } } PK1B\nd d 7tests/event/course_module_instance_list_viewed_test.phpnu[. /** * 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 context_course; use context_module; /** * 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 */ final class course_module_instance_list_viewed_test extends advanced_testcase { /** * Test course_module_instance_list_viewed event. */ public function test_course_module_instance_list_viewed(): void { // There is no proper API to call to trigger this event, so what we are // doing here is simply making sure that the events returns the right information. $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $params = [ 'context' => context_course::instance($course->id) ]; $event = course_module_instance_list_viewed::create($params); // Triggering and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_h5pactivity\event\course_module_instance_list_viewed', $event); $this->assertEquals(context_course::instance($course->id), $event->get_context()); $this->assertEventContextNotUsed($event); } } PK1B\N. /** * 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 context_course; use context_module; /** * 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 */ final class course_module_viewed_test extends advanced_testcase { /** * Test course_module_viewed event. */ public function test_course_module_viewed(): void { // There is no proper API to call to trigger this event, so what we are // doing here is simply making sure that the events returns the right information. $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course->id]); $params = [ 'context' => context_module::instance($activity->cmid), 'objectid' => $activity->id ]; $event = course_module_viewed::create($params); // Triggering and capturing the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); // Checking that the event contains the expected values. $this->assertInstanceOf('\mod_h5pactivity\event\course_module_viewed', $event); $this->assertEquals(context_module::instance($activity->cmid), $event->get_context()); $this->assertEquals($activity->id, $event->objectid); $this->assertEventContextNotUsed($event); } } PK1B\W ^7^7tests/generator_test.phpnu[. namespace mod_h5pactivity; use mod_h5pactivity\local\manager; /** * Genarator tests class for mod_h5pactivity. * * @package mod_h5pactivity * @category test * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class generator_test extends \advanced_testcase { /** * Test on H5P activity creation. */ public function test_create_instance(): void { global $DB, $CFG, $USER; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); // Create one activity. $this->assertFalse($DB->record_exists('h5pactivity', ['course' => $course->id])); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $records = $DB->get_records('h5pactivity', ['course' => $course->id], 'id'); $this->assertEquals(15, $activity->displayoptions); $this->assertEquals(1, count($records)); $this->assertTrue(array_key_exists($activity->id, $records)); // Create a second one with different name and dusplay options. $params = [ 'course' => $course->id, 'name' => 'Another h5pactivity', 'displayoptions' => 6, 'enabletracking' => 0, 'grademethod' => manager::GRADELASTATTEMPT, ]; $activity = $this->getDataGenerator()->create_module('h5pactivity', $params); $records = $DB->get_records('h5pactivity', ['course' => $course->id], 'id'); $this->assertEquals(6, $activity->displayoptions); $this->assertEquals(0, $activity->enabletracking); $this->assertEquals(manager::GRADELASTATTEMPT, $activity->grademethod); $this->assertEquals(manager::REVIEWCOMPLETION, $activity->reviewmode); $this->assertEquals(2, count($records)); $this->assertEquals('Another h5pactivity', $records[$activity->id]->name); // Examples of specifying the package file (do not validate anything, just check for exceptions). // 1. As path to the file in filesystem. $params = [ 'course' => $course->id, 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/filltheblanks.h5p' ]; $activity = $this->getDataGenerator()->create_module('h5pactivity', $params); // 2. As file draft area id. $fs = get_file_storage(); $params = [ 'course' => $course->id, 'packagefile' => file_get_unused_draft_itemid() ]; $usercontext = \context_user::instance($USER->id); $filerecord = ['component' => 'user', 'filearea' => 'draft', 'contextid' => $usercontext->id, 'itemid' => $params['packagefile'], 'filename' => 'singlescobasic.zip', 'filepath' => '/']; $filepath = $CFG->dirroot.'/h5p/tests/fixtures/filltheblanks.h5p'; $fs->create_file_from_pathname($filerecord, $filepath); $activity = $this->getDataGenerator()->create_module('h5pactivity', $params); } /** * Test that a new H5P activity cannot be generated without a valid file * other user. */ public function test_create_file_exception(): void { global $CFG; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); // Testing generator exceptions. $params = [ 'course' => $course->id, 'packagefilepath' => $CFG->dirroot.'/h5p/tests/fixtures/wrong_file_.xxx' ]; $this->expectException(\coding_exception::class); $activity = $this->getDataGenerator()->create_module('h5pactivity', $params); } /** * Test to create H5P attempts * * @dataProvider create_attempt_data * * @param array $tracks the attempt tracks objects * @param int $attempts the final registered attempts * @param int $results the final registered attempts results * @param bool $exception if an exception is expected * */ public function test_create_attempt(array $tracks, int $attempts, int $results, bool $exception): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->assertEquals(0, $DB->count_records('h5pactivity_attempts')); $this->assertEquals(0, $DB->count_records('h5pactivity_attempts_results')); if ($exception) { $this->expectException(\Exception::class); } foreach ($tracks as $track) { $attemptinfo = [ 'userid' => $user->id, 'h5pactivityid' => $activity->id, 'attempt' => $track['attempt'], 'interactiontype' => $track['interactiontype'], 'rawscore' => $track['rawscore'], 'maxscore' => $track['maxscore'], 'duration' => $track['duration'], 'completion' => $track['completion'], 'success' => $track['success'], ]; $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); $generator->create_attempt($attemptinfo); $this->assert_attempt_matches_info($attemptinfo); } $this->assertEquals($attempts, $DB->count_records('h5pactivity_attempts')); $this->assertEquals($results, $DB->count_records('h5pactivity_attempts_results')); } /** * Data provider for create attempt test. * * @return array */ public static function create_attempt_data(): array { return [ 'Compound statement' => [ [ [ 'interactiontype' => 'compound', 'attempt' => 1, 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0 ], ], 1, 1, false, ], 'Choice statement' => [ [ [ 'interactiontype' => 'choice', 'attempt' => 1, 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0 ], ], 1, 1, false, ], 'Matching statement' => [ [ [ 'interactiontype' => 'matching', 'attempt' => 1, 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0 ], ], 1, 1, false, ], 'Fill-in statement' => [ [ [ 'interactiontype' => 'fill-in', 'attempt' => 1, 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0 ], ], 1, 1, false, ], 'True-false statement' => [ [ [ 'interactiontype' => 'true-false', 'attempt' => 1, 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0 ], ], 1, 1, false, ], 'Long-fill-in statement' => [ [ [ 'interactiontype' => 'long-fill-in', 'attempt' => 1, 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0 ], ], 1, 1, false, ], 'Sequencing statement' => [ [ [ 'interactiontype' => 'sequencing', 'attempt' => 1, 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0 ], ], 1, 1, false, ], 'Other statement' => [ [ [ 'interactiontype' => 'other', 'attempt' => 1, 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0 ], ], 1, 1, false, ], 'No graded statement' => [ [ [ 'interactiontype' => 'other', 'attempt' => 1, 'rawscore' => 0, 'maxscore' => 0, 'duration' => 1, 'completion' => 1, 'success' => 0 ], ], 1, 1, false, ], 'Invalid statement type' => [ [ [ 'interactiontype' => 'no-valid-statement-type', 'attempt' => 1, 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0 ], ], 0, 0, true, ], 'Adding a second statement to attempt' => [ [ [ 'interactiontype' => 'true-false', 'attempt' => 1, 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0 ], [ 'interactiontype' => 'compound', 'attempt' => 1, 'rawscore' => 3, 'maxscore' => 3, 'duration' => 2, 'completion' => 1, 'success' => 0 ], ], 1, 2, false, ], 'Creating two attempts' => [ [ [ 'interactiontype' => 'compound', 'attempt' => 1, 'rawscore' => 2, 'maxscore' => 2, 'duration' => 1, 'completion' => 1, 'success' => 0 ], [ 'interactiontype' => 'compound', 'attempt' => 2, 'rawscore' => 3, 'maxscore' => 3, 'duration' => 1, 'completion' => 1, 'success' => 0 ], ], 2, 2, false, ], ]; } /** * Insert track into attempt, creating the attempt if necessary. * * @param array $attemptinfo the attempt track information */ private function assert_attempt_matches_info($attemptinfo): void { global $DB; $attempt = $DB->get_record('h5pactivity_attempts', [ 'userid' => $attemptinfo['userid'], 'h5pactivityid' => $attemptinfo['h5pactivityid'], 'attempt' => $attemptinfo['attempt'], ]); $this->assertEquals($attemptinfo['rawscore'], $attempt->rawscore); $this->assertEquals($attemptinfo['maxscore'], $attempt->maxscore); $this->assertEquals($attemptinfo['duration'], $attempt->duration); $this->assertEquals($attemptinfo['completion'], $attempt->completion); $this->assertEquals($attemptinfo['success'], $attempt->success); $track = $DB->get_record('h5pactivity_attempts_results', [ 'attemptid' => $attempt->id, 'interactiontype' => $attemptinfo['interactiontype'], ]); $this->assertEquals($attemptinfo['rawscore'], $track->rawscore); $this->assertEquals($attemptinfo['maxscore'], $track->maxscore); $this->assertEquals($attemptinfo['duration'], $track->duration); $this->assertEquals($attemptinfo['completion'], $track->completion); $this->assertEquals($attemptinfo['success'], $track->success); } /** * Test exceptions when creating an invalid attempt. * * @dataProvider create_attempt_exceptions_data * * @param bool $validmod if the activity id is provided * @param bool $validuser if the user id is provided */ public function test_create_attempt_exceptions(bool $validmod, bool $validuser): void { global $DB; $this->resetAfterTest(); $this->setAdminUser(); $course = $this->getDataGenerator()->create_course(); $activity = $this->getDataGenerator()->create_module('h5pactivity', ['course' => $course]); $user = $this->getDataGenerator()->create_and_enrol($course, 'student'); $this->expectException(\coding_exception::class); $attemptinfo = [ 'attempt' => 1, 'interactiontype' => 'compound', 'rawscore' => 2, 'maxscore' => 1, 'duration' => 1, 'completion' => 1, 'success' => 0, ]; if ($validmod) { $attemptinfo['h5pactivityid'] = $activity->id; } if ($validuser) { $attemptinfo['userid'] = $user->id; } $generator = $this->getDataGenerator()->get_plugin_generator('mod_h5pactivity'); $generator->create_attempt($attemptinfo); } /** * Data provider for data request creation tests. * * @return array */ public static function create_attempt_exceptions_data(): array { return [ 'Invalid user' => [true, false], 'Invalid activity' => [false, true], 'Invalid user and activity' => [false, false], ]; } } PK1B\O  .tests/behat/h5pactivity_grade_settings.featurenu[@mod @mod_h5pactivity @core_h5p Feature: Teacher can control h5p activity grading setting In order to set h5p activity grade As a teacher I need to be able to control h5p activity grading setting Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | student3 | Student | 3 | student3@example.com | | student4 | Student | 4 | student4@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | | student4 | C1 | student | @javascript Scenario: Verify that invalid grades given to students are not saved Given the following "activities" exist: | activity | name | course | | h5pactivity | H5P point | C1 | # Activity grade settings are not saved using generators so manual setting is necessary. And I am on the "H5P point" "h5pactivity activity editing" page logged in as teacher1 And I set the following fields to these values: | grade[modgrade_type] | Point | | grade[modgrade_point] | 10 | And I press "Save and return to course" And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on When I give the grade "50" to the user "Student 1" for the grade item "H5P point" And I press "Save changes" And I turn editing mode off # Confirm that grades are not saved when grade entered is > maximum grade. Then the following should exist in the "user-grades" table: | -1- | -2- | -3- | | Student 1 | student1@example.com | - | | Student 2 | student2@example.com | - | | Student 3 | student3@example.com | - | | Student 4 | student4@example.com | - | @javascript Scenario: Verify that valid grades given to students are saved Given the following "activities" exist: | activity | name | course | | h5pactivity | H5P point | C1 | # Activity grade settings are not saved using generators so manual setting is necessary. And I am on the "H5P point" "h5pactivity activity editing" page logged in as teacher1 And I set the following fields to these values: | grade[modgrade_type] | Point | | grade[modgrade_point] | 10 | And I press "Save and return to course" And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on When I give the grade "10" to the user "Student 1" for the grade item "H5P point" And I give the grade "5" to the user "Student 2" for the grade item "H5P point" And I give the grade "0" to the user "Student 3" for the grade item "H5P point" And I press "Save changes" And I turn editing mode off # Confirm that corresponding grades are stored for each student. And the following should exist in the "user-grades" table: | -1- | -2- | -3- | | Student 1 | student1@example.com | 10 | | Student 2 | student2@example.com | 5 | | Student 3 | student3@example.com | 0 | | Student 4 | student4@example.com | - | @javascript Scenario: Verify that scales given to students are saved Given the following "activities" exist: | activity | name | course | | h5pactivity | H5P scale | C1 | # Activity grade settings are not saved using generators so manual setting is necessary. And I am on the "H5P scale" "h5pactivity activity editing" page logged in as teacher1 And I set the following fields to these values: | grade[modgrade_type] | Scale | | grade[modgrade_scale] | Default competence scale | And I press "Save and return to course" And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "Not yet competent" to the user "Student 1" for the grade item "H5P scale" And I give the grade "Competent" to the user "Student 2" for the grade item "H5P scale" And I give the grade "Competent" to the user "Student 4" for the grade item "H5P scale" When I press "Save changes" And I turn editing mode off # Confirm that scale set for student is successfully saved. Then the following should exist in the "user-grades" table: | -1- | -2- | -3- | | Student 1 | student1@example.com | Not yet competent | | Student 2 | student2@example.com | Competent | | Student 3 | student3@example.com | - | | Student 4 | student4@example.com | Competent | PK1B\'5tests/behat/h5pactivity_completion_pass_grade.featurenu[@mod @mod_h5pactivity @core_h5p @_switch_iframe @core_completion Feature: Completion of H5P activity by achieving a passing grade In order to complete an H5P activity As a student I need to be able to complete the h5p activity to receive a grade Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Vinnie | Student1 | student1@example.com | | student2 | Vinnie | Student2 | student2@example.com | | student3 | Vinnie | Student3 | student3@example.com | | teacher1 | Darrell | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Music history | | completion | 2 | | completionview | 1 | | completionusegrade | 1 | | completionpassgrade | 1 | | gradepass | 25 | | packagefilepath | h5p/tests/fixtures/filltheblanks.h5p | Scenario: Verify that the h5p completion conditions are displayed to teachers Given I change window size to "large" And I am on the "Music history" "h5pactivity activity" page logged in as teacher1 Then "Music history" should have the "View" completion condition And "Music history" should have the "Receive a grade" completion condition And "Music history" should have the "Receive a passing grade" completion condition @javascript Scenario: Verify that students can complete an H5P activity by achieving a passing grade # Student 1 attempt the H5P and fills the blanks with the wrong answers... needs more geography lessons! Given I am on the "Music history" "h5pactivity activity" page logged in as student1 And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Of which countries are Berlin, Washington, Beijing, Canberra and Brasilia the capitals?" And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Rio de Janeiro" And I set the field with xpath "//input[contains(@aria-label,\"Blank input 2 of 4\")]" to "New York" And I set the field with xpath "//input[contains(@aria-label,\"Blank input 3 of 4\")]" to "Hamburg" And I set the field with xpath "//input[contains(@aria-label,\"Blank input 4 of 4\")]" to "Sydney" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I switch to the main frame And I reload the page And the "View" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "done" And the "Receive a passing grade" completion condition of "Music history" is displayed as "failed" # Student 2 attempts the H5P and fills the blanks with the correct answers. And I am on the "Music history" "h5pactivity activity" page logged in as student2 And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Of which countries are Berlin, Washington, Beijing, Canberra and Brasilia the capitals?" And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Brasilia" And I set the field with xpath "//input[contains(@aria-label,\"Blank input 2 of 4\")]" to "Washington" And I set the field with xpath "//input[contains(@aria-label,\"Blank input 3 of 4\")]" to "Berlin" And I set the field with xpath "//input[contains(@aria-label,\"Blank input 4 of 4\")]" to "Canberra" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I switch to the main frame And I reload the page And the "View" completion condition of "Music history" is displayed as "done" And the "Receive a grade" completion condition of "Music history" is displayed as "done" And the "Receive a passing grade" completion condition of "Music history" is displayed as "done" # Teacher confirms which students have completed the H5P activity. When I am on the "Course 1" "course" page logged in as "teacher1" Then "Vinnie Student1" user has completed "Music history" activity And "Vinnie Student2" user has completed "Music history" activity And "Vinnie Student3" user has not completed "Music history" activity PK1B\/+F$tests/behat/contentbank_link.featurenu[@mod @mod_h5pactivity @core_h5p @_switch_iframe Feature: Content bank link in the activity settings form In order to have direct access to the Content bank As a teacher I need to see a Content bank link in the activity settings Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | filltheblanks.h5p | /h5p/tests/fixtures/filltheblanks.h5p | @javascript Scenario: The content bank link should go to the course Content bank When I log in as "teacher1" And I add an h5pactivity activity to course "Course 1" section "1" Then I should see "Use the content bank (opens in new window) to manage your H5P files" And I click on "content bank (opens in new window)" "link" in the "General" "fieldset" And I switch to a second window And I should see "Content bank" in the "page-content" "region" And I should see "filltheblanks.h5p" in the "page-content" "region" And I close all opened windows @javascript Scenario: Content bank is not displayed if the user don't have access to the content bank Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/contentbank:access | Prevent | editingteacher | Course | C1 | When I log in as "teacher1" And I add an h5pactivity activity to course "Course 1" section "1" Then I should not see "Use the content Bank (opens in new window) to manage your H5P files" @javascript Scenario: A different message should be displayed if the package file is a link to the content bank file Given I log in as "admin" And I add an h5pactivity activity to course "Course 1" section "1" And I set the following fields to these values: | Name | H5P package added with link to content bank | | Description | Description | And I click on "Add..." "button" in the "Package file" "form_row" And I select "Content bank" repository in file picker And I click on "filltheblanks.h5p" "file" in repository content area And I click on "Link to the file" "radio" And I click on "Select this file" "button" And I click on "Save and display" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Of which countries are Berlin, Washington, Beijing, Canberra and Brasilia the capitals?" And I switch to the main frame When I navigate to "Settings" in current page administration Then I should not see "Use the content Bank (opens in new window) to manage your H5P files" And I should see "Access the H5P file in the content bank (opens in a new window)." And I follow "Access the H5P file in the content bank" @javascript Scenario: The content bank link should go to the course Content bank if the file is a copy to a content bank file Given I log in as "admin" And I add an h5pactivity activity to course "Course 1" section "1" And I set the following fields to these values: | Name | H5P package added with link to content bank | | Description | Description | And I click on "Add..." "button" in the "Package file" "form_row" And I select "Content bank" repository in file picker And I click on "filltheblanks.h5p" "file" in repository content area And I click on "Make a copy of the file" "radio" And I click on "Select this file" "button" And I click on "Save and display" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Of which countries are Berlin," And I switch to the main frame When I navigate to "Settings" in current page administration Then I should see "Use the content bank (opens in new window) to manage your H5P files" @javascript Scenario: The content bank link should go to the course Content bank if the file is referenced but to another repository Given the following "user private file" exists: | user | admin | | filepath | h5p/tests/fixtures/guess-the-answer.h5p | When I log in as "admin" And I add an h5pactivity activity to course "Course 1" section "1" And I set the following fields to these values: | Name | H5P package added with link to content bank | | Description | Description | And I click on "Add..." "button" in the "Package file" "form_row" And I select "Private files" repository in file picker And I click on "guess-the-answer.h5p" "file" in repository content area And I click on "Link to the file" "radio" And I click on "Select this file" "button" And I click on "Save and display" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Which fruit is this?" And I switch to the main frame And I navigate to "Settings" in current page administration Then I should see "Use the content bank (opens in new window) to manage your H5P files" PK1B\^ I !tests/behat/result_fillin.featurenu[@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript Feature: View fill the blanks attempt report In order to let users to review a fill the blanks attempt As a user I need to view fill in interactions in the report Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And the following config values are set as admin: # No HTML should appear even with formatstringstriptags disabled. | formatstringstriptags | 0 | And the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Awesome H5P package | | grademethod | 2 | | packagefilepath | h5p/tests/fixtures/filltheblanks.h5p | Scenario: View attempt in a fill the blanks content Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 # Do an attempt. And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Brigadoon" And I set the field with xpath "//input[contains(@aria-label,\"Blank input 2 of 4\")]" to "Emerald city" And I set the field with xpath "//input[contains(@aria-label,\"Blank input 3 of 4\")]" to "Narnia" And I set the field with xpath "//input[contains(@aria-label,\"Blank input 4 of 4\")]" to "Canberra" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I switch to the main frame And I reload the page # Check attempt. When I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "Of which countries are Berlin, Washington, Beijing, Canberra and Brasilia the capitals?" And I should see "Brigadoon" in the "Brasilia" "table_row" And "Your answer is incorrect" "icon" should exist in the "Brasilia" "table_row" And I should see "Emerald city" in the "Washington" "table_row" And I should see "Narnia" in the "Berlin" "table_row" And "Your answer is incorrect" "icon" should exist in the "Berlin" "table_row" And "Your answer is correct" "icon" should exist in the "Canberra" "table_row" And I should not see "

" PK1B\< ,tests/behat/h5pactivity_reset_grades.featurenu[@mod @mod_h5pactivity @core_h5p Feature: Teacher can reset H5P activity grades As a teacher, I should be able to reset H5P activity grades Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | One | teacher1@example.com | | student1 | First | Student | student1@example.com | | student2 | Second | Student | student2@example.com | | student3 | Third | Student | student3@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | And the following "activities" exist: | activity | course | name | grade[modgrade_type] | grade[modgrade_point] | | h5pactivity | C1 | H5P Activity | point | 10 | @javascript Scenario:Teacher can reset H5P activity grades Given I am on the "Course 1" course page logged in as teacher1 And I navigate to "View > Grader report" in the course gradebook And I turn editing mode on And I give the grade "7" to the user "First Student" for the grade item "H5P Activity" And I give the grade "5" to the user "Second Student" for the grade item "H5P Activity" And I give the grade "0" to the user "Third Student" for the grade item "H5P Activity" And I press "Save changes" # Confirm that grade was sucessfully saved And I turn editing mode off And I should see "7.00" in the "First Student" "table_row" And I should see "5.00" in the "Second Student" "table_row" And I should see "0.00" in the "Third Student" "table_row" When I am on the "Course 1" "reset" page And I press "Deselect all" # Check `Delete all grades` in course reset page to reset grades And I click on "All grades" "checkbox" And I press "Reset course" And I click on "Reset course" "button" in the "Reset course?" "dialogue" Then I should see "Done" in the "Gradebook" "table_row" And I press "Continue" And I run all adhoc tasks # Confirm that previously saved grades are gone And I navigate to "View > Grader report" in the course gradebook And I should not see "7.00" in the "First Student" "table_row" And I should not see "5.00" in the "Second Student" "table_row" And I should not see "0.00" in the "Third Student" "table_row" PK1B\jtests/behat/locking.featurenu[@mod @mod_h5pactivity @core_h5p Feature: Add H5P activity context locking In order to let users access a H5P attempts As a user I need to access attempts reports even if no more users can submit attempts Background: Given the following config values are set as admin: | contextlocking | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | # This test is only about reporting, we don't need to specify any valid H5P file for it. And the following "activity" exists: | activity | h5pactivity | | name | H5P package | | intro | Test H5P description | | course | C1 | | idnumber | h5ppackage | And the following "mod_h5pactivity > attempt" exists: | user | student1 | | h5pactivity | H5P package | | attempt | 1 | | interactiontype | compound | | rawscore | 2 | | maxscore | 2 | | duration | 4 | | completion | 1 | | success | 1 | Scenario: Access participants report on a freeze context Given the "h5ppackage" "Activity module" is context frozen And I am on the "H5P package" "h5pactivity activity" page logged in as admin When I navigate to "Attempts report" in current page administration Then I should see "Student 1" And I should see "View (1)" in the "Student 1" "table_row" And I should see "Student 2" And I should not see "Teacher 1" Scenario: Access own attempts on a freeze context Given the "h5ppackage" "Activity module" is context frozen And I am on the "H5P package" "h5pactivity activity" page logged in as student1 When I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "Attempt #1: Student 1" And I should see "This attempt is completed" Scenario: Access participants report without any user with submit capability Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | mod/h5pactivity:submit | Prohibit | student | System | | And I am on the "H5P package" "h5pactivity activity" page logged in as admin When I navigate to "Attempts report" in current page administration Then I should see "Student 1" And I should see "View (1)" in the "Student 1" "table_row" And I should see "Student 2" And I should not see "Teacher 1" Scenario: Access participant report to list students with submit capability but no view one Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | mod/h5pactivity:view | Prohibit | student | System | | And I am on the "H5P package" "h5pactivity activity" page logged in as admin When I navigate to "Attempts report" in current page administration Then I should see "No participants to display" Scenario: Access participant report but with no users with view or submit capability Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | mod/h5pactivity:submit | Prohibit | student | System | | | mod/h5pactivity:view | Prohibit | student | System | | And I am on the "H5P package" "h5pactivity activity" page logged in as admin When I navigate to "Attempts report" in current page administration Then I should see "No participants to display" Scenario: Access participant report in a hidden activity Given I log in as "admin" And I am on "Course 1" course homepage with editing mode on And I click on "Hide" "link" in the "H5P package" activity When I am on the "H5P package" "h5pactivity activity" page And I navigate to "Attempts report" in current page administration Then I should see "Student 1" And I should see "View (1)" And I should see "Student 2" And I should not see "Teacher 1" PK1B\rE6? ? #tests/behat/define_settings.featurenu[@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript Feature: Set up attempt grading options into H5P activity In order to use automatic grading in H5P activity As a teacher I need to be able to configure the attempt settings Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/h5p:updatelibraries | Allow | editingteacher | System | | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on And I add an h5pactivity activity to course "Course 1" section "1" Scenario: Default values should have tracking and grading When the field "Type" matches value "Point" Then the "Grading method" "select" should be enabled Scenario: Scale grading should not have a grading method. When I set the following fields to these values: | Name | Awesome H5P package | | Type | Scale | Then the "Grading method" "select" should be disabled Scenario: None grading should not have a grading method. When I set the following fields to these values: | Name | Awesome H5P package | | Type | None | Then the "Grading method" "select" should be disabled Scenario: Point grading should have a grading method. When I set the following fields to these values: | Name | Awesome H5P package | | Type | Point | Then the "Grading method" "select" should be enabled Scenario: Disable tracking should make grading method disappear. When I set the following fields to these values: | Name | Awesome H5P package | | Enable attempt tracking | No | Then "Grading method" "field" should not be visible PK1B\^ ^ *tests/behat/h5pactivity_deployment.featurenu[@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe Feature: Undeployed H5P activities packages should be available only to any user that can deploy packages. Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | teacher2 | Teacher | 2 | teacher2@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | editingteacher | | student1 | C1 | student | # Make sure that the teacher2 can update libraries so it show the right info when. And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/h5p:updatelibraries | Allow | editingteacher | System | | # Now create the activity as teacher1. And the following "activities" exist: | activity | course | name | username | packagefilepath | | h5pactivity | C1 | Music history | teacher1 | h5p/tests/fixtures/filltheblanks.h5p | And I log in as "admin" And I navigate to "Users > Accounts > Browse list of users" in site administration And I press "Delete" action in the "Teacher 1" report row And I click on "Delete" "button" in the "Delete user" "dialogue" And I should see "Deleted user Teacher 1" @javascript Scenario: In an H5P activity, as student I should not be able to deploy the package if not deployed by the teacher beforehand. Then if a second teacher deploys the package, I can see it. Given I am on the "Music history" "h5pactivity activity" page logged in as student1 And I switch to "h5p-player" class iframe And "This file can't be displayed because it has been uploaded by a user without the required capability to deploy H5P content" "text" should exist And I switch to the main frame And I log out # Then teacher2 will be allowed to deploy the package. And I am on the "Music history" "h5pactivity activity" page logged in as teacher2 And I switch to "h5p-player" class iframe When I switch to "h5p-iframe" class iframe Then I should see "Of which countries are Berlin" And I switch to the main frame And I log out # Now student1 should be able to see the package. And I am on the "Music history" "h5pactivity activity" page logged in as student1 And I switch to "h5p-player" class iframe When I switch to "h5p-iframe" class iframe Then I should see "Of which countries are Berlin" PK1B\bزtests/behat/reports.featurenu[@mod @mod_h5pactivity @core_h5p Feature: Reporting information in the h5p activity In order to let teachers view a report on the attempts made in the h5p activity As a teacher I can access the report page and see the attempts made by the students Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | # This test is only about reporting, we don't need to specify any valid H5P file for it. And the following "activity" exists: | activity | h5pactivity | | name | H5P package | | intro | Test H5P description | | course | C1 | | idnumber | h5ppackage | And the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 | | student2 | H5P package | 1 | choice | 2 | 2 | 4 | 1 | 1 | | student1 | H5P package | 2 | compound | 2 | 2 | 4 | 1 | 1 | Scenario: Access the report page and check the label for the attempts and attempt headers Given I am on the "H5P package" "h5pactivity activity" page logged in as teacher1 When I navigate to "Attempts report" in current page administration Then I should see "View (2)" in the "Student 1" "table_row" And I should see "View (1)" in the "Student 2" "table_row" And I should see "Attempts (3)" in the "table" "table" PK1B\v;;(tests/behat/display_result_types.featurenu[@mod @mod_h5pactivity @core_h5p @core_xapi Feature: Report different types of interactions. In order to let users to review attempts As a user I need to view all valid interactions in the report Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | # This test is only about reporting, we don't need to specify any valid H5P file for it. And the following "activities" exist: | activity | name | intro | course | idnumber | | h5pactivity | H5P package | Test H5P description | C1 | h5ppackage | Scenario: General success attempt information Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "2 out of 2" And I should see "Pass" And I should see "4 seconds" And I should see "This attempt is completed" Scenario: General failed attempt statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "0 out of 2" And I should see "Fail" And I should see "4 seconds" And I should see "This attempt is completed" Scenario: View a success choice statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | choice | 2 | 2 | 1 | 1 | 1 | | student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "Select the correct answers" And "Correct answer" "icon" should exist in the "This is also a correct answer" "table_row" And I should see "Score: 2 out of 2" Scenario: View a failed choice statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | choice | 0 | 2 | 1 | 1 | 0 | | student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "Select the correct answers" And "Incorrect answer" "icon" should exist in the "Another wrong answer" "table_row" And I should see "Score: 0 out of 2" Scenario: View a success matching statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | matching | 2 | 2 | 1 | 1 | 1 | | student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "Drag and Drop example 1" And "Your answer is correct" "icon" should exist in the "Drop item A" "table_row" And I should see "Score: 2 out of 2" Scenario: View a failed matching statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | matching | 0 | 2 | 1 | 1 | 1 | | student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 1 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "Drag and Drop example 1" And "Your answer is incorrect" "icon" should exist in the "Drop item A" "table_row" And I should see "Score: 0 out of 2" Scenario: View a success true-false statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | true-false | 2 | 2 | 1 | 1 | 1 | | student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "The correct answer is true" And "Correct answer" "icon" should exist in the "True" "table_row" And I should see "Score: 2 out of 2" Scenario: View a failed true-false statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | true-false | 0 | 2 | 1 | 1 | 0 | | student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "The correct answer is true" And "Incorrect answer" "icon" should exist in the "False" "table_row" And I should see "Score: 0 out of 2" Scenario: View a success fill-in statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | fill-in | 2 | 2 | 1 | 1 | 1 | | student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "This an example of missing word text" And "Your answer is correct" "icon" should exist in the "Gap #1" "table_row" And I should see "first" in the "Gap #1" "table_row" And "Your answer is correct" "icon" should exist in the "Gap #2" "table_row" And I should see "second" in the "Gap #2" "table_row" And I should see "Score: 2 out of 2" Scenario: View a failed fill-in statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | fill-in | 0 | 2 | 1 | 1 | 0 | | student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "This an example of missing word text" And "Your answer is incorrect" "icon" should exist in the "Gap #1" "table_row" And I should see "first" in the "Gap #1" "table_row" And I should see "something" in the "Gap #1" "table_row" And "Your answer is incorrect" "icon" should exist in the "Gap #2" "table_row" And I should see "second" in the "Gap #2" "table_row" And I should see "else" in the "Gap #2" "table_row" And I should see "Score: 0 out of 2" Scenario: View a success long-fill-in statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | long-fill-in | 2 | 2 | 1 | 1 | 1 | | student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "Please describe the novel The Hobbit" And I should see "The Hobbit is book" # Fill-in does not have a partial scope indicador, we only check the general one. And I should see "2 out of 2" Scenario: View a failed long-fill-in statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | long-fill-in | 0 | 2 | 1 | 1 | 0 | | student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "Please describe the novel The Hobbit" And I should see "Who cares?" # Fill-in does not have a partial scope indicador, we only check the general one. And I should see "0 out of 2" # The current H5P implementation does not send a complete sequencing interaction statement # we check only the warning and the final result. Scenario: View a success sequencing statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | sequencing | 2 | 2 | 1 | 1 | 1 | | student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "This interaction (sequencing) does not provide tracking information" # Sequencing does not have a partial scope indicador, we only check the general one. And I should see "2 out of 2" # The current H5P implementation does not send a complete sequencing interaction statement # we check only the warning and the final result. Scenario: View a failed sequencing statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | sequencing | 2 | 2 | 1 | 1 | 0 | | student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "This interaction (sequencing) does not provide tracking information" # Sequencing does not have a partial scope indicador, we only check the general one. And I should see "0 out of 2" # The current H5P implementation does not send a complete sequencing interaction statement # we check only the warning and the final result. Scenario: View a success other statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | other | 2 | 2 | 1 | 1 | 1 | | student1 | H5P package | 1 | compound | 2 | 2 | 4 | 1 | 1 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "This interaction (other) does not provide tracking information" # Other does not have a partial scope indicador, we only check the general one. And I should see "2 out of 2" # The current H5P implementation does not send a complete sequencing interaction statement # we check only the warning and the final result. Scenario: View a failed other statement Given the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | other | 2 | 2 | 1 | 1 | 0 | | student1 | H5P package | 1 | compound | 0 | 2 | 4 | 1 | 0 | When I am on the "H5P package" "h5pactivity activity" page logged in as student1 And I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "This interaction (other) does not provide tracking information" # Other does not have a partial scope indicador, we only check the general one. And I should see "0 out of 2" PK1B\E *tests/behat/h5pactivity_completion.featurenu[@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript @core_completion Feature: View activity completion information in the h5p activity In order to have visibility of h5p completion requirements As a student I need to be able to view my h5p completion progress Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Vinnie | Student1 | student1@example.com | | teacher1 | Darrell | Teacher1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | enablecompletion | showcompletionconditions | | Course 1 | C1 | 1 | 1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | teacher1 | C1 | editingteacher | And the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Music history | | completion | 2 | | completionview | 1 | | completionusegrade | 1 | | packagefilepath | h5p/tests/fixtures/filltheblanks.h5p | Scenario: A student can manually mark the h5p activity as done but a teacher cannot Given I am on the "Music history" "h5pactivity activity editing" page logged in as teacher1 And I expand all fieldsets And I set the field "Students must manually mark the activity as done" to "1" And I press "Save and display" # Teacher view. And I am on the "Music history" "h5pactivity activity" page And the manual completion button for "Music history" should be disabled # Student view. When I am on the "Music history" "h5pactivity activity" page logged in as student1 Then the manual completion button of "Music history" is displayed as "Mark as done" And I toggle the manual completion state of "Music history" And the manual completion button of "Music history" is displayed as "Done" PK1B\!+0tests/behat/duplicate_delete_h5pactivity.featurenu[@mod @mod_h5pactivity Feature: Duplicate and delete a h5pactivity In order to quickly create and delete h5p activities As a teacher I need to duplicate or delete h5pactivity inside the same course Background: Given the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | Scenario: Duplicate and delete h5p activity Given the following "activities" exist: | activity | course | name | packagefilepath | | h5pactivity | C1 | H5P Activity 1 | h5p/tests/fixtures/filltheblanks.h5p | And I am on the "H5P Activity 1" "h5pactivity activity" page logged in as teacher1 # Initial confirmation that no error occurs when viewing h5p activity And I should see "You are in preview mode." And I am on "Course 1" course homepage with editing mode on # Duplicate the h5p activity When I duplicate "H5P Activity 1" activity # Confirm that h5p activity was duplicated successfully Then I should see "H5P Activity 1 (copy)" And I am on the "H5P Activity 1 (copy)" "h5pactivity activity" page # Confirm there are no errors when viewing duplicate h5p activity And I should see "You are in preview mode." And I am on the "Course 1" course page # Delete the duplicate h5p activity And I delete "H5P Activity 1 (copy)" activity # Confirm duplicate was deleted successfully And I should not see "H5P Activity 1 (copy)" And I am on the "H5P Activity 1" "h5pactivity activity" page # Confirm there are no errors on the original h5p activity after deleting the duplicate And I should see "You are in preview mode." PK1B\ff#tests/behat/recent_activity.featurenu[@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript Feature: Users can see the H5P recent activity from the recent activity block In order to quickly see the updates from H5P activity in my course As a user I need to be able to see the recent activity in the recent activity block Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/h5p:updatelibraries | Allow | editingteacher | System | | And the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Awesome H5P package | | packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p | And the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | | recent_activity | Course | C1 | course-view-* | side-pre | And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 # The H5P content needs some time to be displayed (so better to wait for 1 second to avoid random errors). And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I switch to the main frame And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student2 # The H5P content needs some time to be displayed (so better to wait for 1 second to avoid random errors). And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I click on "Correct one" "text" in the ".h5p-question-content" "css_element" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I switch to the main frame Scenario: Student see only his own activity Given I am on the "Course 1" course page logged in as student1 And I should see "H5P submitted:" in the "Recent activity" "block" And I should see "Student 1" in the "Recent activity" "block" And I should not see "Grade:" in the "Recent activity" "block" And I should not see "Student 2" in the "Recent activity" "block" When I click on "Full report of recent activity" "link" Then I should see "H5P Awesome H5P package" And I should see "Student 1 - " And I should not see "Grade:" And I should not see "Student 2 - " Scenario: Teacher see each student activity Given I am on the "Course 1" course page logged in as teacher1 And I should see "H5P submitted:" in the "Recent activity" "block" And I should see "Student 1" in the "Recent activity" "block" And I should not see "Grade:" in the "Recent activity" "block" And I should see "Student 2" in the "Recent activity" "block" When I click on "Full report of recent activity" "link" Then I should see "H5P Awesome H5P package" And I should see "Student 1 - " And I should see "Grade:" And I should see "Student 2 - " PK1B\g&-""&tests/behat/save_content_state.featurenu[@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript Feature: Users can save the current state of an H5P activity In order to continue an H5P activity where I left As a user I need to be able to save the current state Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "course" exists: | fullname | Course 1 | | shortname | C1 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C1 | editingteacher | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/h5p:updatelibraries | Allow | editingteacher | System | | And the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Awesome H5P package | | packagefilepath | h5p/tests/fixtures/filltheblanks.h5p | Scenario: Content state is not saved when enablesavestate is disabled Given the following config values are set as admin: | enablesavestate | 0 | mod_h5pactivity| And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia" And I switch to the main frame And I am on the "Course 1" course page When I am on the "Awesome H5P package" "h5pactivity activity" page And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia" Scenario: Content state is saved when enablesavestate is enabled Given the following config values are set as admin: | enablesavestate | 1 | mod_h5pactivity| And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia" And I switch to the main frame And I am on the "Course 1" course page When I am on the "Awesome H5P package" "h5pactivity activity" page And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Narnia" Scenario: Content state is not saved for teachers when enablesavestate is enabled Given the following config values are set as admin: | enablesavestate | 1 | mod_h5pactivity| And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as teacher1 And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia" And I switch to the main frame And I am on the "Course 1" course page When I am on the "Awesome H5P package" "h5pactivity activity" page And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia" Scenario: Content state is reseted when content changes Given the following config values are set as admin: | enablesavestate | 1 | mod_h5pactivity| And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia" And I switch to the main frame And I am on the "Course 1" course page When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as admin # Change the content. And I follow "Edit H5P content" And I switch to "h5p-editor-iframe" class iframe And I set the field "Title" to "Capitals" And I switch to the main frame And I click on "Save changes" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Check" # Check the content state has been reseted. And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should see "Data Reset" And I should see "This content has changed since you last used it." And I click on "OK" "button" And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia" Scenario: Content state is not reseted when content edition is cancelled Given the following config values are set as admin: | enablesavestate | 1 | mod_h5pactivity| And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia" And I switch to the main frame And I am on the "Course 1" course page When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as admin # Start content edition. And I follow "Edit H5P content" And I switch to "h5p-editor-iframe" class iframe And I set the field "Title" to "Capitals" And I switch to the main frame And I click on "Cancel" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Check" # Check the content state hasn't been reseted. And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 And I should see "Awesome H5P package" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe Then I should not see "Data Reset" And I should not see "This content has changed since you last used it." And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Narnia" Scenario: Content state is removed when an attempt is created Given the following config values are set as admin: | enablesavestate | 1 | mod_h5pactivity| # Save state content for student2, to check this data is not removed when student1 finishes their attempt. And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student2 And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Vallhonesta" # Confirm the content state has been saved properly. And I reload the page And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Vallhonesta" # Create an attempt for student1. And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 And I should not see "Attempts report" When I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I set the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" to "Narnia" And I click on "Check" "button" # Check the state content has been removed. And I reload the page Then I should see "Attempts report" And I am on the "Awesome H5P package" "h5pactivity activity" page And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" does not match value "Narnia" And I switch to the main frame # Check the state content for student2 is still there. And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student2 And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And the field with xpath "//input[contains(@aria-label,\"Blank input 1 of 4\")]" matches value "Vallhonesta" PK1B\Ǵ,tests/behat/h5pactivity_availability.featurenu[@mod @mod_h5pactivity @core_5hp Feature: Control H5P activity availability for students In order to restrict student access to H5P activity As a teacher I need to control the availability of the H5P activity Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | course | name | | h5pactivity | C1 | H5P Test | @javascript Scenario Outline: Restrict H5P activity access by date Given I am on the "H5P Test" "h5pactivity activity editing" page logged in as teacher1 And I expand all fieldsets And I click on "Add restriction..." "button" And I click on "Date" "button" in the "Add restriction..." "dialogue" And I set the following fields to these values: | Direction | from | | x[day] | 1 | | x[month] | 1 | | x[year] | | And I press "Save and return to course" When I am on the "Course 1" course page logged in as student1 Then I see "Available from" Examples: | year | fromvisibility | | ## -1 year ## %Y ## | should not | | ## +1 year ## %Y ## | should | PK1B\o(#tests/behat/sending_attempt.featurenu[@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe Feature: Do a H5P attempt In order to let students do a H5P attempt As a teacher I need to list students attempts on various reports Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/h5p:updatelibraries | Allow | editingteacher | System | | And the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Awesome H5P package | | packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p | | grademethod | 2 | Scenario: View an H5P as a teacher When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as teacher1 Then I should see "You are in preview mode." @javascript Scenario: Do an attempt and check on course log report When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 And I should not see "You are in preview mode." And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I click on "Correct one" "text" in the ".h5p-question-content" "css_element" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I switch to the main frame And I am on the "Course 1" course page logged in as teacher1 And I navigate to course participants And I follow "Student 1" Then I follow "Today's logs" And I should see "xAPI statement received" @javascript Scenario: Do various attempts and check them with the attempts and user grades reports Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 And I should not see "You are in preview mode." And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I click on "Retry" "button" in the ".h5p-question-buttons" "css_element" # We need to wait 1 second here because, in very quick environments, the 2nd # attempts happen too close to the 1st one and it's not sent properly. See MDL-76010. And I wait "1" seconds And I click on "Correct one" "text" in the ".h5p-question-content" "css_element" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" # H5P does not allow to Retry if the user checks the correct answer, we need to refresh the page. And I switch to the main frame And I reload the page And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe # Because of the steps above, the 2nd and 3rd attempts are enough "separated" and we don't # need to add any wait here. And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I click on "Retry" "button" in the ".h5p-question-buttons" "css_element" # Again, the wait between 3rd and 4th attempt, to separate them a little bit. And I wait "1" seconds And I click on "Correct one" "text" in the ".h5p-question-content" "css_element" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I switch to the main frame When I navigate to "Attempts report" in current page administration And "1" row "Score" column of "table" table should contain "0" And "2" row "Score" column of "table" table should contain "1" And "3" row "Score" column of "table" table should contain "0" And "4" row "Score" column of "table" table should contain "1" And I am on the "Course 1" "grades > User report > View" page logged in as "teacher1" And I click on "Student 1" in the "Search users" search combo box Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | | Awesome H5P package | 50.00 | 50.00 % | PK1B\U¿#tests/behat/add_h5pactivity.featurenu[@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe Feature: Add H5P activity In order to let students access a H5P package As a teacher I need to add H5P activity to a course Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/h5p:updatelibraries | Allow | editingteacher | System | | And I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on @javascript Scenario: Add an h5pactivity to a course Given the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Awesome H5P package | | intro | H5P activity Description | | packagefilepath | h5p/tests/fixtures/ipsums.h5p | When I am on the "Awesome H5P package" "h5pactivity activity" page Then I should see "H5P activity Description" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Lorum ipsum" And I should not see "Reuse" And I should not see "Rights of use" And I should not see "Embed" @javascript Scenario: Add an h5pactivity with download display option Given the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Awesome H5P package | | displayoptions | 12 | | packagefilepath | h5p/tests/fixtures/ipsums.h5p | When I am on the "Awesome H5P package" "h5pactivity activity" page Then I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And "Reuse" "text" should exist in the ".h5p-actions" "css_element" And I should not see "Rights of use" And I should not see "Embed" @javascript Scenario: Add an h5pactivity with embed display option Given the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Awesome H5P package | | displayoptions | 10 | | packagefilepath | h5p/tests/fixtures/ipsums.h5p | When I am on the "Awesome H5P package" "h5pactivity activity" page Then I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And "Reuse" "text" should not exist in the ".h5p-actions" "css_element" And I should not see "Rights of use" And I should see "Embed" @javascript Scenario: Add an h5pactivity with copyright display option using a content with copyright Given the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Awesome H5P package | | displayoptions | 6 | | packagefilepath | h5p/tests/fixtures/guess-the-answer.h5p | And I change window size to "large" When I am on the "Awesome H5P package" "h5pactivity activity" page Then I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And "Reuse" "text" should not exist in the ".h5p-actions" "css_element" And I should see "Rights of use" And I should not see "Embed" And I click on "Rights of use" "button" in the ".h5p-actions" "css_element" And I should see "Fruits" And I should see "Attribution (CC BY) 4.0 International (CC BY 4.0)" And I should see "H5P Author" And I should see "https://h5p.org (Originator)" And I should see "2000-2023" And I should see "This is licence extras information added for testing purposes." And I should see "Add metadata information, Another user, 01-11-23" @javascript Scenario: Add an h5pactivity with copyright display option using a content without copyright Given the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Awesome H5P package | | displayoptions | 6 | | packagefilepath | h5p/tests/fixtures/ipsums.h5p | When I am on the "Awesome H5P package" "h5pactivity activity" page Then I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And "Reuse" "text" should not exist in the ".h5p-actions" "css_element" And I should not see "Rights of use" And I should not see "Embed" @javascript Scenario: Add an h5pactivity with all display options enabled Given the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Awesome H5P package | | displayoptions | 0 | | packagefilepath | h5p/tests/fixtures/guess-the-answer.h5p | When I am on the "Awesome H5P package" "h5pactivity activity" page Then I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And "Reuse" "text" should exist in the ".h5p-actions" "css_element" And I should see "Rights of use" And I should see "Embed" PK1B\ Yj j $tests/behat/grading_attempts.featurenu[@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe Feature: Change grading options in an H5P activity In order to let students do a H5P attempt As a teacher I need to define what students attempts are used for grading Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/h5p:updatelibraries | Allow | editingteacher | System | | And the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Awesome H5P package | | packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p | And the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | Awesome H5P package | 1 | choice | 0 | 1 | 4 | 1 | 0 | | student1 | Awesome H5P package | 2 | choice | 1 | 1 | 4 | 1 | 1 | | student1 | Awesome H5P package | 3 | choice | 0 | 1 | 4 | 1 | 0 | @javascript Scenario: Default grading is max attempt grade Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1 And I expand all fieldsets And the field "Grading method" matches value "Highest grade" And I click on "Save and return to course" "button" When I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "Search users" search combo box Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | | Awesome H5P package | 100.00 | 100.00 % | @javascript Scenario: Change setting to first attempt Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1 When I set the following fields to these values: | Grading method | First attempt | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "Search users" search combo box Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | | Awesome H5P package | 0.00 | 0.00 % | @javascript Scenario: Change setting to last attempt Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1 When I set the following fields to these values: | Grading method | Last attempt | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "Search users" search combo box Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | | Awesome H5P package | 0.00 | 0.00 % | @javascript Scenario: Change setting to average attempt Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1 When I set the following fields to these values: | Grading method | Average grade | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "Search users" search combo box Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | | Awesome H5P package | 33.33 | 33.33 % | @javascript Scenario: Change setting to manual grading Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1 When I set the following fields to these values: | Grading method | Don't calculate a grade | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "Search users" search combo box Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | | Awesome H5P package | - | - | @javascript Scenario: Disable tracking Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1 When I set the following fields to these values: | Enable attempt tracking | No | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "Search users" search combo box Then the following should exist in the "user-grade" table: | Grade item | Grade | Percentage | | Awesome H5P package | - | - | @javascript Scenario: Reescale existing grades changing the maximum grade # First we set to average and recalculate grades. Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1 When I set the following fields to these values: | Grading method | Average grade | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "Search users" search combo box Then the following should exist in the "user-grade" table: | Grade item | Grade | Range | Percentage | | Awesome H5P package | 33.33 | 0–100 | 33.33 % | # Now we modify the maximum grade with rescaling. And I am on the "Awesome H5P package" "h5pactivity activity editing" page And I set the following fields to these values: | Rescale existing grades | Yes | | Maximum grade | 50 | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "Search users" search combo box Then the following should exist in the "user-grade" table: | Grade item | Grade | Range | Percentage | | Awesome H5P package | 16.67 | 0–50 | 33.33 % | @javascript Scenario: Change maximum grade without rescaling grade # First we set to average and recalculate grades. Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1 When I set the following fields to these values: | Grading method | Average grade | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "Search users" search combo box Then the following should exist in the "user-grade" table: | Grade item | Grade | Range | Percentage | | Awesome H5P package | 33.33 | 0–100 | 33.33 % | # Now we modify the maximum grade with rescaling. When I am on the "Awesome H5P package" "h5pactivity activity editing" page And I set the following fields to these values: | Rescale existing grades | No | | Maximum grade | 50 | And I click on "Save and return to course" "button" And I navigate to "View > User report" in the course gradebook And I click on "Student 1" in the "Search users" search combo box Then the following should exist in the "user-grade" table: | Grade item | Grade | Range | Percentage | | Awesome H5P package | 33.33 | 0–50 | 66.67 % | PK1B\;o#tests/behat/attempt_options.featurenu[@mod @mod_h5pactivity @core_h5p Feature: Attempts review settings. In order to let users to review attempts As a teacher I need to have specific settings to let students access the attempts report Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | | teacher2 | Teacher | 2 | teacher2@example.com | And the following config values are set as admin: | enrol_guest | Yes | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And I am on the "Course 1" "enrolment methods" page logged in as admin And I click on "Enable" "link" in the "Guest access" "table_row" And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C1 | editingteacher | | teacher2 | C1 | teacher | Scenario Outline: Attempt review behaviour when accessing an H5P activity Given the following "activity" exists: | activity | h5pactivity | | name | H5P package | | intro | Test H5P description | | course | C1 | | idnumber | h5ppackage | | enabletracking | | | reviewmode | | And the following "mod_h5pactivity > attempts" exist: | user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success | | student1 | H5P package | 1 | compound | 0 | 2 | 6 | 1 | 0 | | student1 | H5P package | 2 | compound | 2 | 2 | 4 | 1 | 1 | | student2 | H5P package | 1 | compound | 1 | 2 | 8 | 1 | 0 | When I am on the "H5P package" "h5pactivity activity" page logged in as Then "Attempts report" "link" should in current page administration And I should "You are in preview mode." And I should "Attempt tracking is not enabled for this activity." And I should "You can enable it in Settings." And I should "View attempts (3)" Examples: | user | enabletracking | reviewmode | attemptsreportlink | previewmode | attempttracking | attempttrackingsettings | viewattempts | | guest | 1 | 1 | not exist | not see | not see | not see | not see | | guest | 1 | 0 | not exist | not see | not see | not see | not see | | guest | 0 | 1 | not exist | not see | not see | not see | not see | | student1 | 1 | 1 | exist | not see | not see | not see | not see | | student1 | 1 | 0 | not exist | not see | not see | not see | not see | | student1 | 0 | 1 | not exist | not see | not see | not see | not see | | teacher1 | 1 | 1 | exist | see | not see | not see | see | | teacher1 | 1 | 0 | exist | see | not see | not see | see | | teacher1 | 0 | 1 | not exist | see | see | see | not see | | teacher2 | 0 | 1 | not exist | see | see | not see | not see | | teacher2 | 1 | 1 | exist | see | not see | not see | see | Scenario: View link behaviour Given the following "activity" exists: | activity | h5pactivity | | name | H5P package | | intro | Test H5P description | | course | C1 | | idnumber | h5ppackage | | enabletracking | 1 | | reviewmode | 1 | When I am on the "H5P package" "h5pactivity activity" page logged in as teacher1 # The link is displayed with the correct number of attempts (in that case 0 because there are no attempts yet). Then I should see "View attempts (0)" Then I follow "View attempts (0)" And I should not see "View user attempts" But the following "mod_h5pactivity > attempt" exists: | user | student1 | | h5pactivity | H5P package | | attempt | 1 | | interactiontype | compound | | rawscore | 2 | | maxscore | 2 | | duration | 4 | | completion | 1 | | success | 1 | And I am on the "H5P package" "h5pactivity activity" page And I follow "View attempts (1)" And I should see "View (1)" PK1B\%tests/behat/result_longfillin.featurenu[@mod @mod_h5pactivity @core_h5p @_file_upload @_switch_iframe @javascript Feature: View essay attempt report In order to let users to review an essay attempt As a user I need to view long fill in interactions in the report Background: Given the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And the following config values are set as admin: # No HTML should appear even with formatstringstriptags disabled. | formatstringstriptags | 0 | And the following "activity" exists: | activity | h5pactivity | | course | C1 | | name | Awesome H5P package | | grademethod | 2 | | packagefilepath | h5p/tests/fixtures/basic_essay.h5p | Scenario: View attempt essay content # Do an attempt. Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1 And I change window size to "large" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I set the field with xpath "//textarea" to "This is a smurfing smurf" And I click on "Check" "button" in the ".h5p-question-buttons" "css_element" And I switch to the main frame And I reload the page # Check attempt. When I navigate to "Attempts report" in current page administration And I follow "View report" Then I should see "This is a smurfing smurf" And I should not see "smurf" PK1B\tp,((*tests/behat/inline_editing_content.featurenu[@mod @mod_h5pactivity @core_h5p @_switch_iframe Feature: Inline editing H5P content In order to edit an existing H5P activity file As a teacher I need to see the button and access to the H5P editor Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | teacher2 | Teacher | 2 | teacher2@example.com | | student1 | Student | 1 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher2 | C1 | editingteacher | | student1 | C1 | student | And the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | moodle/h5p:updatelibraries | Allow | editingteacher | System | | @javascript Scenario: Add H5P activity using link to content bank file Given the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | teacher1 | Greeting card | /h5p/tests/fixtures/greeting-card.h5p | And I log in as "admin" # Add the navigation block. And I am on "Course 1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present # Create an H5P activity with a link to the content-bank file. And I add an h5pactivity activity to course "Course 1" section "1" And I set the following fields to these values: | Name | H5P package added as link to content bank | | Description | Description | And I click on "Add..." "button" in the "Package file" "form_row" And I select "Content bank" repository in file picker And I click on "Greeting card" "file" in repository content area And I click on "Link to the file" "radio" And I click on "Select this file" "button" And I click on "Save and display" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Hello world!" And I switch to the main frame # Modify the H5P content using the edit button (which opens the H5P editor). And I follow "Edit H5P content" And I should see "This content may be in use in other places." And I switch to "h5p-editor-iframe" class iframe And I set the field "Greeting text" to "It's a Wonderful Life!" And I switch to the main frame And I click on "Save changes" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe # Check the H5P content has changed. And I should not see "Hello world!" And I should see "It's a Wonderful Life!" And I switch to the main frame # Check the H5P has also changed into the content bank. And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I click on "Greeting card" "link" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should not see "Hello world!" And I should see "It's a Wonderful Life!" And I switch to the main frame And I log out # Check teacher1 can see the Edit button (because she is the author of this file in the content bank). And I am on the "H5P package added as link to content bank" "h5pactivity activity" page logged in as teacher1 And I should see "Edit H5P content" And I log out # Check teacher2 can't see the Edit button (because the file was created by the teacher1). When I am on the "H5P package added as link to content bank" "h5pactivity activity" page logged in as teacher2 Then I should not see "Edit H5P content" And I log out # Check student1 can't see the Edit button. And I am on the "H5P package added as link to content bank" "h5pactivity activity" page logged in as student1 And I should not see "Edit H5P content" @javascript Scenario: Add H5P activity using copy to content bank file Given the following "contentbank content" exist: | contextlevel | reference | contenttype | user | contentname | filepath | | Course | C1 | contenttype_h5p | admin | Greeting card | /h5p/tests/fixtures/greeting-card.h5p | And I log in as "admin" # Add the navigation block. And I am on "Course 1" course homepage with editing mode on And the following config values are set as admin: | unaddableblocks | | theme_boost| And I add the "Navigation" block if not present # Create an H5P activity with a copy to the content-bank file. And I add an h5pactivity activity to course "Course 1" section "1" And I set the following fields to these values: | Name | H5P package added as copy to content bank | | Description | Description | And I click on "Add..." "button" in the "Package file" "form_row" And I select "Content bank" repository in file picker And I click on "Greeting card" "file" in repository content area And I click on "Make a copy of the file" "radio" And I click on "Select this file" "button" And I click on "Save and display" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Hello world!" And I switch to the main frame # Modify the H5P content using the edit button (which opens the H5P editor). And I follow "Edit H5P content" And I should not see "This content may be in use in other places." And I switch to "h5p-editor-iframe" class iframe And I set the field "Greeting text" to "The nightmare before Christmas" And I switch to the main frame And I click on "Save changes" "button" # Check the H5P content has changed. And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should not see "Hello world!" And I should see "The nightmare before Christmas" And I switch to the main frame # Check the H5P has also changed into the content bank. And I am on "Course 1" course homepage And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Content bank" "link" in the "Navigation" "block" And I click on "Greeting card" "link" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Hello world!" And I should not see "The nightmare before Christmas" And I switch to the main frame And I log out # Check teacher1 can see the Edit button (because the file is a copy). And I am on the "H5P package added as copy to content bank" "h5pactivity activity" page logged in as teacher1 And I should see "Edit H5P content" And I log out # Check teacher2 can also see the Edit button (because the file is a copy). When I am on the "H5P package added as copy to content bank" "h5pactivity activity" page logged in as teacher2 Then I should see "Edit H5P content" And I log out # Check student1 can't see the Edit button. And I am on the "H5P package added as copy to content bank" "h5pactivity activity" page logged in as student1 And I should not see "Edit H5P content" @javascript Scenario: Add H5P activity using private user file Given the following "user private file" exists: | user | teacher1 | | filepath | h5p/tests/fixtures/greeting-card.h5p | # Create an H5P activity with a private user file. When I log in as "teacher1" And I add an h5pactivity activity to course "Course 1" section "1" And I set the following fields to these values: | Name | H5P package added as private user file | | Description | Description | And I click on "Add..." "button" in the "Package file" "form_row" And I select "Private files" repository in file picker And I click on "greeting-card.h5p" "file" in repository content area And I click on "Link to the file" "radio" And I click on "Select this file" "button" And I click on "Save and display" "button" And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should see "Hello world!" And I switch to the main frame # Modify the H5P content using the edit button (which opens the H5P editor). And I follow "Edit H5P content" And I should see "This content may be in use in other places." And I switch to "h5p-editor-iframe" class iframe And I set the field "Greeting text" to "Little women" And I switch to the main frame And I click on "Save changes" "button" # Check the H5P content has changed. And I switch to "h5p-player" class iframe And I switch to "h5p-iframe" class iframe And I should not see "Hello world!" And I should see "Little women" And I switch to the main frame And I log out # Check admin can't see the Edit button (because the file belongs to teacher1). And I am on the "H5P package added as private user file" "h5pactivity activity" page logged in as admin And I should not see "Edit H5P content" And I log out # Check teacher2 can't see the Edit button (because the file belongs to teacher1). When I am on the "H5P package added as private user file" "h5pactivity activity" page logged in as teacher2 Then I should not see "Edit H5P content" And I log out # Check student1 can't see the Edit button. And I am on the "H5P package added as private user file" "h5pactivity activity" page logged in as student1 And I should not see "Edit H5P content" PK1B\cMLL settings.phpnu[. /** * Module admin settings. * * @package mod_h5pactivity * @copyright 2023 Sara Arjona (sara@moodle.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { $settings->add(new admin_setting_configcheckbox('mod_h5pactivity/enablesavestate', get_string('enablesavestate', 'mod_h5pactivity'), get_string('enablesavestate_help', 'mod_h5pactivity'), 1)); $settings->add(new admin_setting_configtext('mod_h5pactivity/savestatefreq', get_string('savestatefreq', 'mod_h5pactivity'), get_string('savestatefreq_help', 'mod_h5pactivity'), 60, PARAM_INT)); } PK1B\T L'L' mod_form.phpnu[. /** * The main mod_h5pactivity configuration form. * * @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; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot.'/course/moodleform_mod.php'); /** * Module instance settings form. * * @package mod_h5pactivity * @copyright 2020 Ferran Recio * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_h5pactivity_mod_form extends moodleform_mod { /** * Defines forms elements */ public function definition(): void { global $CFG, $OUTPUT; $mform = $this->_form; // Adding the "general" fieldset, where all the common settings are shown. $mform->addElement('header', 'general', get_string('general', 'form')); // Adding the standard "name" field. $mform->addElement('text', 'name', get_string('name'), ['size' => '64']); if (!empty($CFG->formatstringstriptags)) { $mform->setType('name', PARAM_TEXT); } else { $mform->setType('name', PARAM_CLEANHTML); } $mform->addRule('name', null, 'required', null, 'client'); $mform->addRule('name', get_string('maximumchars', '', 255), 'maxlength', 255, 'client'); $this->standard_intro_elements(); // Adding the rest of mod_h5pactivity settings, spreading all them into this fieldset. $options = []; $options['accepted_types'] = ['.h5p']; $options['maxbytes'] = 0; $options['maxfiles'] = 1; $options['subdirs'] = 0; $mform->addElement('filemanager', 'packagefile', get_string('package', 'mod_h5pactivity'), null, $options); $mform->addHelpButton('packagefile', 'package', 'mod_h5pactivity'); $mform->addRule('packagefile', null, 'required'); // Add a link to the Content Bank if the user can access. $course = $this->get_course(); $coursecontext = context_course::instance($course->id); if (has_capability('moodle/contentbank:access', $coursecontext)) { $msg = null; $context = $this->get_context(); if ($context instanceof \context_module) { // This is an existing activity. If the H5P file it's a referenced file from the content bank, a link for // displaying this specific content will be used instead of the generic link to the main page of the content bank. $fs = get_file_storage(); $files = $fs->get_area_files($context->id, 'mod_h5pactivity', 'package', 0, 'sortorder, itemid, filepath, filename', false); $file = reset($files); if ($file && $file->get_reference() != null) { $referencedfile = \repository::get_moodle_file($file->get_reference()); if ($referencedfile->get_component() == 'contentbank') { // If the attached file is a referencedfile in the content bank, display a link to open this content. $url = new moodle_url('/contentbank/view.php', ['id' => $referencedfile->get_itemid()]); $msg = get_string('opencontentbank', 'mod_h5pactivity', $url->out()); $msg .= ' '.$OUTPUT->help_icon('contentbank', 'mod_h5pactivity'); } } } if (!isset($msg)) { $url = new moodle_url('/contentbank/index.php', ['contextid' => $coursecontext->id]); $msg = get_string('usecontentbank', 'mod_h5pactivity', $url->out()); $msg .= ' '.$OUTPUT->help_icon('contentbank', 'mod_h5pactivity'); } $mform->addElement('static', 'contentbank', '', $msg); } // H5P displaying options. $factory = new \core_h5p\factory(); $core = $factory->get_core(); $displayoptions = (array) \core_h5p\helper::decode_display_options($core); $mform->addElement('header', 'h5pdisplay', get_string('h5pdisplay', 'mod_h5pactivity')); foreach ($displayoptions as $key => $value) { $name = get_string('display'.$key, 'mod_h5pactivity'); $fieldname = "displayopt[$key]"; $mform->addElement('checkbox', $fieldname, $name); $mform->setType($fieldname, PARAM_BOOL); } // Add standard grading elements. $this->standard_grading_coursemodule_elements(); // Attempt options. $mform->addElement('header', 'h5pattempts', get_string('h5pattempts', 'mod_h5pactivity')); $mform->addElement('static', 'trackingwarning', '', get_string('tracking_messages', 'mod_h5pactivity')); $options = [1 => get_string('yes'), 0 => get_string('no')]; $mform->addElement('select', 'enabletracking', get_string('enabletracking', 'mod_h5pactivity'), $options); $mform->setDefault('enabletracking', 1); $options = manager::get_grading_methods(); $mform->addElement('select', 'grademethod', get_string('grade_grademethod', 'mod_h5pactivity'), $options); $mform->setType('grademethod', PARAM_INT); $mform->hideIf('grademethod', 'enabletracking', 'neq', 1); $mform->disabledIf('grademethod', 'grade[modgrade_type]', 'neq', 'point'); $mform->addHelpButton('grademethod', 'grade_grademethod', 'mod_h5pactivity'); $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', 'neq', 1); // Add standard elements. $this->standard_coursemodule_elements(); // Add standard buttons. $this->add_action_buttons(); } /** * Enforce validation rules here * * @param array $data array of ("fieldname"=>value) of submitted data * @param array $files array of uploaded files "element_name"=>tmp_file_path * @return array **/ public function validation($data, $files) { global $USER; $errors = parent::validation($data, $files); if (empty($data['packagefile'])) { $errors['packagefile'] = get_string('required'); } else { $draftitemid = file_get_submitted_draft_itemid('packagefile'); file_prepare_draft_area($draftitemid, $this->context->id, 'mod_h5pactivity', 'packagefilecheck', null, ['subdirs' => 0, 'maxfiles' => 1]); // Get file from users draft area. $usercontext = context_user::instance($USER->id); $fs = get_file_storage(); $files = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id', false); if (count($files) < 1) { $errors['packagefile'] = get_string('required'); return $errors; } $file = reset($files); if (!$file->is_external_file() && !empty($data['updatefreq'])) { // Make sure updatefreq is not set if using normal local file. $errors['updatefreq'] = get_string('updatefreq_error', 'mod_h5pactivity'); } } return $errors; } /** * Enforce defaults here. * * @param array $defaultvalues Form defaults * @return void **/ public function data_preprocessing(&$defaultvalues) { // H5P file. $draftitemid = file_get_submitted_draft_itemid('packagefile'); file_prepare_draft_area($draftitemid, $this->context->id, 'mod_h5pactivity', 'package', 0, ['subdirs' => 0, 'maxfiles' => 1]); $defaultvalues['packagefile'] = $draftitemid; // H5P display options. $factory = new \core_h5p\factory(); $core = $factory->get_core(); if (isset($defaultvalues['displayoptions'])) { $currentdisplay = $defaultvalues['displayoptions']; $displayoptions = (array) \core_h5p\helper::decode_display_options($core, $currentdisplay); } else { $displayoptions = (array) \core_h5p\helper::decode_display_options($core); } foreach ($displayoptions as $key => $value) { $fieldname = "displayopt[$key]"; $defaultvalues[$fieldname] = $value; } } /** * Allows modules to modify the data returned by form get_data(). * This method is also called in the bulk activity completion form. * * Only available on moodleform_mod. * * @param stdClass $data passed by reference */ public function data_postprocessing($data) { parent::data_postprocessing($data); $factory = new \core_h5p\factory(); $core = $factory->get_core(); if (isset($data->displayopt)) { $config = (object) $data->displayopt; } else { $config = \core_h5p\helper::decode_display_options($core); } $data->displayoptions = \core_h5p\helper::get_display_options($core, $config); if (!isset($data->enabletracking)) { $data->enabletracking = 0; } } } PK1B\Tbdb/services.phpnu[PK1B\6aa Gdb/access.phpnu[PK1B\$B Tz z db/upgrade.phpnu[PK1B\x ]]#db/install.xmlnu[PK1B\TY 8=lang/en/h5pactivity.phpnu[PK1B\ޟ ^grade.phpnu[PK1B\ {?/ebackup/moodle2/restore_h5pactivity_stepslib.phpnu[PK1B\X :tbackup/moodle2/restore_h5pactivity_activity_task.class.phpnu[PK1B\ɐ .<backup/moodle2/backup_h5pactivity_stepslib.phpnu[PK1B\ҹY 9backup/moodle2/backup_h5pactivity_activity_task.class.phpnu[PK1B\mpview.phpnu[PK1B\AoZSSpix/monologo.svgnu[PK1B\7`z`z9lib.phpnu[PK1B\í"" +version.phpnu[PK1B\I[.[.&-0classes/external/get_user_attempts.phpnu[PK1B\$S"S"!^classes/external/get_attempts.phpnu[PK1B\9.L  %classes/external/view_h5pactivity.phpnu[PK1B\21ڍclasses/external/get_h5pactivities_by_courses.phpnu[PK1B\&ˢclasses/external/log_report_viewed.phpnu[PK1B\j1classes/external/h5pactivity_summary_exporter.phpnu[PK1B\[ڢ22 bclasses/external/get_results.phpnu[PK1B\_cc7Tclasses/external/get_h5pactivity_access_information.phpnu[PK1B\5bs00classes/privacy/provider.phpnu[PK1B\&nS Gclasses/local/grader.phpnu[PK1B\!H??dclasses/local/attempt.phpnu[PK1B\nZiڻNNclasses/local/manager.phpnu[PK1B\*G&&classes/local/report.phpnu[PK1B\B { { 1classes/local/report/results.phpnu[PK1B\>D!classes/local/report/attempts.phpnu[PK1B\h=F߮%classes/local/report/participants.phpnu[PK1B\>V771classes/xapi/handler.phpnu[PK1B\uQ1ee]Lclasses/search/activity.phpnu[PK1B\,$ee Rclasses/output/reportlink.phpnu[PK1B\:^Yclasses/output/attempt.phpnu[PK1B\2Ra]]!yclasses/output/reportattempts.phpnu[PK1B\f- uclasses/output/reportresults.phpnu[PK1B\Iu  #classes/output/result/truefalse.phpnu[PK1B\|}"classes/output/result/matching.phpnu[PK1B\bvv classes/output/result/fillin.phpnu[PK1B\LV classes/output/result/choice.phpnu[PK1B\(u$classes/output/result/longfillin.phpnu[PK1B\^ ^ $1classes/output/result/sequencing.phpnu[PK1B\ԋclasses/output/result/other.phpnu[PK1B\ QF$$classes/output/result.phpnu[PK1B\;d  &classes/event/course_module_viewed.phpnu[PK1B\r\9classes/event/report_viewed.phpnu[PK1B\_\s* * $ *classes/event/statement_received.phpnu[PK1B\[;  44classes/event/course_module_instance_list_viewed.phpnu[PK1B\:f  :templates/reportresults.mustachenu[PK1B\q]< IEtemplates/attempt.mustachenu[PK1B\ ??'Stemplates/local/result/options.mustachenu[PK1B\"qww&ctemplates/local/result/header.mustachenu[PK1B\@@&htemplates/local/result/answer.mustachenu[PK1B\z<<yqtemplates/attempts.mustachenu[PK1B\{aatemplates/reportlink.mustachenu[PK1B\HP!templates/reviewattempts.mustachenu[PK1B\%aa!templates/reportattempts.mustachenu[PK1B\W7/ttTtemplates/result.mustachenu[PK1B\E [[ upgrade.txtnu[PK1B\'$ report.phpnu[PK1B\ eh h index.phpnu[PK1B\ 4UPGRADING.mdnu[PK1B\­)zXzXOtests/generator/lib.phpnu[PK1B\>32tests/generator/behat_mod_h5pactivity_generator.phpnu[PK1B\ 5S5S$s9tests/external/get_attempts_test.phpnu[PK1B\J99)tests/external/log_report_viewed_test.phpnu[PK1B\((4tests/external/get_h5pactivities_by_courses_test.phpnu[PK1B\`t  (tests/external/view_h5pactivity_test.phpnu[PK1B\}}:6tests/external/get_h5pactivity_access_information_test.phpnu[PK1B\m%m%)tests/external/get_user_attempts_test.phpnu[PK1B\AA#tests/external/get_results_test.phpnu[PK1B\!Σ|:|:Wtests/privacy/provider_test.phpnu[PK1B\7D.44tests/local/grader_test.phpnu[PK1B\G I Itests/local/attempt_test.phpnu[PK1B\G @tests/local/manager_test.phpnu[PK1B\#sGG5tests/lib_test.phpnu[PK1B\X`&&tests/restore_test.phpnu[PK1B\۰ KK ?tests/xapi/handler_test.phpnu[PK1B\EU 3 3#(tests/output/result/result_test.phpnu[PK1B\r"tests/event/report_viewed_test.phpnu[PK1B\U L L 'mtests/event/statement_received_test.phpnu[PK1B\nd d 7tests/event/course_module_instance_list_viewed_test.phpnu[PK1B\N