ÿØÿà 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-Ї Ç PK{\progress/styles.cssnu[#page-report-progress-index #completion-progress th, #page-report-progress-index #completion-progress td { padding: 2px 2px; font-weight: normal; border-right: 1px solid #eee; } #page-report-progress-index .progress-actions { text-align: center; list-style: none; } #page-report-progress-index .completion_pagingbar { margin: 1em 0; text-align: center; } #page-report-progress-index .completion_prev { display: inline; margin-right: 2em; } #page-report-progress-index .completion_pagingbar p { display: inline; } #page-report-progress-index .completion_next { display: inline; margin-left: 2em; } #page-report-progress-index .completion-header, #page-report-progress-index .completion-progresscell { text-align: center; } /* Custom CSS for rotated header.. */ #page-report-progress-index .rotated-text-container { display: inline-block; width: 16px; } #page-report-progress-index .rotated-text { display: inline-block; white-space: nowrap; /*rtl:remove*/ transform: translate(0, 100%) rotate(-90deg); /*rtl:raw: transform: translate(0, 100%) rotate(90deg); */ transform-origin: 0 0; vertical-align: middle; } #page-report-progress-index .rotated-text:after { content: ""; float: left; margin-top: 100%; } #page-report-progress-index .modicon { padding-top: 5px; } #completion-progress .icon { margin-left: 0.5rem; } PK{\rprogress/db/install.phpnu[. /** * Post installation and migration code. * * @package report * @subpackage progress * @copyright 2011 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; function xmldb_report_progress_install() { global $DB; } PK{\6progress/db/access.phpnu[. /** * Capabilities * * @package report_progress * @copyright 2008 Sam Marshall * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'report/progress:view' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'coursereport/progress:view', ) ); PK{\ֆmgg$progress/lang/en/report_progress.phpnu[. /** * Lang strings * * @package report * @subpackage progress * @copyright 2008 Sam Marshall * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Activity completion'; $string['activityorder'] = 'Activity order'; $string['activitysection'] = 'Section'; $string['allactivitiesandresources'] = 'All activities and resources'; $string['alphabetical'] = 'Alphabetical '; $string['include'] = 'Include'; $string['no_filter_by_section'] = 'Do not filter by section'; $string['orderincourse'] = 'Order in course'; $string['page-report-progress-x'] = 'Any activity completion report'; $string['page-report-progress-index'] = 'Activity completion report'; $string['page-report-progress-user'] = 'User activity completion report'; $string['privacy:metadata'] = 'The Activity completion report only shows data stored in other locations.'; $string['progress:view'] = 'View activity completion reports'; PK{\i{  progress/lib.phpnu[. /** * This file contains functions used by the progress report * * @package report * @subpackage progress * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * This function extends the navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $course The course to object for the report * @param stdClass $context The context of the course */ function report_progress_extend_navigation_course($navigation, $course, $context) { global $CFG, $OUTPUT; require_once($CFG->libdir.'/completionlib.php'); $showonnavigation = has_capability('report/progress:view', $context); $group = groups_get_course_group($course,true); // Supposed to verify group if($group===0 && $course->groupmode==SEPARATEGROUPS) { $showonnavigation = ($showonnavigation && has_capability('moodle/site:accessallgroups', $context)); } $completion = new completion_info($course); $showonnavigation = ($showonnavigation && $completion->is_enabled() && $completion->has_activities()); if ($showonnavigation) { $url = new moodle_url('/report/progress/index.php', array('course'=>$course->id)); $navigation->add(get_string('pluginname','report_progress'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/report', '')); } } /** * Return a list of page types * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block * @return array */ function report_progress_page_type_list($pagetype, $parentcontext, $currentcontext) { $array = array( '*' => get_string('page-x', 'pagetype'), 'report-*' => get_string('page-report-x', 'pagetype'), 'report-progress-*' => get_string('page-report-progress-x', 'report_progress'), 'report-progress-index' => get_string('page-report-progress-index', 'report_progress'), ); return $array; }PK{\5؉progress/version.phpnu[. /** * Version details * * @package report * @subpackage progress * @copyright 2008 Sam Marshall * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_progress'; // Full name of the plugin (used for diagnostics) PK{\DVc^%progress/classes/privacy/provider.phpnu[. /** * Privacy provider implementation for report_progress. * * @package report_progress * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_progress\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy provider implementation for report_progress. * * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK{\\ !progress/classes/local/helper.phpnu[. namespace report_progress\local; /** * Helper for report progress. * * @package report_progress * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL Juv3 or later */ class helper { /** The default number of results to be shown per page. */ const COMPLETION_REPORT_PAGE = 25; /** * Get activities information by the activity include and activity order option. * * @param \completion_info $completion Completion information of course. * @param string $activityinclude Activity type for filtering. * @param string $activityorder Activity sort option. * @param int $activitysection Section number for filtering. * @return array The available activity types and activities array after filtering and sorting. * @throws \coding_exception */ public static function get_activities_to_show(\completion_info $completion, string $activityinclude, string $activityorder, int $activitysection = -1): array { // Get all activity types. $activities = $completion->get_activities(); $availableactivitytypes = []; foreach ($activities as $activity) { if ($activity->modname == $activityinclude || $activitysection == -1 || $activity->sectionnum == $activitysection) { $availableactivitytypes[$activity->modname] = $activity->get_module_type_name(true); } } asort($availableactivitytypes); $availableactivitytypes = ['all' => get_string('allactivitiesandresources', 'report_progress')] + $availableactivitytypes; // Filter activities by section. if ($activitysection > -1) { $activities = array_filter($activities, function($activity) use ($activitysection) { return $activity->sectionnum == $activitysection; }); } // Filter activities by type. if (!empty($activityinclude) && $activityinclude !== 'all') { $activities = array_filter($activities, function($activity) use ($activityinclude) { return $activity->modname === $activityinclude; }); } // The activities are sorted by activity order on course page by default. if ($activityorder === 'alphabetical') { usort($activities, function($a, $b) { return strcmp($a->name, $b->name); }); } return [$availableactivitytypes, $activities]; } } PK{\հE$progress/classes/output/renderer.phpnu[. namespace report_progress\output; use single_select; use plugin_renderer_base; use html_writer; /** * Renderer for report progress. * * @package report_progress * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL Juv3 or later */ class renderer extends plugin_renderer_base { /** * Render include activity single select box. * * @param \moodle_url $url The base url. * @param array $activitytypes The activity type options. * @param string $activityinclude The current selected option. * @return string HTML * @throws \coding_exception */ public function render_include_activity_select(\moodle_url $url, array $activitytypes, string $activityinclude): string { $includeurl = fullclone($url); $includeurl->remove_params(['page', 'activityinclude']); $activityincludeselect = new single_select( $url, 'activityinclude', $activitytypes, $activityinclude, null, 'include-activity-select-report' ); $activityincludeselect->set_label(get_string('include', 'report_progress')); return \html_writer::div($this->output->render($activityincludeselect), 'include-activity-selector d-inline-block me-3' ); } /** * Render activity order single select box. * * @param \moodle_url $url The base url. * @param string $activityorder The current selected option. * @return string HTML * @throws \coding_exception */ public function render_activity_order_select(\moodle_url $url, string $activityorder): string { $activityorderurl = fullclone($url); $activityorderurl->remove_params(['activityorder']); $options = ['orderincourse' => get_string('orderincourse', 'report_progress'), 'alphabetical' => get_string('alphabetical', 'report_progress')]; $sorttable = new single_select( $activityorderurl, 'activityorder', $options, $activityorder, null, 'activity-order-select-report' ); $sorttable->set_label(get_string('activityorder', 'report_progress')); return \html_writer::div($this->output->render($sorttable), 'activity-order-selector include-activity-selector d-inline-block'); } /** * Render groups single select box. * * @param \moodle_url $url The base url. * @param \stdClass $course Current course. * @return string HTML */ public function render_groups_select(\moodle_url $url, \stdClass $course): string { $groupurl = fullclone($url); $groupurl->remove_params(['page', 'group']); $groupoutput = groups_print_course_menu($course, $groupurl, true); if (empty($groupoutput)) { return $groupoutput; } return \html_writer::div($groupoutput, 'd-inline-block me-3'); } /** * Render activity section single select box. * * @param \moodle_url $url The base url. * @param string $activitysection The current selected section. * @param array $sections An array containing all sections of the course * @return string HTML * @throws \coding_exception */ public function render_activity_section_select(\moodle_url $url, string $activitysection, array $sections): string { $activitysectionurl = fullclone($url); $activitysectionurl->remove_params(['activitysection']); $options = $sections; $options[-1] = get_string('no_filter_by_section', 'report_progress'); $sorttable = new single_select( $activitysectionurl, 'activitysection', $options, $activitysection, null, 'activity-section-select-report' ); $sorttable->set_label(get_string('activitysection', 'report_progress')); return \html_writer::div($this->output->render($sorttable), 'activity-section-selector include-activity-selector d-inline-block ms-3'); } /** * Render download buttons. * * @param \moodle_url $url The base url. * @return string HTML * @throws \coding_exception */ public function render_download_buttons(\moodle_url $url): string { $downloadurl = fullclone($url); $downloadurl->remove_params(['page']); $downloadurl->param('format', 'csv'); $downloadhtml = html_writer::start_tag('ul', ['class' => 'progress-actions']); $downloadhtml .= html_writer::tag('li', html_writer::link($downloadurl, get_string('csvdownload', 'completion'))); $downloadurl->param('format', 'excelcsv'); $downloadhtml .= html_writer::tag('li', html_writer::link($downloadurl, get_string('excelcsvdownload', 'completion'))); $downloadhtml .= html_writer::end_tag('ul'); return $downloadhtml; } } PK{\ζ٫>>progress/index.phpnu[. /** * Activity progress reports * * @package report * @subpackage progress * @copyright 2008 Sam Marshall * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\report_helper; use \report_progress\local\helper; require('../../config.php'); require_once($CFG->libdir . '/completionlib.php'); // Get course $id = required_param('course',PARAM_INT); $course = $DB->get_record('course',array('id'=>$id)); if (!$course) { throw new \moodle_exception('invalidcourseid'); } $context = context_course::instance($course->id); // Sort (default lastname, optionally firstname) $sort = optional_param('sort','',PARAM_ALPHA); $firstnamesort = $sort == 'firstname'; // CSV format $format = optional_param('format','',PARAM_ALPHA); $excel = $format == 'excelcsv'; $csv = $format == 'csv' || $excel; // Paging, sorting and filtering. $page = optional_param('page', 0, PARAM_INT); $sifirst = optional_param('sifirst', 'all', PARAM_NOTAGS); $silast = optional_param('silast', 'all', PARAM_NOTAGS); $groupid = optional_param('group', 0, PARAM_INT); $activityinclude = optional_param('activityinclude', 'all', PARAM_TEXT); $activityorder = optional_param('activityorder', 'orderincourse', PARAM_TEXT); $activitysection = optional_param('activitysection', -1, PARAM_INT); // Whether to show extra user identity information $userfields = \core_user\fields::for_identity($context); $extrafields = $userfields->get_required_fields([\core_user\fields::PURPOSE_IDENTITY]); $leftcols = 1 + count($extrafields); function csv_quote($value) { global $excel; if ($excel) { return core_text::convert('"'.str_replace('"',"'",$value).'"','UTF-8','UTF-16LE'); } else { return '"'.str_replace('"',"'",$value).'"'; } } $url = new moodle_url('/report/progress/index.php', array('course'=>$id)); $PAGE->navigation->override_active_url($url); if ($sort !== '') { $url->param('sort', $sort); } if ($format !== '') { $url->param('format', $format); } if ($page !== 0) { $url->param('page', $page); } if ($sifirst !== 'all') { $url->param('sifirst', $sifirst); } if ($silast !== 'all') { $url->param('silast', $silast); } if ($groupid !== 0) { $url->param('group', $groupid); } if ($activityinclude !== '') { $url->param('activityinclude', $activityinclude); } if ($activityorder !== '') { $url->param('activityorder', $activityorder); } if ($activitysection !== '') { $url->param('activitysection', $activitysection); } $PAGE->set_url($url); $PAGE->set_pagelayout('report'); require_login($course); // Check basic permission require_capability('report/progress:view',$context); // Get group mode $group = groups_get_course_group($course,true); // Supposed to verify group if ($group===0 && $course->groupmode==SEPARATEGROUPS) { require_capability('moodle/site:accessallgroups',$context); } // Get data on activities and progress of all users, and give error if we've // nothing to display (no users or no activities). $completion = new completion_info($course); list($activitytypes, $activities) = helper::get_activities_to_show($completion, $activityinclude, $activityorder, $activitysection); $output = $PAGE->get_renderer('report_progress'); if ($sifirst !== 'all') { set_user_preference('ifirst', $sifirst); } if ($silast !== 'all') { set_user_preference('ilast', $silast); } if (!empty($USER->preference['ifirst'])) { $sifirst = $USER->preference['ifirst']; } else { $sifirst = 'all'; } if (!empty($USER->preference['ilast'])) { $silast = $USER->preference['ilast']; } else { $silast = 'all'; } // Generate where clause $where = array(); $where_params = array(); if ($sifirst !== 'all') { $where[] = $DB->sql_like('u.firstname', ':sifirst', false, false); $where_params['sifirst'] = $sifirst.'%'; } if ($silast !== 'all') { $where[] = $DB->sql_like('u.lastname', ':silast', false, false); $where_params['silast'] = $silast.'%'; } // Get user match count $total = $completion->get_num_tracked_users(implode(' AND ', $where), $where_params, $group); // Total user count $grandtotal = $completion->get_num_tracked_users('', array(), $group); // Get user data $progress = array(); if ($total) { $progress = $completion->get_progress_all( implode(' AND ', $where), $where_params, $group, $firstnamesort ? 'u.firstname ASC, u.lastname ASC' : 'u.lastname ASC, u.firstname ASC', $csv ? 0 : helper::COMPLETION_REPORT_PAGE, $csv ? 0 : $page * helper::COMPLETION_REPORT_PAGE, $context ); } if ($csv && $grandtotal && count($activities)>0) { // Only show CSV if there are some users/actvs $shortname = format_string($course->shortname, true, array('context' => $context)); header('Content-Disposition: attachment; filename=progress.'. preg_replace('/[^a-z0-9-]/','_',core_text::strtolower(strip_tags($shortname))).'.csv'); // Unicode byte-order mark for Excel if ($excel) { header('Content-Type: text/csv; charset=UTF-16LE'); print chr(0xFF).chr(0xFE); $sep="\t".chr(0); $line="\n".chr(0); } else { header('Content-Type: text/csv; charset=UTF-8'); $sep=","; $line="\n"; } } else { // Navigation and header $strreports = get_string("reports"); $strcompletion = get_string('activitycompletion', 'completion'); $PAGE->set_title($strcompletion); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); // Print the selected dropdown. $pluginname = get_string('pluginname', 'report_progress'); report_helper::print_report_selector($pluginname); $PAGE->requires->js_call_amd('report_progress/completion_override', 'init', [fullname($USER)]); // Handle groups (if enabled). echo $output->render_groups_select($url, $course); // Display include activity filter. echo $output->render_include_activity_select($url, $activitytypes, $activityinclude); // Display activity order options. echo $output->render_activity_order_select($url, $activityorder); // Display section selector. $modinfo = get_fast_modinfo($course); $sections = []; $cmids = array_keys($completion->get_activities()); foreach ($modinfo->get_sections() as $sectionnum => $section) { if (empty(array_intersect($section, $cmids))) { continue; } $sectionname = get_section_name($course, $sectionnum); if (empty($sectionname)) { $sectionname = get_string('section') . ' ' . $sectionnum; } $sections[$sectionnum] = $sectionname; } echo $output->render_activity_section_select($url, $activitysection, $sections); } if (count($activities)==0) { echo $OUTPUT->container(get_string('err_noactivities', 'completion'), 'errorbox errorboxcontent'); echo $OUTPUT->footer(); exit; } // If no users in this course what-so-ever if (!$grandtotal) { echo $OUTPUT->container(get_string('err_nousers', 'completion'), 'errorbox errorboxcontent'); echo $OUTPUT->footer(); exit; } // Build link for paging $link = $CFG->wwwroot.'/report/progress/?course='.$course->id; if (strlen($sort)) { $link .= '&sort='.$sort; } $link .= '&start='; $pagingbar = ''; // Initials bar. $prefixfirst = 'sifirst'; $prefixlast = 'silast'; // The URL used in the initials bar should reset the 'start' parameter. $initialsbarurl = fullclone($url); $initialsbarurl->remove_params('page'); $pagingbar .= $OUTPUT->initials_bar($sifirst, 'firstinitial mt-2', get_string('firstname'), $prefixfirst, $initialsbarurl); $pagingbar .= $OUTPUT->initials_bar($silast, 'lastinitial', get_string('lastname'), $prefixlast, $initialsbarurl); $pagingbar .= $OUTPUT->paging_bar($total, $page, helper::COMPLETION_REPORT_PAGE, $url); // Okay, let's draw the table of progress info, // Start of table if (!$csv) { print '
'; // ugh print $pagingbar; if (!$total) { echo $OUTPUT->notification(get_string('nothingtodisplay'), 'info', false); echo $OUTPUT->footer(); exit; } print '
'; print ''; // User heading / sort option print ''; // Print user identity columns foreach ($extrafields as $field) { echo ''; } } else { foreach ($extrafields as $field) { echo $sep . csv_quote(\core_user\fields::get_display_name($field)); } } // Activities $formattedactivities = array(); foreach($activities as $activity) { $datepassed = $activity->completionexpected && $activity->completionexpected <= time(); $datepassedclass = $datepassed ? 'completion-expired' : ''; if ($activity->completionexpected && !$csv) { $datetext = userdate($activity->completionexpected, get_string('strftimedate', 'langconfig')); } else { $datetext = get_string('completed'); } // Some names (labels) come URL-encoded and can be very long, so shorten them $displayname = format_string($activity->name, true, array('context' => $activity->context)); if ($csv) { print $sep.csv_quote($displayname).$sep.csv_quote($datetext); } else { $shortenedname = shorten_text($displayname); print ''; } $formattedactivities[$activity->id] = (object)array( 'datepassedclass' => $datepassedclass, 'displayname' => $displayname, ); } if ($csv) { print $line; } else { print ''; } // Row for each user foreach($progress as $user) { // User name if ($csv) { print csv_quote(fullname($user, has_capability('moodle/site:viewfullnames', $context))); foreach ($extrafields as $field) { echo $sep . csv_quote($user->{$field}); } } else { print ''; foreach ($extrafields as $field) { echo ''; } } // Progress for each activity foreach($activities as $activity) { // Get progress information and state if (array_key_exists($activity->id, $user->progress)) { $thisprogress = $user->progress[$activity->id]; $state = $thisprogress->completionstate; $overrideby = $thisprogress->overrideby; $date = userdate($thisprogress->timemodified); } else { $state = COMPLETION_INCOMPLETE; $overrideby = 0; $date = ''; } // Work out how it corresponds to an icon switch($state) { case COMPLETION_INCOMPLETE : $completiontype = 'n'.($overrideby ? '-override' : ''); break; case COMPLETION_COMPLETE : $completiontype = 'y'.($overrideby ? '-override' : ''); break; case COMPLETION_COMPLETE_PASS : $completiontype = 'pass'; break; case COMPLETION_COMPLETE_FAIL : $completiontype = 'fail'; break; } $completiontrackingstring = $activity->completion == COMPLETION_TRACKING_AUTOMATIC ? 'auto' : 'manual'; $completionicon = 'completion-' . $completiontrackingstring. '-' . $completiontype; if ($overrideby) { $overridebyuser = \core_user::get_user($overrideby, '*', MUST_EXIST); $describe = get_string('completion-' . $completiontype, 'completion', fullname($overridebyuser)); } else { $describe = get_string('completion-' . $completiontype, 'completion'); } $a=new StdClass; $a->state=$describe; $a->date=$date; $a->user = fullname($user, has_capability('moodle/site:viewfullnames', $context)); $a->activity = $formattedactivities[$activity->id]->displayname; $fulldescribe=get_string('progress-title','completion',$a); if ($csv) { if ($date != '') { $date = userdate($thisprogress->timemodified, "%F %T"); } print $sep.csv_quote($describe).$sep.csv_quote($date); } else { $celltext = $OUTPUT->pix_icon('i/' . $completionicon, s($fulldescribe)); if (has_capability('moodle/course:overridecompletion', $context) && $state != COMPLETION_COMPLETE_PASS && $state != COMPLETION_COMPLETE_FAIL) { $newstate = ($state == COMPLETION_COMPLETE) ? COMPLETION_INCOMPLETE : COMPLETION_COMPLETE; $changecompl = $user->id . '-' . $activity->id . '-' . $newstate; $url = new moodle_url($PAGE->url, ['sesskey' => sesskey()]); $celltext = html_writer::link($url, $celltext, array('class' => 'changecompl', 'data-changecompl' => $changecompl, 'data-activityname' => $a->activity, 'data-userfullname' => $a->user, 'data-completiontracking' => $completiontrackingstring, 'role' => 'button')); } print ''; } } if ($csv) { print $line; } else { print ''; } } if ($csv) { exit; } print '
'; $sorturl = fullclone($url); if ($firstnamesort) { $sorturl->param('sort', 'lastname'); $sortlink = html_writer::link($sorturl, get_string('lastname')); print get_string('firstname') . " / $sortlink"; } else { $sorturl->param('sort', 'firstname'); $sortlink = html_writer::link($sorturl, get_string('firstname')); print "$sortlink / " . get_string('lastname'); } print '' . \core_user\fields::get_display_name($field) . ''. ''. '
'.$shortenedname.'
'. '
'. $OUTPUT->image_icon('monologo', get_string('modulename', $activity->modname), $activity->modname) . '
'. '
'; if ($activity->completionexpected) { print '
'.$datetext.'
'; } print '
' . fullname($user, has_capability('moodle/site:viewfullnames', $context)) . '' . s($user->{$field}) . ''. $celltext . '
'; print '
'; echo $output->render_download_buttons($url); echo $OUTPUT->footer(); PK{\D[W'progress/amd/src/completion_override.jsnu[// 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 . /** * AMD module to handle overriding activity completion status. * * @module report_progress/completion_override * @copyright 2016 onwards Eiz Eddin Al Katrib * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define(['jquery', 'core/ajax', 'core/str', 'core/modal_save_cancel', 'core/modal_events', 'core/notification', 'core/custom_interaction_events', 'core/templates', 'core/pending'], function($, Ajax, Str, ModalSaveCancel, ModalEvents, Notification, CustomEvents, Templates, Pending) { /** * @var {String} the full name of the current user. * @private */ var userFullName; /** * @var {JQuery} JQuery object containing the element (completion link) that was most recently activated. * @private */ var triggerElement; /** * Helper function to get the pix icon key based on the completion state. * @method getIconDescriptorFromState * @param {number} state The current completion state. * @param {string} tracking The completion tracking type, either 'manual' or 'auto'. * @return {string} the key for the respective icon. * @private */ var getIconKeyFromState = function(state, tracking) { return state > 0 ? 'i/completion-' + tracking + '-y-override' : 'i/completion-' + tracking + '-n-override'; }; /** * Handles the confirmation of an override change, calling the web service to update it. * @method setOverride * @param {Object} override the override data * @private */ var setOverride = function(override) { const pendingPromise = new Pending('report_progress/compeletion_override/setOverride'); // Generate a loading spinner while we're working. Templates.render('core/loading', {}).then(function(html) { // Append the loading spinner to the trigger element. triggerElement.append(html); // Update the completion status override. return Ajax.call([{ methodname: 'core_completion_override_activity_completion_status', args: override }])[0]; }).then(function(results) { var completionState = (results.state > 0) ? 1 : 0; // Now, build the new title string, get the new icon, and update the DOM. var tooltipKey = completionState ? 'completion-y-override' : 'completion-n-override'; Str.get_string(tooltipKey, 'completion', userFullName).then(function(stateString) { var params = { state: stateString, date: '', user: triggerElement.attr('data-userfullname'), activity: triggerElement.attr('data-activityname') }; return Str.get_string('progress-title', 'completion', params); }).then(function(titleString) { var completionTracking = triggerElement.attr('data-completiontracking'); return Templates.renderPix(getIconKeyFromState(completionState, completionTracking), 'core', titleString); }).then(function(html) { var oppositeState = completionState > 0 ? 0 : 1; triggerElement.find('.loading-icon').remove(); triggerElement.data('changecompl', override.userid + '-' + override.cmid + '-' + oppositeState); triggerElement.attr('data-changecompl', override.userid + '-' + override.cmid + '-' + oppositeState); triggerElement.children("img").replaceWith(html); return; }).catch(Notification.exception); return; }) .then(() => { pendingPromise.resolve(); return; }).catch(Notification.exception); }; /** * Handler for activation of a completion status button element. * @method userConfirm * @param {Event} e the CustomEvents event (CustomEvents.events.activate in this case) * @param {Object} data an object containing the original event (click, keydown, etc.). * @private */ var userConfirm = function(e, data) { data.originalEvent.preventDefault(); data.originalEvent.stopPropagation(); e.preventDefault(); e.stopPropagation(); triggerElement = $(e.currentTarget); var elemData = triggerElement.data('changecompl').split('-'); var override = { userid: elemData[0], cmid: elemData[1], newstate: elemData[2] }; var newStateStr = (override.newstate == 1) ? 'completion-y' : 'completion-n'; Str.get_strings([ {key: newStateStr, component: 'completion'} ]).then(function(strings) { return Str.get_strings([ {key: 'confirm', component: 'moodle'}, {key: 'areyousureoverridecompletion', component: 'completion', param: strings[0]} ]); }).then(function(strings) { // Create a save/cancel modal. return ModalSaveCancel.create({ title: strings[0], body: strings[1], show: true, removeOnClose: true, }); }).then(function(modal) { // Now set up the handlers for the confirmation or cancellation of the modal, and show it. // Confirmation only. modal.getRoot().on(ModalEvents.save, function() { setOverride(override); }); return modal; }).catch(Notification.exception); }; /** * Init this module which allows activity completion state to be changed via ajax. * @method init * @param {string} fullName The current user's full name. * @private */ var init = function(fullName) { userFullName = fullName; // Register the click, space and enter events as activators for the trigger element. $('#completion-progress a.changecompl').each(function(index, element) { CustomEvents.define(element, [CustomEvents.events.activate]); }); // Set the handler on the parent element (the table), but filter so the callback is only called for type children // having the '.changecompl' class. The element can then be accessed in the callback via e.currentTarget. $('#completion-progress').on(CustomEvents.events.activate, "a.changecompl", function(e, data) { userConfirm(e, data); }); }; return /** @alias module:report_progress/completion_override */ { init: init }; }); PK{\ !2 -progress/amd/build/completion_override.min.jsnu[/** * AMD module to handle overriding activity completion status. * * @module report_progress/completion_override * @copyright 2016 onwards Eiz Eddin Al Katrib * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since 3.1 */ define("report_progress/completion_override",["jquery","core/ajax","core/str","core/modal_save_cancel","core/modal_events","core/notification","core/custom_interaction_events","core/templates","core/pending"],(function($,Ajax,Str,ModalSaveCancel,ModalEvents,Notification,CustomEvents,Templates,Pending){var userFullName,triggerElement,userConfirm=function(e,data){data.originalEvent.preventDefault(),data.originalEvent.stopPropagation(),e.preventDefault(),e.stopPropagation();var elemData=(triggerElement=$(e.currentTarget)).data("changecompl").split("-"),override={userid:elemData[0],cmid:elemData[1],newstate:elemData[2]},newStateStr=1==override.newstate?"completion-y":"completion-n";Str.get_strings([{key:newStateStr,component:"completion"}]).then((function(strings){return Str.get_strings([{key:"confirm",component:"moodle"},{key:"areyousureoverridecompletion",component:"completion",param:strings[0]}])})).then((function(strings){return ModalSaveCancel.create({title:strings[0],body:strings[1],show:!0,removeOnClose:!0})})).then((function(modal){return modal.getRoot().on(ModalEvents.save,(function(){!function(override){const pendingPromise=new Pending("report_progress/compeletion_override/setOverride");Templates.render("core/loading",{}).then((function(html){return triggerElement.append(html),Ajax.call([{methodname:"core_completion_override_activity_completion_status",args:override}])[0]})).then((function(results){var completionState=results.state>0?1:0,tooltipKey=completionState?"completion-y-override":"completion-n-override";Str.get_string(tooltipKey,"completion",userFullName).then((function(stateString){var params={state:stateString,date:"",user:triggerElement.attr("data-userfullname"),activity:triggerElement.attr("data-activityname")};return Str.get_string("progress-title","completion",params)})).then((function(titleString){var tracking,completionTracking=triggerElement.attr("data-completiontracking");return Templates.renderPix((tracking=completionTracking,completionState>0?"i/completion-"+tracking+"-y-override":"i/completion-"+tracking+"-n-override"),"core",titleString)})).then((function(html){var oppositeState=completionState>0?0:1;triggerElement.find(".loading-icon").remove(),triggerElement.data("changecompl",override.userid+"-"+override.cmid+"-"+oppositeState),triggerElement.attr("data-changecompl",override.userid+"-"+override.cmid+"-"+oppositeState),triggerElement.children("img").replaceWith(html)})).catch(Notification.exception)})).then((()=>{pendingPromise.resolve()})).catch(Notification.exception)}(override)})),modal})).catch(Notification.exception)};return{init:function(fullName){userFullName=fullName,$("#completion-progress a.changecompl").each((function(index,element){CustomEvents.define(element,[CustomEvents.events.activate])})),$("#completion-progress").on(CustomEvents.events.activate,"a.changecompl",(function(e,data){userConfirm(e,data)}))}}})); //# sourceMappingURL=completion_override.min.js.mapPK{\3>**1progress/amd/build/completion_override.min.js.mapnu[{"version":3,"file":"completion_override.min.js","sources":["../src/completion_override.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * AMD module to handle overriding activity completion status.\n *\n * @module report_progress/completion_override\n * @copyright 2016 onwards Eiz Eddin Al Katrib \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @since 3.1\n */\ndefine(['jquery', 'core/ajax', 'core/str', 'core/modal_save_cancel', 'core/modal_events', 'core/notification',\n 'core/custom_interaction_events', 'core/templates', 'core/pending'],\n function($, Ajax, Str, ModalSaveCancel, ModalEvents, Notification, CustomEvents, Templates, Pending) {\n\n /**\n * @var {String} the full name of the current user.\n * @private\n */\n var userFullName;\n\n /**\n * @var {JQuery} JQuery object containing the element (completion link) that was most recently activated.\n * @private\n */\n var triggerElement;\n\n /**\n * Helper function to get the pix icon key based on the completion state.\n * @method getIconDescriptorFromState\n * @param {number} state The current completion state.\n * @param {string} tracking The completion tracking type, either 'manual' or 'auto'.\n * @return {string} the key for the respective icon.\n * @private\n */\n var getIconKeyFromState = function(state, tracking) {\n return state > 0 ? 'i/completion-' + tracking + '-y-override' : 'i/completion-' + tracking + '-n-override';\n };\n\n /**\n * Handles the confirmation of an override change, calling the web service to update it.\n * @method setOverride\n * @param {Object} override the override data\n * @private\n */\n var setOverride = function(override) {\n const pendingPromise = new Pending('report_progress/compeletion_override/setOverride');\n // Generate a loading spinner while we're working.\n Templates.render('core/loading', {}).then(function(html) {\n // Append the loading spinner to the trigger element.\n triggerElement.append(html);\n\n // Update the completion status override.\n return Ajax.call([{\n methodname: 'core_completion_override_activity_completion_status',\n args: override\n }])[0];\n }).then(function(results) {\n var completionState = (results.state > 0) ? 1 : 0;\n\n // Now, build the new title string, get the new icon, and update the DOM.\n var tooltipKey = completionState ? 'completion-y-override' : 'completion-n-override';\n Str.get_string(tooltipKey, 'completion', userFullName).then(function(stateString) {\n var params = {\n state: stateString,\n date: '',\n user: triggerElement.attr('data-userfullname'),\n activity: triggerElement.attr('data-activityname')\n };\n return Str.get_string('progress-title', 'completion', params);\n }).then(function(titleString) {\n var completionTracking = triggerElement.attr('data-completiontracking');\n return Templates.renderPix(getIconKeyFromState(completionState, completionTracking), 'core', titleString);\n }).then(function(html) {\n var oppositeState = completionState > 0 ? 0 : 1;\n triggerElement.find('.loading-icon').remove();\n triggerElement.data('changecompl', override.userid + '-' + override.cmid + '-' + oppositeState);\n triggerElement.attr('data-changecompl', override.userid + '-' + override.cmid + '-' + oppositeState);\n triggerElement.children(\"img\").replaceWith(html);\n return;\n }).catch(Notification.exception);\n\n return;\n })\n .then(() => {\n pendingPromise.resolve();\n return;\n }).catch(Notification.exception);\n };\n\n /**\n * Handler for activation of a completion status button element.\n * @method userConfirm\n * @param {Event} e the CustomEvents event (CustomEvents.events.activate in this case)\n * @param {Object} data an object containing the original event (click, keydown, etc.).\n * @private\n */\n var userConfirm = function(e, data) {\n data.originalEvent.preventDefault();\n data.originalEvent.stopPropagation();\n e.preventDefault();\n e.stopPropagation();\n\n triggerElement = $(e.currentTarget);\n var elemData = triggerElement.data('changecompl').split('-');\n var override = {\n userid: elemData[0],\n cmid: elemData[1],\n newstate: elemData[2]\n };\n var newStateStr = (override.newstate == 1) ? 'completion-y' : 'completion-n';\n\n Str.get_strings([\n {key: newStateStr, component: 'completion'}\n ]).then(function(strings) {\n return Str.get_strings([\n {key: 'confirm', component: 'moodle'},\n {key: 'areyousureoverridecompletion', component: 'completion', param: strings[0]}\n ]);\n }).then(function(strings) {\n // Create a save/cancel modal.\n return ModalSaveCancel.create({\n title: strings[0],\n body: strings[1],\n show: true,\n removeOnClose: true,\n });\n }).then(function(modal) {\n // Now set up the handlers for the confirmation or cancellation of the modal, and show it.\n\n // Confirmation only.\n modal.getRoot().on(ModalEvents.save, function() {\n setOverride(override);\n });\n\n return modal;\n }).catch(Notification.exception);\n };\n\n /**\n * Init this module which allows activity completion state to be changed via ajax.\n * @method init\n * @param {string} fullName The current user's full name.\n * @private\n */\n var init = function(fullName) {\n userFullName = fullName;\n\n // Register the click, space and enter events as activators for the trigger element.\n $('#completion-progress a.changecompl').each(function(index, element) {\n CustomEvents.define(element, [CustomEvents.events.activate]);\n });\n\n // Set the handler on the parent element (the table), but filter so the callback is only called for type children\n // having the '.changecompl' class. The element can then be accessed in the callback via e.currentTarget.\n $('#completion-progress').on(CustomEvents.events.activate, \"a.changecompl\", function(e, data) {\n userConfirm(e, data);\n });\n };\n\n return /** @alias module:report_progress/completion_override */ {\n init: init\n };\n });\n"],"names":["define","$","Ajax","Str","ModalSaveCancel","ModalEvents","Notification","CustomEvents","Templates","Pending","userFullName","triggerElement","userConfirm","e","data","originalEvent","preventDefault","stopPropagation","elemData","currentTarget","split","override","userid","cmid","newstate","newStateStr","get_strings","key","component","then","strings","param","create","title","body","show","removeOnClose","modal","getRoot","on","save","pendingPromise","render","html","append","call","methodname","args","results","completionState","state","tooltipKey","get_string","stateString","params","date","user","attr","activity","titleString","tracking","completionTracking","renderPix","oppositeState","find","remove","children","replaceWith","catch","exception","resolve","setOverride","init","fullName","each","index","element","events","activate"],"mappings":";;;;;;;;AAuBAA,6CAAO,CAAC,SAAU,YAAa,WAAY,yBAA0B,oBAAqB,oBAClF,iCAAkC,iBAAkB,iBACxD,SAASC,EAAGC,KAAMC,IAAKC,gBAAiBC,YAAaC,aAAcC,aAAcC,UAAWC,aAMpFC,aAMAC,eAwEAC,YAAc,SAASC,EAAGC,MAC1BA,KAAKC,cAAcC,iBACnBF,KAAKC,cAAcE,kBACnBJ,EAAEG,iBACFH,EAAEI,sBAGEC,UADJP,eAAiBV,EAAEY,EAAEM,gBACSL,KAAK,eAAeM,MAAM,KACpDC,SAAW,CACXC,OAAQJ,SAAS,GACjBK,KAAML,SAAS,GACfM,SAAUN,SAAS,IAEnBO,YAAoC,GAArBJ,SAASG,SAAiB,eAAiB,eAE9DrB,IAAIuB,YAAY,CACZ,CAACC,IAAKF,YAAaG,UAAW,gBAC/BC,MAAK,SAASC,gBACN3B,IAAIuB,YAAY,CACnB,CAACC,IAAK,UAAWC,UAAW,UAC5B,CAACD,IAAK,+BAAgCC,UAAW,aAAcG,MAAOD,QAAQ,SAEnFD,MAAK,SAASC,gBAEN1B,gBAAgB4B,OAAO,CAC1BC,MAAOH,QAAQ,GACfI,KAAMJ,QAAQ,GACdK,MAAM,EACNC,eAAe,OAEpBP,MAAK,SAASQ,cAIbA,MAAMC,UAAUC,GAAGlC,YAAYmC,MAAM,YAtF3B,SAASnB,gBACjBoB,eAAiB,IAAIhC,QAAQ,oDAEnCD,UAAUkC,OAAO,eAAgB,IAAIb,MAAK,SAASc,aAE/ChC,eAAeiC,OAAOD,MAGfzC,KAAK2C,KAAK,CAAC,CACdC,WAAY,sDACZC,KAAM1B,YACN,MACLQ,MAAK,SAASmB,aACTC,gBAAmBD,QAAQE,MAAQ,EAAK,EAAI,EAG5CC,WAAaF,gBAAkB,wBAA0B,wBAC7D9C,IAAIiD,WAAWD,WAAY,aAAczC,cAAcmB,MAAK,SAASwB,iBAC7DC,OAAS,CACTJ,MAAOG,YACPE,KAAM,GACNC,KAAM7C,eAAe8C,KAAK,qBAC1BC,SAAU/C,eAAe8C,KAAK,6BAE3BtD,IAAIiD,WAAW,iBAAkB,aAAcE,WACvDzB,MAAK,SAAS8B,iBAnCiBC,SAoC1BC,mBAAqBlD,eAAe8C,KAAK,kCACtCjD,UAAUsD,WArCaF,SAqCkCC,mBAAjBZ,gBApCxC,EAAI,gBAAkBW,SAAW,cAAgB,gBAAkBA,SAAW,eAoCA,OAAQD,gBAC9F9B,MAAK,SAASc,UACToB,cAAgBd,gBAAkB,EAAI,EAAI,EAC9CtC,eAAeqD,KAAK,iBAAiBC,SACrCtD,eAAeG,KAAK,cAAeO,SAASC,OAAS,IAAMD,SAASE,KAAO,IAAMwC,eACjFpD,eAAe8C,KAAK,mBAAoBpC,SAASC,OAAS,IAAMD,SAASE,KAAO,IAAMwC,eACtFpD,eAAeuD,SAAS,OAAOC,YAAYxB,SAE5CyB,MAAM9D,aAAa+D,cAIzBxC,MAAK,KACFY,eAAe6B,aAEhBF,MAAM9D,aAAa+D,WA6CdE,CAAYlD,aAGTgB,SACR+B,MAAM9D,aAAa+D,kBAwBsC,CAC5DG,KAhBO,SAASC,UAChB/D,aAAe+D,SAGfxE,EAAE,sCAAsCyE,MAAK,SAASC,MAAOC,SACzDrE,aAAaP,OAAO4E,QAAS,CAACrE,aAAasE,OAAOC,cAKtD7E,EAAE,wBAAwBsC,GAAGhC,aAAasE,OAAOC,SAAU,iBAAiB,SAASjE,EAAGC,MACpFF,YAAYC,EAAGC"}PK{\g %%.progress/tests/report_progress_helper_test.phpnu[. namespace report_progress; use advanced_testcase; use completion_info; use testing_data_generator; /** * Class for testing report progress helper. * * @package report_progress * @covers \report_progress\local\helper * @copyright 2021 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class report_progress_helper_test extends advanced_testcase { /** @var testing_data_generator data generator.*/ protected $generator; /** * Set up testcase. */ public function setUp(): void { global $CFG; parent::setUp(); $CFG->enablecompletion = true; $this->setAdminUser(); $this->resetAfterTest(); $this->generator = $this->getDataGenerator(); } /** * Test process_activities_by_filter_options function. */ public function test_sort_activities(): void { $expectedactivitytypes = ['all' => 'All activities and resources', 'assign' => 'Assignments', 'quiz' => 'Quizzes']; // Generate test data. $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $quiz2 = $this->generator->create_module('quiz', ['course' => $course->id, 'name' => 'Quiz 2'], ['completion' => 1]); $quiz1 = $this->generator->create_module('quiz', ['course' => $course->id, 'name' => 'Quiz 1'], ['completion' => 1]); $assign1 = $this->generator->create_module('assign', ['course' => $course->id, 'name' => 'Assignment'], ['completion' => 1]); $completion = new completion_info($course); // Sort the activities by name. list($activitytypes, $activities) = \report_progress\local\helper::get_activities_to_show($completion, 'quiz', 'alphabetical'); // Check weather the result is filtered and sorted. $this->assertEquals(2, count($activities)); $this->assertEquals('Quiz 1', $activities[0]->name); $this->assertEquals('Quiz 2', $activities[1]->name); $this->assertEquals($activitytypes, $expectedactivitytypes); } /** * Test filtering by section. */ public function test_filter_activities_by_section(): void { $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1)); $this->generator->create_module('quiz', ['course' => $course->id, 'name' => 'Quiz 2', 'section' => 1], ['completion' => 1]); $this->generator->create_module('quiz', ['course' => $course->id, 'name' => 'Quiz 1', 'section' => 2], ['completion' => 1]); $this->generator->create_module('assign', ['course' => $course->id, 'name' => 'Assignment', 'section' => 2], ['completion' => 1]); $completion = new completion_info($course); // Get activities of section 0. list($activitytypes, $activities) = \report_progress\local\helper::get_activities_to_show($completion, '', '', 0); $this->assertEquals(0, count($activities)); $this->assertEquals($activitytypes, ['all' => 'All activities and resources']); // Get activities of section 1. list($activitytypes, $activities) = \report_progress\local\helper::get_activities_to_show($completion, '', '', 1); $this->assertEquals(1, count($activities)); $this->assertEquals('Quiz 2', array_shift($activities)->name); $this->assertEquals($activitytypes, ['all' => 'All activities and resources', 'quiz' => 'Quizzes']); // Get activities of section 2. list($activitytypes, $activities) = \report_progress\local\helper::get_activities_to_show($completion, '', '', 2); $this->assertEquals(2, count($activities)); $this->assertEquals('Quiz 1', array_shift($activities)->name); $this->assertEquals('Assignment', array_shift($activities)->name); $this->assertEquals( $activitytypes, ['all' => 'All activities and resources', 'assign' => 'Assignments', 'quiz' => 'Quizzes'] ); // Get assignments of section 2. list($activitytypes, $activities) = \report_progress\local\helper::get_activities_to_show($completion, 'assign', '', 2); $this->assertEquals(1, count($activities)); $this->assertEquals('Assignment', array_shift($activities)->name); $this->assertEquals( $activitytypes, ['all' => 'All activities and resources', 'assign' => 'Assignments', 'quiz' => 'Quizzes'] ); // Get assignments of section 1. list($activitytypes, $activities) = \report_progress\local\helper::get_activities_to_show($completion, 'assign', '', 1); $this->assertEquals(0, count($activities)); $this->assertEquals( $activitytypes, ['all' => 'All activities and resources', 'assign' => 'Assignments', 'quiz' => 'Quizzes'] ); } } PK{\ 3progress/tests/behat/course_report_progress.featurenu[@report @report_progress Feature: In a course administration page, navigate through report page, test for selector in activity completion In order to view and override a student's activity completion status As a teacher Go to course administration -> Reports -> Activity completion Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | | Course 1 | C1 | topics | 1 | And the following "activities" exist: | activity | name | intro | course | idnumber | section | completion | completionview | completionusegrade | assignsubmission_onlinetext_enabled | submissiondrafts | | assign | my assignment | A1 desc | C1 | assign1 | 0 | 1 | 0 | | 0 | 0 | | assign | my assignment 2 | A2 desc | C1 | assign2 | 0 | 2 | 1 | | 0 | 0 | | assign | my assignment 3 | A3 desc | C1 | assign3 | 0 | 2 | 1 | 1 | 1 | 0 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | One | teacher1@example.com | | student1 | Student | One | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | @javascript Scenario: Selector should be available in the course acitivity completion page Given I log in as "admin" And I am on "Course 1" course homepage When I navigate to "Reports" in current page administration And I click on "Activity completion" "link" Then "Report" "field" should exist in the "tertiary-navigation" "region" And I should see "Activity completion" in the "tertiary-navigation" "region" Scenario: Custom profile fields selected for identity should display on the activity completion report Given the following "custom profile fields" exist: | datatype | shortname | name | | text | frog | Favourite frog | And the following "users" exist: | username | firstname | lastname | profile_field_frog | | student2 | Student | Two | Kermit | And the following "course enrolments" exist: | user | course | role | | student2 | C1 | student | And the following config values are set as admin: | showuseridentity | email,profile_field_frog | When I am on the "C1" "Course" page logged in as "admin" And I navigate to "Reports" in current page administration And I click on "Activity completion" "link" Then I should see "Favourite frog" Then I should see "Kermit" in the "Student Two" "table_row" PK{\FF>progress/tests/behat/activity_completion_report_filter.featurenu[@report @report_progress Feature: Teacher can view and filter activity completion data by group, activity type and section. Background: Given the following "course" exists: | fullname | Course 1 | | shortname | C1 | | format | topics | | enablecompletion | 1 | | groupmode | 1 | | initsections | 1 | And the following "activities" exist: | activity | name | intro | course | idnumber | section | completion | completionview | | quiz | My quiz B | A3 desc | C1 | quizb | 0 | 2 | 1 | | quiz | My quiz A | A3 desc | C1 | quiza | 1 | 2 | 1 | | page | My page | A4 desc | C1 | page1 | 2 | 2 | 1 | | assign | My assignment | A1 desc | C1 | assign1 | 2 | 2 | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | One | teacher1@example.com | | student1 | Student | One | student1@example.com | | student2 | Student | Two | student2@example.com | And the following "groups" exist: | name | course | idnumber | | Group 1 | C1 | G1 | | Group 2 | C1 | G2 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "group members" exist: | user | group | | student1 | G1 | | student2 | G2 | | teacher1 | G1 | | teacher1 | G2 | @javascript Scenario: Teacher can view the activity completion report using filtering and sorting options. When I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to "Reports" in current page administration And I click on "Activity completion" "link" Then "My quiz B" "link" should appear before "My quiz A" "link" in the "completion-progress" "table" And I should see "My assignment" in the "completion-progress" "table" And I should see "My page" in the "completion-progress" "table" And I should see "Student One" in the "completion-progress" "table" And I should see "Student Two" in the "completion-progress" "table" And I set the field "Separate groups" to "Group 1" And I set the field "Include" to "Quizzes" And I set the field "Activity order" to "Alphabetical" And "My quiz A" "link" should appear before "My quiz B" "link" in the "completion-progress" "table" And I should not see "My assignment" in the "completion-progress" "table" And I should not see "My page" in the "completion-progress" "table" And I should see "Student One" in the "completion-progress" "table" And I should not see "Student Two" in the "completion-progress" "table" And I set the field "Separate groups" to "All participants" And I set the field "Include" to "All activities and resources" And I set the field "Section" to "Section 1" And I should not see "My assignment" in the "completion-progress" "table" And I should not see "My page" in the "completion-progress" "table" And I should not see "My assignment" in the "completion-progress" "table" And I should not see "My quiz B" in the "completion-progress" "table" And I should see "My quiz A" in the "completion-progress" "table" And I should see "Quiz" in the "activityinclude" "select" And I should not see "Page" in the "activityinclude" "select" And I set the field "Section" to "Section 2" And I should not see "Quiz" in the "activityinclude" "select" And I should see "Page" in the "activityinclude" "select" And I should see "Assignment" in the "activityinclude" "select" And I set the field "Include" to "Page" And I set the field "Section" to "Section 1" And I should see "Page" in the "activityinclude" "select" PK{\-H  7progress/tests/behat/activity_completion_report.featurenu[@report @report_progress Feature: Teacher can view and override users' activity completion data via the progress report. In order to view and override a student's activity completion status As a teacher I need to view the course progress report and click the respective completion status icon Background: Given the following "courses" exist: | fullname | shortname | format | enablecompletion | | Course 1 | C1 | topics | 1 | And the following "activities" exist: | activity | name | intro | course | idnumber | section | completion | completionview | completionusegrade | assignsubmission_onlinetext_enabled | submissiondrafts | | assign | my assignment | A1 desc | C1 | assign1 | 0 | 1 | 0 | | 0 | 0 | | assign | my assignment 2 | A2 desc | C1 | assign2 | 0 | 2 | 1 | | 0 | 0 | | assign | my assignment 3 | A3 desc | C1 | assign3 | 0 | 2 | 1 | 1 | 1 | 0 | And the following "users" exist: | username | firstname | lastname | email | idnumber | middlename | alternatename | firstnamephonetic | lastnamephonetic | | teacher1 | Teacher | One | teacher1@example.com | t1 | | fred | | | | student1 | Grainne | Beauchamp | student1@example.com | s1 | Ann | Jill | Gronya | Beecham | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following config values are set as admin: | fullnamedisplay | firstname | | alternativefullnameformat | middlename, alternatename, firstname, lastname | # Course comprising one activity with auto completion (student must view it) and one with manual completion. # This confirms that after being completed by the student and overridden by the teacher, that both activities can still be # completed again via normal mechanisms. @javascript Scenario: Given the status has been overridden, when a student tries to complete it again, completion can still occur. # Student completes the activities, manual and automatic completion. Given I am on the "Course 1" course page logged in as student1 And the manual completion button of "my assignment" is displayed as "Mark as done" And I toggle the manual completion state of "my assignment" And the manual completion button of "my assignment" is displayed as "Done" And I am on the "my assignment 2" "assign activity" page And the "View" completion condition of "my assignment 2" is displayed as "done" And I log out # Teacher overrides the activity completion statuses to incomplete. When I am on the "Course 1" course page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Activity completion" "link" And "Ann, Jill, Grainne, Beauchamp, my assignment: Completed" "icon" should exist in the "Ann, Jill, Grainne, Beauchamp" "table_row" And "Ann, Jill, Grainne, Beauchamp, my assignment 2: Completed" "icon" should exist in the "Ann, Jill, Grainne, Beauchamp" "table_row" And I click on "my assignment" "link" in the "Ann, Jill, Grainne, Beauchamp" "table_row" And I click on "Save changes" "button" And "Ann, Jill, Grainne, Beauchamp, my assignment: Not completed (set by Teacher)" "icon" should exist in the "Ann, Jill, Grainne, Beauchamp" "table_row" And I click on "my assignment 2" "link" in the "Ann, Jill, Grainne, Beauchamp" "table_row" And I click on "Save changes" "button" And "Ann, Jill, Grainne, Beauchamp, my assignment 2: Not completed (set by Teacher)" "icon" should exist in the "Ann, Jill, Grainne, Beauchamp" "table_row" And I log out # Student can now complete the activities again, via normal means. And I am on the "Course 1" course page logged in as student1 Then the manual completion button of "my assignment" overridden by "Teacher" is displayed as "Mark as done" And the "View" completion condition of "my assignment 2" overridden by "Teacher" is displayed as "todo" And I toggle the manual completion state of "my assignment" And the manual completion button of "my assignment" is displayed as "Done" And I am on the "my assignment 2" "assign activity" page And I am on "Course 1" course homepage And the "View" completion condition of "my assignment 2" is displayed as "done" And I log out # And the activity completion report should show the same. And I am on the "Course 1" Course page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Activity completion" "link" And "Ann, Jill, Grainne, Beauchamp, my assignment: Completed" "icon" should exist in the "Ann, Jill, Grainne, Beauchamp" "table_row" And "Ann, Jill, Grainne, Beauchamp, my assignment 2: Completed" "icon" should exist in the "Ann, Jill, Grainne, Beauchamp" "table_row" # Course comprising one activity with auto completion (student must view it and receive a grade) and one with manual completion. # This confirms that after being overridden to complete by the teacher, that the completion status for activities with automatic # completion can no longer be affected by any normal completion mechanisms triggered by the student. Manual completion unaffected. @javascript Scenario: Given the status has been overridden to complete, when a student triggers completion updates, the status remains fixed. # When the teacher overrides the activity completion statuses to complete. When I am on the "Course 1" Course page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Activity completion" "link" And "Ann, Jill, Grainne, Beauchamp, my assignment: Not completed" "icon" should exist in the "Ann, Jill, Grainne, Beauchamp" "table_row" And "Ann, Jill, Grainne, Beauchamp, my assignment 3: Not completed" "icon" should exist in the "Ann, Jill, Grainne, Beauchamp" "table_row" And I click on "my assignment" "link" in the "Ann, Jill, Grainne, Beauchamp" "table_row" And I click on "Save changes" "button" And "Ann, Jill, Grainne, Beauchamp, my assignment: Completed (set by Teacher)" "icon" should exist in the "Ann, Jill, Grainne, Beauchamp" "table_row" And I click on "my assignment 3" "link" in the "Ann, Jill, Grainne, Beauchamp" "table_row" And I click on "Save changes" "button" And "Ann, Jill, Grainne, Beauchamp, my assignment 3: Completed (set by Teacher)" "icon" should exist in the "Ann, Jill, Grainne, Beauchamp" "table_row" And I log out # Then as a student, confirm that automatic completion checks are no longer triggered (such as after an assign submission). And I am on the "Course 1" course page logged in as student1 Then the "Receive a grade" completion condition of "my assignment 3" overridden by "Teacher" is displayed as "done" And I am on the "my assignment 3" "assign activity" page And I press "Add submission" And I set the following fields to these values: | Online text | I'm the student first submission | And I press "Save changes" And I should see "Submitted for grading" And I am on "Course 1" course homepage And the "Receive a grade" completion condition of "my assignment 3" overridden by "Teacher" is displayed as "done" # And Confirm that manual completion changes are still allowed. And I am on "Course 1" course homepage And the manual completion button of "my assignment" overridden by "Teacher" is displayed as "Done" And I toggle the manual completion state of "my assignment" And the manual completion button of "my assignment" is displayed as "Mark as done" PK{\rМview.phpnu[. /** * Page to view the course reports * * @package core * @subpackage report * @copyright 2021 Sujith Haridasan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../config.php'); // Course id. $courseid = required_param('courseid', PARAM_INT); $PAGE->set_url(new moodle_url('/report/view.php', array('courseid' => $courseid))); // Basic access checks. if (!$course = $DB->get_record('course', array('id' => $courseid))) { throw new \moodle_exception('invalidcourseid'); } require_login($course); // Otherwise, output the page with a notification stating that there are no available course reports. $PAGE->set_title(get_string('reports')); $PAGE->set_pagelayout('incourse'); $PAGE->set_heading($course->fullname); $PAGE->set_pagetype('course-view-' . $course->format); $PAGE->add_body_class('limitedwidth'); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('reports')); // Check if there is at least one displayable report. $hasreports = false; if ($reportnode = $PAGE->settingsnav->find('coursereports', \navigation_node::TYPE_CONTAINER)) { foreach ($reportnode->children as $child) { if ($child->display) { $hasreports = true; break; } } } if ($hasreports) { echo $OUTPUT->render_from_template('core/report_link_page', ['node' => $reportnode]); } else { echo html_writer::div($OUTPUT->notification(get_string('noreports', 'debug'), 'error'), 'mt-3'); } echo $OUTPUT->footer(); PK{\cc&configlog/lang/en/report_configlog.phpnu[. /** * Strings for component 'report_configlog', language 'en', branch 'MOODLE_20_STABLE' * * @package report * @subplugin configlog * @copyright 2009 petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['configlog'] = 'Config changes'; $string['datefrom'] = 'Date from'; $string['dateto'] = 'Date to'; $string['entityconfigchange'] = 'Config change'; $string['plugin'] = 'Plugin'; $string['pluginname'] = 'Config changes'; $string['setting'] = 'Setting'; $string['timemodified'] = 'Date'; $string['user'] = 'User'; $string['usernone'] = 'CLI or install'; $string['user_help'] = 'Search by user first name or last name'; $string['value'] = 'Value'; $string['value_help'] = 'Search by new or original value of the configuration'; $string['valuenew'] = 'New value'; $string['valueold'] = 'Original value'; $string['privacy:metadata'] = 'The Config changes plugin does not store any personal data.'; PK{\VŌconfiglog/version.phpnu[. /** * Version details. * * @package report * @subpackage configlog * @copyright 2011 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_configlog'; // Full name of the plugin (used for diagnostics) PK{\U1f]]@configlog/classes/reportbuilder/local/entities/config_change.phpnu[. namespace report_configlog\reportbuilder\local\entities; use lang_string; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\helpers\format; use core_reportbuilder\local\report\column; use core_reportbuilder\local\report\filter; use core_reportbuilder\local\filters\date; use core_reportbuilder\local\filters\text; /** * Config change entity class implementation * * Defines all the columns and filters that can be added to reports that use this entity. * * @package report_configlog * @copyright 2020 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class config_change extends base { /** * Database tables that this entity uses * * @return string[] */ protected function get_default_tables(): array { return [ 'config_log', ]; } /** * The default title for this entity * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('entityconfigchange', 'report_configlog'); } /** * Initialize the entity * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } $filters = $this->get_all_filters(); foreach ($filters as $filter) { $this->add_filter($filter); } return $this; } /** * Returns list of all available columns * * @return column[] */ protected function get_all_columns(): array { $tablealias = $this->get_table_alias('config_log'); // Time modified column. $columns[] = (new column( 'timemodified', new lang_string('timemodified', 'report_configlog'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TIMESTAMP) ->add_fields("{$tablealias}.timemodified") ->set_is_sortable(true) ->add_callback([format::class, 'userdate']); // Plugin column. $columns[] = (new column( 'plugin', new lang_string('plugin', 'report_configlog'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$tablealias}.plugin") ->set_is_sortable(true) ->add_callback(static function(?string $plugin): string { return $plugin ?? 'core'; }); // Setting column. $columns[] = (new column( 'setting', new lang_string('setting', 'report_configlog'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$tablealias}.name") ->set_is_sortable(true); // New value column. $columns[] = (new column( 'newvalue', new lang_string('valuenew', 'report_configlog'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$tablealias}.value") ->set_is_sortable(true) ->add_callback(static function(?string $value): string { return format_text($value, FORMAT_PLAIN); }); // Old value column. $columns[] = (new column( 'oldvalue', new lang_string('valueold', 'report_configlog'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_field("{$tablealias}.oldvalue") ->set_is_sortable(true) ->add_callback(static function(?string $oldvalue): string { return format_text($oldvalue, FORMAT_PLAIN); }); return $columns; } /** * Return list of all available filters * * @return filter[] */ protected function get_all_filters(): array { $tablealias = $this->get_table_alias('config_log'); // Time modified filter. $filters[] = (new filter( date::class, 'timemodified', new lang_string('timemodified', 'report_configlog'), $this->get_entity_name(), "{$tablealias}.timemodified" )) ->add_joins($this->get_joins()) ->set_limited_operators([ date::DATE_ANY, date::DATE_RANGE, date::DATE_PREVIOUS, date::DATE_CURRENT, ]); // Plugin filter. $filters[] = (new filter( text::class, 'plugin', new lang_string('plugin', 'report_configlog'), $this->get_entity_name(), "COALESCE({$tablealias}.plugin, 'core')" )) ->add_joins($this->get_joins()); // Setting filter. $filters[] = (new filter( text::class, 'setting', new lang_string('setting', 'report_configlog'), $this->get_entity_name(), "{$tablealias}.name" )) ->add_joins($this->get_joins()); // New value filter. $filters[] = (new filter( text::class, 'value', new lang_string('valuenew', 'report_configlog'), $this->get_entity_name(), "{$tablealias}.value" )) ->add_joins($this->get_joins()); // Old value filter. $filters[] = (new filter( text::class, 'oldvalue', new lang_string('valueold', 'report_configlog'), $this->get_entity_name(), "{$tablealias}.oldvalue" )) ->add_joins($this->get_joins()); return $filters; } } PK{\0eZPPFconfiglog/classes/reportbuilder/local/systemreports/config_changes.phpnu[. namespace report_configlog\reportbuilder\local\systemreports; use context_system; use report_configlog\reportbuilder\local\entities\config_change; use core_reportbuilder\system_report; use core_reportbuilder\local\entities\user; use stdClass; /** * Config changes system report class implementation * * @package report_configlog * @copyright 2020 Paul Holden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class config_changes extends system_report { /** * Initialise report, we need to set the main table, load our entities and set columns/filters */ protected function initialise(): void { // Our main entity, it contains all of the column definitions that we need. $entitymain = new config_change(); $entitymainalias = $entitymain->get_table_alias('config_log'); $this->set_main_table('config_log', $entitymainalias); $this->add_entity($entitymain); // We can join the "user" entity to our "main" entity using standard SQL JOIN. $entityuser = new user(); $entityuseralias = $entityuser->get_table_alias('user'); $this->add_entity($entityuser ->add_join("LEFT JOIN {user} {$entityuseralias} ON {$entityuseralias}.id = {$entitymainalias}.userid") ); // Now we can call our helper methods to add the content we want to include in the report. $this->add_columns(); $this->add_filters(); // Set if report can be downloaded. $this->set_downloadable(true, get_string('pluginname', 'report_configlog')); } /** * Validates access to view this report * * @return bool */ protected function can_view(): bool { return has_capability('moodle/site:config', context_system::instance()); } /** * Adds the columns we want to display in the report * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier */ protected function add_columns(): void { $columns = [ 'config_change:timemodified', 'user:fullnamewithlink', 'config_change:plugin', 'config_change:setting', 'config_change:newvalue', 'config_change:oldvalue', ]; $this->add_columns_from_entities($columns); // Default sorting. $this->set_initial_sort_column('config_change:timemodified', SORT_DESC); // Custom callback to show 'CLI or install' in fullname column when there is no user. if ($column = $this->get_column('user:fullnamewithlink')) { $column->add_callback(static function(string $fullname, stdClass $row): string { return $fullname ?: get_string('usernone', 'report_configlog'); }); } } /** * Adds the filters we want to display in the report * * They are all provided by the entities we previously added in the {@see initialise} method, referencing each by their * unique identifier */ protected function add_filters(): void { $filters = [ 'config_change:plugin', 'config_change:setting', 'config_change:value', 'config_change:oldvalue', 'user:fullname', 'config_change:timemodified', ]; $this->add_filters_from_entities($filters); } } PK{\U&configlog/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_configlog. * * @package report_configlog * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_configlog\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_configlog implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\A$$configlog/index.phpnu[. /** * Config changes report * * @package report * @subpackage configlog * @copyright 2009 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_reportbuilder\system_report_factory; use core_reportbuilder\local\filters\text; use report_configlog\reportbuilder\local\systemreports\config_changes; require(__DIR__.'/../../config.php'); require_once($CFG->libdir.'/adminlib.php'); // Allow searching by setting when providing parameter directly. $search = optional_param('search', '', PARAM_TEXT); admin_externalpage_setup('reportconfiglog', '', ['search' => $search], '', ['pagelayout' => 'report']); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('configlog', 'report_configlog')); // Create out report instance, setting initial filtering if required. $report = system_report_factory::create(config_changes::class, context_system::instance()); if (!empty($search)) { $report->set_filter_values([ 'config_change:setting_operator' => text::IS_EQUAL_TO, 'config_change:setting_value' => $search, ]); } echo $report->output(); echo $OUTPUT->footer(); PK{\y )configlog/tests/behat/view_report.featurenu[@report @report_configlog @core_reportbuilder Feature: In a report, admin can see configuration changes In order see configuration changes As an admin I need to view the configuration changes report and use search to filter the report # Set some config values so the report contains known data. Background: Given I log in as "admin" And I change the window size to "large" And I set the following administration settings values: | Initial number of overall feedback fields | 5 | | Maximum folder download size | 2048 | | Default city | Perth | @javascript Scenario: Display configuration changes report When I navigate to "Reports > Config changes" in site administration Then the following should exist in the "reportbuilder-table" table: | First name | Plugin | Setting | New value | Original value | | Admin User | quiz | initialnumfeedbacks | 5 | 2 | | Admin User | folder | maxsizetodownload | 2048 | 0 | | Admin User | core | defaultcity | Perth | | @javascript Scenario Outline: Search configuration changes report When I navigate to "Reports > Config changes" in site administration And I click on "Filters" "button" And I set the following fields in the "" "core_reportbuilder > Filter" to these values: | operator | Contains | | value | | And I click on "Apply" "button" in the "[data-region='report-filters']" "css_element" And I should see "Filters applied" Then the following should exist in the "reportbuilder-table" table: | Plugin | Setting | New value | | | | | And I should not see "" in the "reportbuilder-table" "table" Examples: | field | search | plugin | setting | value | excluded | | Plugin | folder | folder | maxsizetodownload | 2048 | quiz | | Setting | initialnumfeedbacks | quiz | initialnumfeedbacks | 5 | maxsizetodownload | | Setting | maxsizetodownload | folder | maxsizetodownload | 2048 | initialnumfeedbacks | | New value | Perth | core | defaultcity | Perth | maxsizetodownload | | Full name | Admin User | core | defaultcity | Perth | zzzzzzzzz | PK{\fOPPconfiglog/settings.phpnu[. /** * Report settings * * @package report * @subpackage configlog * @copyright 2009 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $ADMIN->add('reports', new admin_externalpage('reportconfiglog', get_string('configlog', 'report_configlog'), "$CFG->wwwroot/report/configlog/index.php")); // no report settings $settings = null; PK{\log/styles.cssnu[#page-report-log-index .info { margin: 10px; } #page-report-log-index .logselectform { margin: 10px auto; } #page-report-log-user .info { margin: 10px; text-align: center; } PK{\klog/db/install.phpnu[. /** * Post installation and migration code. * * Contains code that are run during the installation of report/logs * * @package report_log * @copyright 2011 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * Contains codes to be run during installation of report/logs * * @global moodle_database $DB * @return void */ function xmldb_report_log_install() { global $DB; } PK{\#Blog/db/access.phpnu[. /** * Capabilities * * Defines capablities related to logs * * @package report_log * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'report/log:view' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'coursereport/log:view', ), 'report/log:viewtoday' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'coursereport/log:viewtoday', ) ); PK{\Zlog/lang/en/report_log.phpnu[. /** * Lang strings. * * Language strings to be used by report/logs * * @package report_log * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['allsources'] = 'All sources'; $string['cli'] = 'CLI'; $string['eventcomponent'] = 'Component'; $string['eventcontext'] = 'Event context'; $string['eventloggedas'] = '{$a->realusername} as {$a->asusername}'; $string['eventorigin'] = 'Origin'; $string['eventrelatedfullnameuser'] = 'Affected user'; $string['eventreportviewed'] = 'Log report viewed'; $string['eventuserreportviewed'] = 'User log report viewed'; $string['log:view'] = 'View course logs'; $string['log:viewtoday'] = 'View today\'s logs'; $string['page'] = 'Page {$a}'; $string['logsformat'] = 'Logs format'; $string['missingcontext'] = 'Deleted {$a->name} (id \'{$a->instanceid}\')'; $string['missingcourse'] = 'Missing course (id \'{$a->instanceid}\')'; $string['nocapability'] = 'Can not access user log report'; $string['nologreaderenabled'] = 'No log reader enabled'; $string['origin'] = 'Source'; $string['other'] = 'Other'; $string['page-report-log-x'] = 'Any log report'; $string['page-report-log-index'] = 'Course log report'; $string['page-report-log-user'] = 'User course log report'; $string['pluginname'] = 'Logs'; $string['restore'] = 'Restore'; $string['selectlogreader'] = 'Select log reader'; $string['web'] = 'Web'; $string['ws'] = 'Web service'; $string['privacy:metadata'] = 'The Logs plugin does not store any personal data.'; PK{\. /** * Public API of the log report. * * Defines the APIs used by log reports * * @package report_log * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\report_helper; defined('MOODLE_INTERNAL') || die; /** * This function extends the navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $course The course to object for the report * @param stdClass $context The context of the course */ function report_log_extend_navigation_course($navigation, $course, $context) { if ( has_capability('report/log:view', $context) && report_helper::has_valid_group($context) ) { $url = new moodle_url('/report/log/index.php', array('id'=>$course->id)); $navigation->add(get_string('pluginname', 'report_log'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/report', '')); } } /** * Callback to verify if the given instance of store is supported by this report or not. * * @param string $instance store instance. * * @return bool returns true if the store is supported by the report, false otherwise. */ function report_log_supports_logstore($instance) { if ($instance instanceof \core\log\sql_reader) { return true; } return false; } /** * This function extends the course navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $user * @param stdClass $course The course to object for the report */ function report_log_extend_navigation_user($navigation, $user, $course) { list($all, $today) = report_log_can_access_user_report($user, $course); if ($today) { $url = new moodle_url('/report/log/user.php', array('id'=>$user->id, 'course'=>$course->id, 'mode'=>'today')); $navigation->add(get_string('todaylogs'), $url); } if ($all) { $url = new moodle_url('/report/log/user.php', array('id'=>$user->id, 'course'=>$course->id, 'mode'=>'all')); $navigation->add(get_string('alllogs'), $url); } } /** * Is current user allowed to access this report * * @access private defined in lib.php for performance reasons * @global stdClass $USER * @param stdClass $user * @param stdClass $course * @return array with two elements $all, $today */ function report_log_can_access_user_report($user, $course) { global $USER; $coursecontext = context_course::instance($course->id); $personalcontext = context_user::instance($user->id); if ($user->id == $USER->id) { if ($course->showreports and (is_viewing($coursecontext, $USER) or is_enrolled($coursecontext, $USER))) { return array(true, true); } } else if (has_capability('moodle/user:viewuseractivitiesreport', $personalcontext)) { if ($course->showreports and (is_viewing($coursecontext, $user) or is_enrolled($coursecontext, $user))) { return array(true, true); } } // Check if $USER shares group with $user (in case separated groups are enabled and 'moodle/site:accessallgroups' is disabled). if (!groups_user_groups_visible($course, $user->id)) { return array(false, false); } $today = false; $all = false; if (has_capability('report/log:viewtoday', $coursecontext)) { $today = true; } if (has_capability('report/log:view', $coursecontext)) { $all = true; } return array($all, $today); } /** * This function extends the module navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $cm */ function report_log_extend_navigation_module($navigation, $cm) { if ( has_capability('report/log:view', context_course::instance($cm->course)) && report_helper::has_valid_group(context_module::instance($cm->id)) ) { $url = new moodle_url('/report/log/index.php', [ 'chooselog' => '1', 'id' => $cm->course, 'modid' => $cm->id, 'isactivitypage' => '1', ]); $navigation->add(get_string('logs'), $url, navigation_node::TYPE_SETTING, null, 'logreport', new pix_icon('i/report', '')); } } /** * Return a list of page types * * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block * @return array a list of page types */ function report_log_page_type_list($pagetype, $parentcontext, $currentcontext) { $array = array( '*' => get_string('page-x', 'pagetype'), 'report-*' => get_string('page-report-x', 'pagetype'), 'report-log-*' => get_string('page-report-log-x', 'report_log'), 'report-log-index' => get_string('page-report-log-index', 'report_log'), 'report-log-user' => get_string('page-report-log-user', 'report_log') ); return $array; } /** * Add nodes to myprofile page. * * @param \core_user\output\myprofile\tree $tree Tree object * @param stdClass $user user object * @param bool $iscurrentuser * @param stdClass $course Course object * * @return bool */ function report_log_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) { if (empty($course)) { // We want to display these reports under the site context. $course = get_fast_modinfo(SITEID)->get_course(); } list($all, $today) = report_log_can_access_user_report($user, $course); if ($today) { // Today's log. $url = new moodle_url('/report/log/user.php', array('id' => $user->id, 'course' => $course->id, 'mode' => 'today')); $node = new core_user\output\myprofile\node('reports', 'todayslogs', get_string('todaylogs'), null, $url); $tree->add_node($node); } if ($all) { // All logs. $url = new moodle_url('/report/log/user.php', array('id' => $user->id, 'course' => $course->id, 'mode' => 'all')); $node = new core_user\output\myprofile\node('reports', 'alllogs', get_string('alllogs'), null, $url); $tree->add_node($node); } return true; } PK{\#log/version.phpnu[. /** * Version info * * This File contains information about the current version of report/logs * * @package report_log * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_log'; // Full name of the plugin (used for diagnostics) PK{\WbB.B.log/classes/renderer.phpnu[. /** * Log report renderer. * * @package report_log * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * Report log renderer's for printing reports. * * @package report_log * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_log_renderer extends plugin_renderer_base { /** * This method should never be manually called, it should only be called by process. * * @deprecated since 2.8, to be removed in 2.9 * @param report_log_renderable $reportlog * @return string */ public function render_report_log_renderable(report_log_renderable $reportlog) { debugging('Do not call this method. Please call $renderer->render($reportlog) instead.', DEBUG_DEVELOPER); return $this->render($reportlog); } /** * Render log report page. * * @param report_log_renderable $reportlog object of report_log. */ protected function render_report_log(report_log_renderable $reportlog) { if (empty($reportlog->selectedlogreader)) { echo $this->output->notification(get_string('nologreaderenabled', 'report_log'), 'notifyproblem'); return; } if ($reportlog->showselectorform) { $this->report_selector_form($reportlog); } if ($reportlog->showreport) { $reportlog->tablelog->out($reportlog->perpage, true); } } /** * Prints/return reader selector * * @param report_log_renderable $reportlog log report. */ public function reader_selector(report_log_renderable $reportlog) { $readers = $reportlog->get_readers(true); if (empty($readers)) { $readers = array(get_string('nologreaderenabled', 'report_log')); } $url = fullclone ($reportlog->url); $url->remove_params(array('logreader')); $select = new single_select($url, 'logreader', $readers, $reportlog->selectedlogreader, null); $select->set_label(get_string('selectlogreader', 'report_log')); echo $this->output->render($select); } /** * This function is used to generate and display selector form * * @param report_log_renderable $reportlog log report. */ public function report_selector_form(report_log_renderable $reportlog) { echo html_writer::start_tag('form', array('class' => 'logselecform', 'action' => $reportlog->url, 'method' => 'get')); echo html_writer::start_div('d-flex flex-wrap align-items-center gap-2'); echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'chooselog', 'value' => '1')); echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'showusers', 'value' => $reportlog->showusers)); echo html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'showcourses', 'value' => $reportlog->showcourses)); $selectedcourseid = empty($reportlog->sitecoursefilter) ? (empty($reportlog->course) ? 0 : $reportlog->course->id) : $reportlog->sitecoursefilter; echo $this->get_course_selector_field($reportlog, $selectedcourseid); // Add group selector. $groups = $reportlog->get_group_list(); if (!empty($groups)) { echo html_writer::label(get_string('selectagroup'), 'menugroup', false, array('class' => 'accesshide')); echo html_writer::select($groups, "group", $reportlog->groupid, get_string("allgroups")); } // Add user selector. $users = $reportlog->get_user_list(); if ($reportlog->showusers) { echo html_writer::label(get_string('selctauser'), 'menuuser', false, array('class' => 'accesshide')); echo html_writer::select($users, "user", $reportlog->userid, get_string("allparticipants")); } else { $users = array(); if (!empty($reportlog->userid)) { $users[$reportlog->userid] = $reportlog->get_selected_user_fullname(); } else { $users[0] = get_string('allparticipants'); } echo html_writer::label(get_string('selctauser'), 'menuuser', false, array('class' => 'accesshide')); echo html_writer::select($users, "user", $reportlog->userid, false); $a = new stdClass(); $a->url = new moodle_url('/report/log/index.php', array('chooselog' => 0, 'group' => $reportlog->get_selected_group(), 'user' => $reportlog->userid, 'id' => $selectedcourseid, 'date' => $reportlog->date, 'modid' => $reportlog->modid, 'showusers' => 1, 'showcourses' => $reportlog->showcourses)); $a->url = $a->url->out(false); echo html_writer::start_span('mx-1'); print_string('logtoomanyusers', 'moodle', $a); echo html_writer::end_span(); } // Add date selector. $dates = $reportlog->get_date_options(); echo html_writer::label(get_string('date'), 'menudate', false, array('class' => 'accesshide')); echo html_writer::select($dates, "date", $reportlog->date, get_string("alldays")); // Add activity selector. echo $this->get_activity_selector_field($reportlog); // Add actions selector. echo html_writer::label(get_string('actions'), 'menumodaction', false, array('class' => 'accesshide')); echo html_writer::select($reportlog->get_actions(), 'modaction', $reportlog->action, get_string("allactions")); // Add origin. $origin = $reportlog->get_origin_options(); echo html_writer::label(get_string('origin', 'report_log'), 'menuorigin', false, array('class' => 'accesshide')); echo html_writer::select($origin, 'origin', $reportlog->origin, false); // Add edulevel. $edulevel = $reportlog->get_edulevel_options(); echo html_writer::label(get_string('edulevel'), 'menuedulevel', false, array('class' => 'accesshide')); echo html_writer::select($edulevel, 'edulevel', $reportlog->edulevel, false) .$this->help_icon('edulevel'); // Add reader option. // If there is some reader available then only show submit button. $readers = $reportlog->get_readers(true); if (!empty($readers)) { if (count($readers) == 1) { $attributes = array('type' => 'hidden', 'name' => 'logreader', 'value' => key($readers)); echo html_writer::empty_tag('input', $attributes); } else { echo html_writer::label(get_string('selectlogreader', 'report_log'), 'menureader', false, array('class' => 'accesshide')); echo html_writer::select($readers, 'logreader', $reportlog->selectedlogreader, false); } echo html_writer::end_div(); echo html_writer::start_div('mt-2'); echo html_writer::empty_tag('input', array('type' => 'submit', 'value' => get_string('gettheselogs'), 'class' => 'btn btn-primary')); echo html_writer::end_div(); } echo html_writer::end_tag('form'); } /** * Generates the course selector field for the log report. * * @param report_log_renderable $reportlog The output instance. * @param int $selectedcourseid * @return string */ private function get_course_selector_field(report_log_renderable $reportlog, int $selectedcourseid): string { // When the log report is accessed vie an activity, we do not need a course selector. if ($reportlog->isactivitypage) { return html_writer::empty_tag( 'input', ['type' => 'hidden', 'name' => 'id', 'value' => $selectedcourseid] ); } $result = ''; $sitecontext = context_system::instance(); $courses = $reportlog->get_course_list(); if (!empty($courses) && $reportlog->showcourses) { $result .= html_writer::label(get_string('selectacourse'), 'menuid', false, ['class' => 'accesshide']); $result .= html_writer::select($courses, "id", $selectedcourseid, null); return $result; } $courses = []; $courseinfo = ($selectedcourseid == SITEID) ? ' (' . get_string('site') . ') ' : ''; $courses[$selectedcourseid] = get_course_display_name_for_list($reportlog->course) . $courseinfo; $result .= html_writer::label(get_string('selectacourse'), 'menuid', false, ['class' => 'accesshide']); $result .= html_writer::select($courses, "id", $selectedcourseid, false); // Check if user is admin and this came because of limitation on number of courses to show in dropdown. if (has_capability('report/log:view', $sitecontext)) { $a = new stdClass(); $a->url = new moodle_url( '/report/log/index.php', [ 'chooselog' => 0, 'group' => $reportlog->get_selected_group(), 'user' => $reportlog->userid, 'id' => $selectedcourseid, 'date' => $reportlog->date, 'modid' => $reportlog->modid, 'showcourses' => 1, 'showusers' => $reportlog->showusers, ] ); $a->url = $a->url->out(false); $result .= get_string('logtoomanycourses', 'moodle', $a); } return $result; } /** * Generates the activity selector field for the log report. * * @param report_log_renderable $reportlog The output instance. * @return string */ private function get_activity_selector_field(report_log_renderable $reportlog): string { $result = ''; // When the log report is accessed vie an activity, we do not need an activity selector. if ($reportlog->isactivitypage) { $result .= html_writer::empty_tag( 'input', ['type' => 'hidden', 'name' => 'isactivitypage', 'value' => $reportlog->isactivitypage] ); $result .= html_writer::empty_tag( 'input', ['type' => 'hidden', 'name' => 'modid', 'value' => $reportlog->modid] ); return $result; } [$activities, $disabled] = $reportlog->get_activities_list(); $result .= html_writer::label( text: get_string('activities'), for: 'menumodid', colonize: false, attributes: ['class' => 'accesshide'], ); $result .= html_writer::select( options: $activities, name: "modid", selected: $reportlog->modid, nothing: get_string("allactivities"), attributes: [], disabled: $disabled, ); return $result; } } PK{\uXuXlog/classes/renderable.phpnu[. /** * Log report renderer. * * @package report_log * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; use core\log\manager; /** * Report log renderable class. * * @package report_log * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_log_renderable implements renderable { /** @var manager log manager */ protected $logmanager; /** @var string selected log reader pluginname */ public $selectedlogreader = null; /** @var int page number */ public $page; /** @var int perpage records to show */ public $perpage; /** @var stdClass course record */ public $course; /** @var moodle_url url of report page */ public $url; /** @var int selected date from which records should be displayed */ public $date; /** @var int selected user id for which logs are displayed */ public $userid; /** @var int selected moduleid */ public $modid; /** @var string selected action filter */ public $action; /** @var int educational level */ public $edulevel; /** @var bool show courses */ public $showcourses; /** @var bool show users */ public $showusers; /** @var bool show report */ public $showreport; /** @var bool show selector form */ public $showselectorform; /** @var string selected log format */ public $logformat; /** @var string order to sort */ public $order; /** @var string origin to filter event origin */ public $origin; /** @var int group id */ public $groupid; /** @var int forces the use of a course filter in site context */ public $sitecoursefilter; /** @var table_log table log which will be used for rendering logs */ public $tablelog; /** @var array Index of delegated sections (indexed by component and itemid) */ protected $delegatedbycm; /** @var bool if the page is activity page */ public $isactivitypage; /** * Constructor. * * @param string $logreader (optional)reader pluginname from which logs will be fetched. * @param stdClass|int $course (optional) course record or id * @param int $userid (optional) id of user to filter records for. * @param int|string $modid (optional) module id or site_errors for filtering errors. * @param string $action (optional) action name to filter. * @param int $groupid (optional) groupid of user. * @param int $edulevel (optional) educational level. * @param bool $showcourses (optional) show courses. * @param bool $showusers (optional) show users. * @param bool $showreport (optional) show report. * @param bool $showselectorform (optional) show selector form. * @param moodle_url|string $url (optional) page url. * @param int $date date (optional) timestamp of start of the day for which logs will be displayed. * @param string $logformat log format. * @param int $page (optional) page number. * @param int $perpage (optional) number of records to show per page. * @param string $order (optional) sortorder of fetched records * @param string $origin (optional) origin of the event. * @param bool $isactivitypage (optional) if the page is activity page. * @param int $sitecoursefilter (optional) use a course filter in site context. */ public function __construct( $logreader = "", $course = 0, $userid = 0, $modid = 0, $action = "", $groupid = 0, $edulevel = -1, $showcourses = false, $showusers = false, $showreport = true, $showselectorform = true, $url = "", $date = 0, $logformat='showashtml', $page = 0, $perpage = 100, $order = "timecreated ASC", $origin ='', bool $isactivitypage = false, $sitecoursefilter = 0, ) { global $PAGE, $SITE; // Use first reader as selected reader, if not passed. if (empty($logreader)) { $readers = $this->get_readers(); if (!empty($readers)) { reset($readers); $logreader = key($readers); } else { $logreader = null; } } // Use page url if empty. if (empty($url)) { $url = new moodle_url($PAGE->url); } else { $url = new moodle_url($url); } $this->selectedlogreader = $logreader; $url->param('logreader', $logreader); // Use site course id, if course is empty. if (!empty($course) && is_int($course)) { $courseid = $course; try { $course = get_course($courseid); } catch (dml_missing_record_exception) { // Missing courses may have be deleted, so display them in site context. $course = $SITE; $sitecoursefilter = $courseid; } } $this->course = $course; $this->userid = $userid; $this->date = $date; $this->page = $page; $this->perpage = $perpage; $this->url = $url; $this->order = $order; $this->modid = $modid; $this->action = $action; $this->groupid = $groupid; $this->edulevel = $edulevel; $this->showcourses = $showcourses; $this->showusers = $showusers; $this->showreport = $showreport; $this->showselectorform = $showselectorform; $this->logformat = $logformat; $this->origin = $origin; $this->isactivitypage = $isactivitypage; $this->sitecoursefilter = $sitecoursefilter; } /** * Get a list of enabled sql_reader objects/name * * @param bool $nameonly if true only reader names will be returned. * @return array core\log\sql_reader object or name. */ public function get_readers($nameonly = false) { if (!isset($this->logmanager)) { $this->logmanager = get_log_manager(); } $readers = $this->logmanager->get_readers('core\log\sql_reader'); if ($nameonly) { foreach ($readers as $pluginname => $reader) { $readers[$pluginname] = $reader->get_name(); } } return $readers; } /** * Helper function to return list of activities to show in selection filter. * * @return array list of activities. */ public function get_activities_list() { $activities = []; $disabled = []; // For site just return site errors option. $sitecontext = context_system::instance(); if ($this->course->id == SITEID && has_capability('report/log:view', $sitecontext)) { $activities["site_errors"] = get_string("siteerrors"); return [$activities, $disabled]; } $modinfo = get_fast_modinfo($this->course); if (!$this->delegatedbycm) { $this->delegatedbycm = $modinfo->get_sections_delegated_by_cm(); } if (!empty($modinfo->cms)) { $section = 0; $thissection = array(); foreach ($modinfo->cms as $cm) { if (!$modname = $this->get_activity_name($cm)) { continue; } if ($cm->sectionnum > 0 and $section <> $cm->sectionnum) { $sectioninfo = $modinfo->get_section_info($cm->sectionnum); // Don't show subsections here. We are showing them in the corresponding module. if ($sectioninfo->get_component_instance()) { continue; } $activities[] = $thissection; $thissection = array(); } $section = $cm->sectionnum; $key = get_section_name($this->course, $cm->sectionnum); if (!isset($thissection[$key])) { $thissection[$key] = []; } $thissection[$key][$cm->id] = $modname; // Check if the module is delegating a section. if (array_key_exists($cm->id, $this->delegatedbycm)) { $delegated = $this->delegatedbycm[$cm->id]; $modules = (empty($delegated->sequence)) ? [] : explode(',', $delegated->sequence); $thissection[$key] = $thissection[$key] + $this->get_delegated_section_activities($modinfo, $modules); $disabled[] = $cm->id; } } if (!empty($thissection)) { $activities[] = $thissection; } } return [$activities, $disabled]; } /** * Helper function to return list of activities in a delegated section. * * @param course_modinfo $modinfo * @param array $cms List of cm ids in the section. * @return array list of activities. */ protected function get_delegated_section_activities(course_modinfo $modinfo, array $cmids): array { $activities = []; $indenter = '    '; foreach ($cmids as $cmid) { $cm = $modinfo->cms[$cmid]; if ($modname = $this->get_activity_name($cm)) { $activities[$cmid] = $indenter.$modname; } } return $activities; } /** * Helper function to return the name to show in the dropdown. * * @param cm_info $cm * @return string The name. */ private function get_activity_name(cm_info $cm): string { // Exclude activities that aren't visible or have no view link (e.g. label). Account for folders displayed inline. // Activities delegating sections might not have a URL, but should be return a name to be shown. $tobeshown = (strcmp($cm->modname, 'folder') == 0) || array_key_exists($cm->id, $this->delegatedbycm); if (!$cm->uservisible || (!$cm->has_view() && !$tobeshown)) { return ''; } $modname = strip_tags($cm->get_formatted_name()); if (core_text::strlen($modname) > 55) { $modname = core_text::substr($modname, 0, 50)."..."; } if (!$cm->visible) { $modname = "(".$modname.")"; } return $modname; } /** * Helper function to get selected group. * * @return int selected group. */ public function get_selected_group() { global $SESSION, $USER; // No groups for system. if (empty($this->course)) { return 0; } $context = context_course::instance($this->course->id); $selectedgroup = 0; // Setup for group handling. $groupmode = groups_get_course_groupmode($this->course); if ($groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { if (isset($SESSION->currentgroup[$this->course->id])) { $selectedgroup = $SESSION->currentgroup[$this->course->id]; } else if ($this->groupid > 0) { $SESSION->currentgroup[$this->course->id] = $this->groupid; $selectedgroup = $this->groupid; } } else if ($groupmode) { $selectedgroup = $this->groupid; } return $selectedgroup; } /** * Return list of actions for log reader. * * @todo MDL-44528 Get list from log_store. * @return array list of action options. */ public function get_actions() { $actions = array( 'c' => get_string('create'), 'r' => get_string('view'), 'u' => get_string('update'), 'd' => get_string('delete'), 'cud' => get_string('allchanges') ); return $actions; } /** * Return selected user fullname. * * @return string user fullname. */ public function get_selected_user_fullname() { $user = core_user::get_user($this->userid); if (empty($this->course)) { // We are in system context. $context = context_system::instance(); } else { // We are in course context. $context = context_course::instance($this->course->id); } return fullname($user, has_capability('moodle/site:viewfullnames', $context)); } /** * Return list of courses to show in selector. * * @return array list of courses. */ public function get_course_list() { global $DB, $SITE; $courses = array(); $sitecontext = context_system::instance(); // First check to see if we can override showcourses and showusers. $numcourses = $DB->count_records("course"); if ($numcourses < COURSE_MAX_COURSES_PER_DROPDOWN && !$this->showcourses) { $this->showcourses = 1; } // Check if course filter should be shown. if (has_capability('report/log:view', $sitecontext) && $this->showcourses) { if ($courserecords = $DB->get_records("course", null, "fullname", "id,shortname,fullname,category")) { foreach ($courserecords as $course) { if ($course->id == SITEID) { $courses[$course->id] = format_string($course->fullname) . ' (' . get_string('site') . ')'; } else { $courses[$course->id] = format_string(get_course_display_name_for_list($course)); } } } // If filtering by a missing course, add a placeholder. if (!empty($this->sitecoursefilter)) { $courses[$this->sitecoursefilter] = get_string('missingcourse', 'report_log', [ 'instanceid' => $this->sitecoursefilter, ]); } core_collator::asort($courses); } return $courses; } /** * Return list of groups that are used in this course. This is done when groups are used in the course * and the user is allowed to see all groups or groups are visible anyway. If groups are used but the * mode is separate groups and the user is not allowed to see all groups, the list contains the groups * only, where the user is member. * If the course uses no groups, the list is empty. * * @return array list of groups. */ public function get_group_list() { global $USER; // No groups for system. if (empty($this->course)) { return []; } $context = context_course::instance($this->course->id); $groupmode = groups_get_course_groupmode($this->course); $grouplist = []; $userid = $groupmode == SEPARATEGROUPS ? $USER->id : 0; if (has_capability('moodle/site:accessallgroups', $context)) { $userid = 0; } $cgroups = groups_get_all_groups($this->course->id, $userid); if (!empty($cgroups)) { $grouplist = array_column($cgroups, 'name', 'id'); } return $grouplist; } /** * Return list of users. * * @return array list of users. */ public function get_user_list() { global $CFG, $SITE; $courseid = $SITE->id; if (!empty($this->course)) { $courseid = $this->course->id; } $context = context_course::instance($courseid); $limitfrom = empty($this->showusers) ? 0 : ''; $limitnum = empty($this->showusers) ? COURSE_MAX_USERS_PER_DROPDOWN + 1 : ''; $userfieldsapi = \core_user\fields::for_name(); // Get the groups of that course that the user can see. $groups = $this->get_group_list(); $groupids = array_keys($groups); // Now doublecheck the value of groupids and deal with special case like USERWITHOUTGROUP. $groupmode = groups_get_course_groupmode($this->course); if ( has_capability('moodle/site:accessallgroups', $context) || $groupmode != SEPARATEGROUPS || empty($groupids) ) { $groupids[] = USERSWITHOUTGROUP; } // First case, the user has selected a group and user is in this group. if ($this->groupid > 0) { if (!isset($groups[$this->groupid])) { // The user is not in this group, so we will ignore the group selection. $groupids = 0; } else { $groupids = [$this->groupid]; } } $courseusers = get_enrolled_users($context, '', $groupids, 'u.id, ' . $userfieldsapi->get_sql('u', false, '', '', false)->selects, null, $limitfrom, $limitnum); if (count($courseusers) < COURSE_MAX_USERS_PER_DROPDOWN && !$this->showusers) { $this->showusers = 1; } $users = array(); if ($this->showusers) { if ($courseusers) { foreach ($courseusers as $courseuser) { $users[$courseuser->id] = fullname($courseuser, has_capability('moodle/site:viewfullnames', $context)); } } $users[$CFG->siteguest] = get_string('guestuser'); } return $users; } /** * Return list of date options. * * @return array date options. */ public function get_date_options() { global $SITE; $strftimedate = get_string("strftimedate"); $strftimedaydate = get_string("strftimedaydate"); // Get all the possible dates. // Note that we are keeping track of real (GMT) time and user time. // User time is only used in displays - all calcs and passing is GMT. $timenow = time(); // GMT. // What day is it now for the user, and when is midnight that day (in GMT). $timemidnight = usergetmidnight($timenow); // Put today up the top of the list. $dates = array("$timemidnight" => get_string("today").", ".userdate($timenow, $strftimedate) ); // If course is empty, get it from frontpage. $course = $SITE; if (!empty($this->course)) { $course = $this->course; } if (!$course->startdate or ($course->startdate > $timenow)) { $course->startdate = $course->timecreated; } $numdates = 1; while ($timemidnight > $course->startdate and $numdates < 365) { $timemidnight = $timemidnight - 86400; $timenow = $timenow - 86400; $dates["$timemidnight"] = userdate($timenow, $strftimedaydate); $numdates++; } return $dates; } /** * Return list of components to show in selector. * * @return array list of origins. */ public function get_origin_options() { $ret = array(); $ret[''] = get_string('allsources', 'report_log'); $ret['cli'] = get_string('cli', 'report_log'); $ret['restore'] = get_string('restore', 'report_log'); $ret['web'] = get_string('web', 'report_log'); $ret['ws'] = get_string('ws', 'report_log'); $ret['---'] = get_string('other', 'report_log'); return $ret; } /** * Return list of edulevel. * * @todo MDL-44528 Get list from log_store. * @return array list of edulevels. */ public function get_edulevel_options() { $edulevels = array( -1 => get_string("edulevel"), 1 => get_string('edulevelteacher'), 2 => get_string('edulevelparticipating'), 0 => get_string('edulevelother') ); return $edulevels; } /** * Setup table log. */ public function setup_table() { $readers = $this->get_readers(); $filter = new \stdClass(); if (!empty($this->course)) { $filter->courseid = $this->course->id; } else { $filter->courseid = 0; } $filter->userid = $this->userid; $filter->modid = $this->modid; $filter->groupid = $this->get_selected_group(); $filter->logreader = $readers[$this->selectedlogreader]; $filter->edulevel = $this->edulevel; $filter->action = $this->action; $filter->date = $this->date; $filter->orderby = $this->order; $filter->origin = $this->origin; $filter->sitecoursefilter = $this->sitecoursefilter; // If showing site_errors. if ('site_errors' === $this->modid) { $filter->siteerrors = true; $filter->modid = 0; } $this->tablelog = new report_log_table_log('report_log', $filter); $this->tablelog->define_baseurl($this->url); $this->tablelog->is_downloadable(true); $this->tablelog->show_download_buttons_at(array(TABLE_P_BOTTOM)); } /** * Download logs in specified format. */ public function download() { $filename = 'logs_' . userdate(time(), get_string('backupnameformat', 'langconfig'), 99, false); if ($this->course->id !== SITEID) { $courseshortname = format_string($this->course->shortname, true, array('context' => context_course::instance($this->course->id))); $filename = clean_filename('logs_' . $courseshortname . '_' . userdate(time(), get_string('backupnameformat', 'langconfig'), 99, false)); } $this->tablelog->is_downloading($this->logformat, $filename); $this->tablelog->out($this->perpage, false); } } PK{\ log/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_log. * * @package report_log * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_log\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_log implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\nf log/classes/helper.phpnu[. namespace report_log; /** * Helper class for displaying logs. * * @package report_log * @copyright 2024 Benjamin Walker * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class helper { /** * Returns a string that attempts to infer event context display when context no longer exists. * This can be used to show a minimum amount of info in log tables, but ideally this would * be a last resort as log display should not change. * * @param \core\event\base|\stdClass $event * @return string inferred context display, or an empty string. */ public static function get_context_fallback(\core\event\base|\stdClass $event): string { if (empty($event) || empty($event->contextlevel) || empty($event->component)) { return ''; } $name = \context_helper::get_level_name($event->contextlevel); $instanceid = $event->contextinstanceid ?? ''; // Use the plugin name for modules and blocks. if ($event->contextlevel == CONTEXT_MODULE || $event->contextlevel == CONTEXT_BLOCK) { // Some events list the component as core and store the modulename in other. $component = !empty($event->other['modulename']) ? 'mod_' . $event->other['modulename'] : $event->component; // Use module name to keep names consistent. Making component human readable is a close approximation. $modulename = preg_replace('/^(mod_|block_)/', '', $component); $name = str_replace('_', ' ', $modulename); } // Can't access context get_url methods, so don't bother showing a url. return get_string('missingcontext', 'report_log', [ 'name' => strtolower($name), 'instanceid' => $instanceid, ]); } } PK{\##log/classes/event/report_viewed.phpnu[. /** * The report_log report viewed event. * * @package report_log * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_log\event; defined('MOODLE_INTERNAL') || die(); /** * The report_log report viewed event class. * * @property-read array $other { * Extra information about the event. * * - int groupid: Group to display. * - int date: Date to display logs from. * - int modid: Module id for which logs were displayed. * - string modaction: Module action. * - string logformat: Log format in which logs were displayed. * } * * @package report_log * @since Moodle 2.7 * @copyright 2013 Ankit Agarwal * @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() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventreportviewed', 'report_log'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the log report for the course with id '$this->courseid'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/log/index.php', array('id' => $this->courseid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['groupid'])) { throw new \coding_exception('The \'groupid\' value must be set in other.'); } if (!isset($this->other['date'])) { throw new \coding_exception('The \'date\' value must be set in other.'); } if (!isset($this->other['modid'])) { throw new \coding_exception('The \'modid\' value must be set in other.'); } if (!isset($this->other['modaction'])) { throw new \coding_exception('The \'modaction\' value must be set in other.'); } if (!isset($this->other['logformat'])) { throw new \coding_exception('The \'logformat\' value must be set in other.'); } if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } } public static function get_other_mapping() { $othermapped = array(); $othermapped['modid'] = array('db' => 'course_modules', 'restore' => 'course_module'); $othermapped['groupid'] = array('db' => 'groups', 'restore' => 'group'); return $othermapped; } } PK{\wk  (log/classes/event/user_report_viewed.phpnu[. /** * The report_log user report viewed event. * * @package report_log * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_log\event; defined('MOODLE_INTERNAL') || die(); /** * The report_log user report viewed event class. * * @property-read array $other { * Extra information about the event. * * - string mode: display mode. * } * * @package report_log * @since Moodle 2.7 * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_report_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventuserreportviewed', 'report_log'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the user log report for the user with id '$this->relateduserid'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/log/user.php', array('course' => $this->courseid, 'id' => $this->relateduserid, 'mode' => $this->other['mode'])); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (empty($this->other['mode'])) { throw new \coding_exception('The \'mode\' value must be set in other.'); } if (empty($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } } public static function get_other_mapping() { // Nothing to map. return false; } } PK{\/RLLlog/classes/table_log.phpnu[. /** * Table log for displaying logs. * * @package report_log * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\dataformat; use core\report_helper; defined('MOODLE_INTERNAL') || die; global $CFG; require_once($CFG->libdir . '/tablelib.php'); /** * Table log class for displaying logs. * * @package report_log * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_log_table_log extends table_sql { /** @var array list of user fullnames shown in report */ private $userfullnames = array(); /** @var array list of context name shown in report */ private $contextname = array(); /** @var stdClass filters parameters */ private $filterparams; /** @var int[] A list of users to filter by */ private ?array $lateuseridfilter = null; /** * Sets up the table_log parameters. * * @param string $uniqueid unique id of form. * @param stdClass $filterparams (optional) filter params. * - int courseid: id of course * - int userid: user id * - int|string modid: Module id or "site_errors" to view site errors * - int groupid: Group id * - \core\log\sql_reader logreader: reader from which data will be fetched. * - int edulevel: educational level. * - string action: view action * - int date: Date from which logs to be viewed. */ public function __construct($uniqueid, $filterparams = null) { parent::__construct($uniqueid); $this->set_attribute('class', 'reportlog generaltable generalbox table-sm'); $this->filterparams = $filterparams; // Add course column if logs are displayed for site. $cols = array(); $headers = array(); if (empty($filterparams->courseid)) { $cols = array('course'); $headers = array(get_string('course')); } $this->define_columns(array_merge($cols, array('time', 'fullnameuser', 'relatedfullnameuser', 'context', 'component', 'eventname', 'description', 'origin', 'ip'))); $this->define_headers(array_merge($headers, array( get_string('time'), get_string('fullnameuser'), get_string('eventrelatedfullnameuser', 'report_log'), get_string('eventcontext', 'report_log'), get_string('eventcomponent', 'report_log'), get_string('eventname'), get_string('description'), get_string('eventorigin', 'report_log'), get_string('ip_address') ) )); $this->collapsible(false); $this->sortable(false); $this->pageable(true); } /** * Gets the user full name. * * This function is useful because, in the unlikely case that the user is * not already loaded in $this->userfullnames it will fetch it from db. * * @since Moodle 2.9 * @param int $userid * @return string|false */ protected function get_user_fullname($userid) { if (empty($userid)) { return false; } // Check if we already have this users' fullname. $userfullname = $this->userfullnames[$userid] ?? null; if (!empty($userfullname)) { return $userfullname; } // We already looked for the user and it does not exist. if ($userfullname === false) { return false; } // If we reach that point new users logs have been generated since the last users db query. $userfieldsapi = \core_user\fields::for_name(); $fields = $userfieldsapi->get_sql('', false, '', '', false)->selects; if ($user = \core_user::get_user($userid, $fields)) { $this->userfullnames[$userid] = fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())); } else { $this->userfullnames[$userid] = false; } return $this->userfullnames[$userid]; } /** * Generate the time column. * * @param stdClass $event event data. * @return string HTML for the time column */ public function col_time($event) { if (empty($this->download)) { $dateformat = get_string('strftimedatetimeaccurate', 'core_langconfig'); } else { $dateformat = get_string('strftimedatetimeshortaccurate', 'core_langconfig'); } return userdate($event->timecreated, $dateformat); } /** * Generate the username column. * * @param \core\event\base $event event data. * @return string HTML for the username column */ public function col_fullnameuser($event) { // Get extra event data for origin and realuserid. $logextra = $event->get_logextra(); $eventusername = $event->userid ? $this->get_user_fullname($event->userid) : false; // Add username who did the action. if (!empty($logextra['realuserid'])) { $a = new stdClass(); if (!$a->realusername = $this->get_user_fullname($logextra['realuserid'])) { $a->realusername = '-'; } if (!$a->asusername = $eventusername) { $a->asusername = '-'; } if (empty($this->download)) { $params = array('id' => $logextra['realuserid']); if ($event->courseid) { $params['course'] = $event->courseid; } $a->realusername = html_writer::link(new moodle_url('/user/view.php', $params), $a->realusername); $params['id'] = $event->userid; $a->asusername = html_writer::link(new moodle_url('/user/view.php', $params), $a->asusername); } $username = get_string('eventloggedas', 'report_log', $a); } else if ($eventusername) { if (empty($this->download)) { $params = ['id' => $event->userid]; if ($event->courseid) { $params['course'] = $event->courseid; } $username = html_writer::link(new moodle_url('/user/view.php', $params), $eventusername); } else { $username = $eventusername; } } else { $username = '-'; } return $username; } /** * Generate the related username column. * * @param stdClass $event event data. * @return string HTML for the related username column */ public function col_relatedfullnameuser($event) { // Add affected user. if (!empty($event->relateduserid) && $username = $this->get_user_fullname($event->relateduserid)) { if (empty($this->download)) { $params = array('id' => $event->relateduserid); if ($event->courseid) { $params['course'] = $event->courseid; } $username = html_writer::link(new moodle_url('/user/view.php', $params), $username); } } else { $username = '-'; } return $username; } /** * Generate the context column. * * @param stdClass $event event data. * @return string HTML for the context column */ public function col_context($event) { // Add context name. if ($event->contextid) { // If context name was fetched before then return, else get one. if (isset($this->contextname[$event->contextid])) { return $this->contextname[$event->contextid]; } else { $context = context::instance_by_id($event->contextid, IGNORE_MISSING); if ($context) { $contextname = $context->get_context_name(true); if (empty($this->download) && $url = $context->get_url()) { $contextname = html_writer::link($url, $contextname); } } else if (!$contextname = \report_log\helper::get_context_fallback($event)) { $contextname = get_string('other'); } } } else { $contextname = get_string('other'); } $this->contextname[$event->contextid] = $contextname; return $contextname; } /** * Generate the component column. * * @param stdClass $event event data. * @return string HTML for the component column */ public function col_component($event) { // Component. $componentname = $event->component; if (($event->component === 'core') || ($event->component === 'legacy')) { return get_string('coresystem'); } else if (get_string_manager()->string_exists('pluginname', $event->component)) { return get_string('pluginname', $event->component); } else { return $componentname; } } /** * Generate the event name column. * * @param stdClass $event event data. * @return string HTML for the event name column */ public function col_eventname($event) { // Event name. $eventname = $event->get_name(); // Only encode as an action link if we're not downloading. if (($url = $event->get_url()) && empty($this->download)) { $eventname = $this->action_link($url, $eventname, 'action'); } return $eventname; } /** * Generate the description column. * * @param stdClass $event event data. * @return string HTML for the description column */ public function col_description($event) { if (empty($this->download) || dataformat::get_format_instance($this->download)->supports_html()) { return format_text($event->get_description(), FORMAT_PLAIN); } else { return $event->get_description(); } } /** * Generate the origin column. * * @param stdClass $event event data. * @return string HTML for the origin column */ public function col_origin($event) { // Get extra event data for origin and realuserid. $logextra = $event->get_logextra(); // Add event origin, normally IP/cron. return $logextra['origin']; } /** * Generate the ip column. * * @param stdClass $event event data. * @return string HTML for the ip column */ public function col_ip($event) { // Get extra event data for origin and realuserid. $logextra = $event->get_logextra(); $ip = $logextra['ip']; if (empty($this->download)) { $url = new moodle_url("/iplookup/index.php?popup=1&ip={$ip}&user={$event->userid}"); $ip = $this->action_link($url, $ip, 'ip'); } return $ip; } /** * Method to create a link with popup action. * * @param moodle_url $url The url to open. * @param string $text Anchor text for the link. * @param string $name Name of the popup window. * * @return string html to use. */ protected function action_link(moodle_url $url, $text, $name = 'popup') { global $OUTPUT; $link = new action_link($url, $text, new popup_action('click', $url, $name, array('height' => 440, 'width' => 700))); return $OUTPUT->render($link); } /** * Helper function to get legacy crud action. * * @param string $crud crud action * @return string legacy action. */ public function get_legacy_crud_action($crud) { $legacyactionmap = array('c' => 'add', 'r' => 'view', 'u' => 'update', 'd' => 'delete'); if (array_key_exists($crud, $legacyactionmap)) { return $legacyactionmap[$crud]; } else { // From old legacy log. return '-view'; } } /** * Helper function which is used by build logs to get action sql and param. * * @return array sql and param for action. */ public function get_action_sql() { global $DB; // In new logs we have a field to pick, and in legacy try get this from action. if (!empty($this->filterparams->action)) { list($sql, $params) = $DB->get_in_or_equal(str_split($this->filterparams->action), SQL_PARAMS_NAMED, 'crud'); $sql = "crud " . $sql; } else { // Add condition for all possible values of crud (to use db index). list($sql, $params) = $DB->get_in_or_equal(array('c', 'r', 'u', 'd'), SQL_PARAMS_NAMED, 'crud'); $sql = "crud ".$sql; } return array($sql, $params); } /** * Helper function which is used by build logs to get course module sql and param. * * @return array sql and param for action. */ public function get_cm_sql() { $joins = array(); $params = array(); $joins[] = "contextinstanceid = :contextinstanceid"; $joins[] = "contextlevel = :contextmodule"; $params['contextinstanceid'] = $this->filterparams->modid; $params['contextmodule'] = CONTEXT_MODULE; $sql = implode(' AND ', $joins); return array($sql, $params); } /** * Query the reader. Store results in the object for use by build_table. * * @param int $pagesize size of page for paginated displayed table. * @param bool $useinitialsbar do you want to use the initials bar. */ public function query_db($pagesize, $useinitialsbar = true) { global $DB, $USER; $joins = array(); $params = array(); // If we filter by userid and module id we also need to filter by crud and edulevel to ensure DB index is engaged. $useextendeddbindex = !empty($this->filterparams->userid) && !empty($this->filterparams->modid); if (!empty($this->filterparams->courseid) && $this->filterparams->courseid != SITEID) { $joins[] = "courseid = :courseid"; $params['courseid'] = $this->filterparams->courseid; } else if (!empty($this->filterparams->sitecoursefilter)) { // Add filters for missing/deleted courses in site context. $joins[] = "courseid = :courseid"; $params['courseid'] = $this->filterparams->sitecoursefilter; } if (!empty($this->filterparams->siteerrors)) { $joins[] = "( action='error' OR action='infected' OR action='failed' )"; } if (!empty($this->filterparams->modid)) { list($actionsql, $actionparams) = $this->get_cm_sql(); $joins[] = $actionsql; $params = array_merge($params, $actionparams); } if (!empty($this->filterparams->action) || $useextendeddbindex) { list($actionsql, $actionparams) = $this->get_action_sql(); $joins[] = $actionsql; $params = array_merge($params, $actionparams); } // Getting all members of a group. [ 'joins' => $groupjoins, 'params' => $groupparams, 'useridfilter' => $this->lateuseridfilter, ] = report_helper::get_group_filter($this->filterparams); $joins = array_merge($joins, $groupjoins); $params = array_merge($params, $groupparams); if (!empty($this->filterparams->date)) { $joins[] = "timecreated > :date AND timecreated < :enddate"; $params['date'] = $this->filterparams->date; $params['enddate'] = $this->filterparams->date + DAYSECS; // Show logs only for the selected date. } if (isset($this->filterparams->edulevel) && ($this->filterparams->edulevel >= 0)) { $joins[] = "edulevel = :edulevel"; $params['edulevel'] = $this->filterparams->edulevel; } else if ($useextendeddbindex) { list($edulevelsql, $edulevelparams) = $DB->get_in_or_equal(array(\core\event\base::LEVEL_OTHER, \core\event\base::LEVEL_PARTICIPATING, \core\event\base::LEVEL_TEACHING), SQL_PARAMS_NAMED, 'edulevel'); $joins[] = "edulevel ".$edulevelsql; $params = array_merge($params, $edulevelparams); } // Origin. if (isset($this->filterparams->origin) && ($this->filterparams->origin != '')) { if ($this->filterparams->origin !== '---') { // Filter by a single origin. $joins[] = "origin = :origin"; $params['origin'] = $this->filterparams->origin; } else { // Filter by everything else. list($originsql, $originparams) = $DB->get_in_or_equal(array('cli', 'restore', 'ws', 'web'), SQL_PARAMS_NAMED, 'origin', false); $joins[] = "origin " . $originsql; $params = array_merge($params, $originparams); } } // Filter out anonymous actions, this is N/A for legacy log because it never stores them. if ($this->filterparams->modid) { $context = context_module::instance($this->filterparams->modid); } else { $context = context_course::instance($this->filterparams->courseid); } if (!has_capability('moodle/site:viewanonymousevents', $context)) { $joins[] = "anonymous = 0"; } $selector = implode(' AND ', $joins); if (!$this->is_downloading()) { $total = $this->filterparams->logreader->get_events_select_count($selector, $params); $this->pagesize($pagesize, $total); } else { $this->pageable(false); } // Get the events. Same query than before; even if it is not likely, logs from new users // may be added since last query so we will need to work around later to prevent problems. // In almost most of the cases this will be better than having two opened recordsets. $this->rawdata = new \CallbackFilterIterator( $this->filterparams->logreader->get_events_select_iterator( $selector, $params, $this->filterparams->orderby, $this->get_page_start(), $this->get_page_size(), ), function ($event) { if ($this->lateuseridfilter === null) { return true; } return isset($this->lateuseridfilter[$event->userid]); }, ); // Set initial bars. if ($useinitialsbar && !$this->is_downloading()) { $this->initialbars($total > $pagesize); } } } PK{\Gm'n^n^log/locallib.phpnu[. /** * This file contains functions used by the log reports * * This files lists the functions that are used during the log report generation. * * @package report_log * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if (!defined('REPORT_LOG_MAX_DISPLAY')) { define('REPORT_LOG_MAX_DISPLAY', 150); // days } require_once(__DIR__.'/lib.php'); /** * This function is used to generate and display the log activity graph * * @global stdClass $CFG * @param stdClass $course course instance * @param int|stdClass $user id/object of the user whose logs are needed * @param string $typeormode type of logs graph needed (usercourse.png/userday.png) or the mode (today, all). * @param int $date timestamp in GMT (seconds since epoch) * @param string $logreader Log reader. * @param int $sitecoursefilter use a course filter in site context. * @return void */ function report_log_print_graph($course, $user, $typeormode, $date=0, $logreader='', $sitecoursefilter = 0) { global $CFG, $OUTPUT; if (!is_object($user)) { $user = core_user::get_user($user); } $logmanager = get_log_manager(); $readers = $logmanager->get_readers(); if (empty($logreader)) { $reader = reset($readers); } else { $reader = $readers[$logreader]; } // If reader is not a sql_internal_table_reader and not legacy store then don't show graph. if (!($reader instanceof \core\log\sql_internal_table_reader)) { return array(); } $coursecontext = context_course::instance($course->id); $a = new stdClass(); $a->coursename = format_string($course->shortname, true, array('context' => $coursecontext)); $a->username = fullname($user, true); if ($typeormode == 'today' || $typeormode == 'userday.png') { $logs = report_log_usertoday_data($course, $user, $date, $logreader, $sitecoursefilter); $title = get_string("hitsoncoursetoday", "", $a); } else if ($typeormode == 'all' || $typeormode == 'usercourse.png') { $logs = report_log_userall_data($course, $user, $logreader, $sitecoursefilter); $title = get_string("hitsoncourse", "", $a); } if (!empty($CFG->preferlinegraphs)) { $chart = new \core\chart_line(); } else { $chart = new \core\chart_bar(); } $series = new \core\chart_series(get_string("hits"), $logs['series']); $chart->add_series($series); $chart->set_title($title); $chart->set_labels($logs['labels']); $yaxis = $chart->get_yaxis(0, true); $yaxis->set_label(get_string("hits")); $yaxis->set_stepsize(max(1, round(max($logs['series']) / 10))); echo $OUTPUT->render($chart); } /** * Select all log records for a given course and user * * @param int $userid The id of the user as found in the 'user' table. * @param int $courseid The id of the course as found in the 'course' table. * @param string $coursestart unix timestamp representing course start date and time. * @param string $logreader log reader to use. * @return array */ function report_log_usercourse($userid, $courseid, $coursestart, $logreader = '') { global $DB; $logmanager = get_log_manager(); $readers = $logmanager->get_readers(); if (empty($logreader)) { $reader = reset($readers); } else { $reader = $readers[$logreader]; } // If reader is not a sql_internal_table_reader and not legacy store then return. if (!($reader instanceof \core\log\sql_internal_table_reader)) { return array(); } $coursestart = (int)$coursestart; // Note: unfortunately pg complains if you use name parameter or column alias in GROUP BY. $logtable = $reader->get_internal_log_table_name(); $timefield = 'timecreated'; $coursefield = 'courseid'; $nonanonymous = 'AND anonymous = 0'; $params = array(); $courseselect = ''; if ($courseid) { $courseselect = "AND $coursefield = :courseid"; $params['courseid'] = $courseid; } $params['userid'] = $userid; return $DB->get_records_sql("SELECT FLOOR(($timefield - $coursestart)/" . DAYSECS . ") AS day, COUNT(*) AS num FROM {" . $logtable . "} WHERE userid = :userid AND $timefield > $coursestart $courseselect $nonanonymous GROUP BY FLOOR(($timefield - $coursestart)/" . DAYSECS .")", $params); } /** * Select all log records for a given course, user, and day * * @param int $userid The id of the user as found in the 'user' table. * @param int $courseid The id of the course as found in the 'course' table. * @param string $daystart unix timestamp of the start of the day for which the logs needs to be retrived * @param string $logreader log reader to use. * @return array */ function report_log_userday($userid, $courseid, $daystart, $logreader = '') { global $DB; $logmanager = get_log_manager(); $readers = $logmanager->get_readers(); if (empty($logreader)) { $reader = reset($readers); } else { $reader = $readers[$logreader]; } // If reader is not a sql_internal_table_reader and not legacy store then return. if (!($reader instanceof \core\log\sql_internal_table_reader)) { return array(); } $daystart = (int)$daystart; // Note: unfortunately pg complains if you use name parameter or column alias in GROUP BY. $logtable = $reader->get_internal_log_table_name(); $timefield = 'timecreated'; $coursefield = 'courseid'; $nonanonymous = 'AND anonymous = 0'; $params = array('userid' => $userid); $courseselect = ''; if ($courseid) { $courseselect = "AND $coursefield = :courseid"; $params['courseid'] = $courseid; } return $DB->get_records_sql("SELECT FLOOR(($timefield - $daystart)/" . HOURSECS . ") AS hour, COUNT(*) AS num FROM {" . $logtable . "} WHERE userid = :userid AND $timefield > $daystart $courseselect $nonanonymous GROUP BY FLOOR(($timefield - $daystart)/" . HOURSECS . ") ", $params); } /** * This function is used to generate and display Mnet selector form * * @global stdClass $USER * @global stdClass $CFG * @global stdClass $SITE * @global moodle_database $DB * @global core_renderer $OUTPUT * @global stdClass $SESSION * @uses CONTEXT_SYSTEM * @uses COURSE_MAX_COURSES_PER_DROPDOWN * @uses CONTEXT_COURSE * @uses SEPARATEGROUPS * @param int $hostid host id * @param stdClass $course course instance * @param int $selecteduser id of the selected user * @param string $selecteddate Date selected * @param string $modname course_module->id * @param string $modid number or 'site_errors' * @param string $modaction an action as recorded in the logs * @param int $selectedgroup Group to display * @param int $showcourses whether to show courses if we're over our limit. * @param int $showusers whether to show users if we're over our limit. * @param string $logformat Format of the logs (downloadascsv, showashtml, downloadasods, downloadasexcel) * @return void */ function report_log_print_mnet_selector_form($hostid, $course, $selecteduser=0, $selecteddate='today', $modname="", $modid=0, $modaction='', $selectedgroup=-1, $showcourses=0, $showusers=0, $logformat='showashtml') { global $USER, $CFG, $SITE, $DB, $OUTPUT, $SESSION; require_once $CFG->dirroot.'/mnet/peer.php'; $mnet_peer = new mnet_peer(); $mnet_peer->set_id($hostid); $sql = "SELECT DISTINCT course, hostid, coursename FROM {mnet_log}"; $courses = $DB->get_records_sql($sql); $remotecoursecount = count($courses); // first check to see if we can override showcourses and showusers $numcourses = $remotecoursecount + $DB->count_records('course'); if ($numcourses < COURSE_MAX_COURSES_PER_DROPDOWN && !$showcourses) { $showcourses = 1; } $sitecontext = context_system::instance(); // Context for remote data is always SITE // Groups for remote data are always OFF if ($hostid == $CFG->mnet_localhost_id) { $context = context_course::instance($course->id); /// Setup for group handling. if ($course->groupmode == SEPARATEGROUPS and !has_capability('moodle/site:accessallgroups', $context)) { $selectedgroup = -1; $showgroups = false; } else if ($course->groupmode) { $showgroups = true; } else { $selectedgroup = 0; $showgroups = false; } if ($selectedgroup === -1) { if (isset($SESSION->currentgroup[$course->id])) { $selectedgroup = $SESSION->currentgroup[$course->id]; } else { $selectedgroup = groups_get_all_groups($course->id, $USER->id); if (is_array($selectedgroup)) { $selectedgroup = array_shift(array_keys($selectedgroup)); $SESSION->currentgroup[$course->id] = $selectedgroup; } else { $selectedgroup = 0; } } } } else { $context = $sitecontext; } // Get all the possible users $users = array(); // Define limitfrom and limitnum for queries below // If $showusers is enabled... don't apply limitfrom and limitnum $limitfrom = empty($showusers) ? 0 : ''; $limitnum = empty($showusers) ? COURSE_MAX_USERS_PER_DROPDOWN + 1 : ''; // If looking at a different host, we're interested in all our site users if ($hostid == $CFG->mnet_localhost_id && $course->id != SITEID) { $userfieldsapi = \core_user\fields::for_name(); $courseusers = get_enrolled_users($context, '', $selectedgroup, 'u.id, ' . $userfieldsapi->get_sql('u', false, '', '', false)->selects, null, $limitfrom, $limitnum); } else { // this may be a lot of users :-( $userfieldsapi = \core_user\fields::for_name(); $courseusers = $DB->get_records('user', array('deleted' => 0), 'lastaccess DESC', 'id, ' . $userfieldsapi->get_sql('', false, '', '', false)->selects, $limitfrom, $limitnum); } if (count($courseusers) < COURSE_MAX_USERS_PER_DROPDOWN && !$showusers) { $showusers = 1; } if ($showusers) { if ($courseusers) { foreach ($courseusers as $courseuser) { $users[$courseuser->id] = fullname($courseuser, has_capability('moodle/site:viewfullnames', $context)); } } $users[$CFG->siteguest] = get_string('guestuser'); } // Get all the hosts that have log records $sql = "select distinct h.id, h.name from {mnet_host} h, {mnet_log} l where h.id = l.hostid order by h.name"; if ($hosts = $DB->get_records_sql($sql)) { foreach($hosts as $host) { $hostarray[$host->id] = $host->name; } } $hostarray[$CFG->mnet_localhost_id] = $SITE->fullname; asort($hostarray); $dropdown = array(); foreach($hostarray as $hostid => $name) { $courses = array(); $sites = array(); if ($CFG->mnet_localhost_id == $hostid) { if (has_capability('report/log:view', $sitecontext) && $showcourses) { if ($ccc = $DB->get_records("course", null, "fullname","id,shortname,fullname,category")) { foreach ($ccc as $cc) { if ($cc->id == SITEID) { $sites["$hostid/$cc->id"] = format_string($cc->fullname).' ('.get_string('site').')'; } else { $courses["$hostid/$cc->id"] = format_string(get_course_display_name_for_list($cc)); } } } } } else { if (has_capability('report/log:view', $sitecontext) && $showcourses) { $sql = "SELECT DISTINCT course, coursename FROM {mnet_log} where hostid = ?"; if ($ccc = $DB->get_records_sql($sql, array($hostid))) { foreach ($ccc as $cc) { if (1 == $cc->course) { // TODO: this might be wrong - site course may have another id $sites["$hostid/$cc->course"] = $cc->coursename.' ('.get_string('site').')'; } else { $courses["$hostid/$cc->course"] = $cc->coursename; } } } } } asort($courses); $dropdown[] = array($name=>($sites + $courses)); } $activities = array(); $selectedactivity = ""; $modinfo = get_fast_modinfo($course); if (!empty($modinfo->cms)) { $section = 0; $thissection = array(); foreach ($modinfo->cms as $cm) { // Exclude activities that aren't visible or have no view link (e.g. label). Account for folder being displayed inline. if (!$cm->uservisible || (!$cm->has_view() && strcmp($cm->modname, 'folder') !== 0)) { continue; } if ($cm->sectionnum > 0 and $section <> $cm->sectionnum) { $activities[] = $thissection; $thissection = array(); } $section = $cm->sectionnum; $modname = strip_tags($cm->get_formatted_name()); if (core_text::strlen($modname) > 55) { $modname = core_text::substr($modname, 0, 50)."..."; } if (!$cm->visible) { $modname = "(".$modname.")"; } $key = get_section_name($course, $cm->sectionnum); if (!isset($thissection[$key])) { $thissection[$key] = array(); } $thissection[$key][$cm->id] = $modname; if ($cm->id == $modid) { $selectedactivity = "$cm->id"; } } if (!empty($thissection)) { $activities[] = $thissection; } } if (has_capability('report/log:view', $sitecontext) && !$course->category) { $activities["site_errors"] = get_string("siteerrors"); if ($modid === "site_errors") { $selectedactivity = "site_errors"; } } $strftimedate = get_string("strftimedate"); $strftimedaydate = get_string("strftimedaydate"); asort($users); // Prepare the list of action options. $actions = array( 'view' => get_string('view'), 'add' => get_string('add'), 'update' => get_string('update'), 'delete' => get_string('delete'), '-view' => get_string('allchanges') ); // Get all the possible dates // Note that we are keeping track of real (GMT) time and user time // User time is only used in displays - all calcs and passing is GMT $timenow = time(); // GMT // What day is it now for the user, and when is midnight that day (in GMT). $timemidnight = $today = usergetmidnight($timenow); // Put today up the top of the list $dates = array( "0" => get_string('alldays'), "$timemidnight" => get_string("today").", ".userdate($timenow, $strftimedate) ); if (!$course->startdate or ($course->startdate > $timenow)) { $course->startdate = $course->timecreated; } $numdates = 1; while ($timemidnight > $course->startdate and $numdates < 365) { $timemidnight = $timemidnight - 86400; $timenow = $timenow - 86400; $dates["$timemidnight"] = userdate($timenow, $strftimedaydate); $numdates++; } if ($selecteddate === "today") { $selecteddate = $today; } echo "
wwwroot/report/log/index.php\" method=\"get\">\n"; echo "
\n";//invisible fieldset here breaks wrapping echo "\n"; echo "\n"; echo "\n"; if (has_capability('report/log:view', $sitecontext) && $showcourses) { $cid = empty($course->id)? '1' : $course->id; echo html_writer::label(get_string('selectacoursesite'), 'menuhost_course', false, array('class' => 'accesshide')); echo html_writer::select($dropdown, "host_course", $hostid.'/'.$cid); } else { $courses = array(); $courses[$course->id] = get_course_display_name_for_list($course) . ((empty($course->category)) ? ' ('.get_string('site').') ' : ''); echo html_writer::label(get_string('selectacourse'), 'menuid', false, array('class' => 'accesshide')); echo html_writer::select($courses,"id",$course->id, false); if (has_capability('report/log:view', $sitecontext)) { $a = new stdClass(); $a->url = "$CFG->wwwroot/report/log/index.php?chooselog=0&group=$selectedgroup&user=$selecteduser" ."&id=$course->id&date=$selecteddate&modid=$selectedactivity&showcourses=1&showusers=$showusers"; print_string('logtoomanycourses','moodle',$a); } } if ($showgroups) { if ($cgroups = groups_get_all_groups($course->id)) { foreach ($cgroups as $cgroup) { $groups[$cgroup->id] = $cgroup->name; } } else { $groups = array(); } echo html_writer::label(get_string('selectagroup'), 'menugroup', false, array('class' => 'accesshide')); echo html_writer::select($groups, "group", $selectedgroup, get_string("allgroups")); } if ($showusers) { echo html_writer::label(get_string('participantslist'), 'menuuser', false, array('class' => 'accesshide')); echo html_writer::select($users, "user", $selecteduser, get_string("allparticipants")); } else { $users = array(); if (!empty($selecteduser)) { $user = $DB->get_record('user', array('id'=>$selecteduser)); $users[$selecteduser] = fullname($user); } else { $users[0] = get_string('allparticipants'); } echo html_writer::label(get_string('participantslist'), 'menuuser', false, array('class' => 'accesshide')); echo html_writer::select($users, "user", $selecteduser, false); $a = new stdClass(); $a->url = "$CFG->wwwroot/report/log/index.php?chooselog=0&group=$selectedgroup&user=$selecteduser" ."&id=$course->id&date=$selecteddate&modid=$selectedactivity&showusers=1&showcourses=$showcourses"; print_string('logtoomanyusers','moodle',$a); } echo html_writer::label(get_string('date'), 'menudate', false, array('class' => 'accesshide')); echo html_writer::select($dates, "date", $selecteddate, false); echo html_writer::label(get_string('showreports'), 'menumodid', false, array('class' => 'accesshide')); echo html_writer::select($activities, "modid", $selectedactivity, get_string("allactivities")); echo html_writer::label(get_string('actions'), 'menumodaction', false, array('class' => 'accesshide')); echo html_writer::select($actions, 'modaction', $modaction, get_string("allactions")); $logformats = array('showashtml' => get_string('displayonpage'), 'downloadascsv' => get_string('downloadtext'), 'downloadasods' => get_string('downloadods'), 'downloadasexcel' => get_string('downloadexcel')); echo html_writer::label(get_string('logsformat', 'report_log'), 'menulogformat', false, array('class' => 'accesshide')); echo html_writer::select($logformats, 'logformat', $logformat, false); echo ''; echo '
'; echo '
'; } /** * Fetch logs since the start of the courses and structure in series and labels to be sent to Chart API. * * @param stdClass $course the course object * @param stdClass $user user object * @param string $logreader the log reader where the logs are. * @param int $sitecoursefilter use a course filter in site context. * @return array structured array to be sent to chart API, split in two indexes (series and labels). */ function report_log_userall_data($course, $user, $logreader, $sitecoursefilter = 0) { global $CFG; $site = get_site(); $timenow = time(); $logs = []; if ($course->id == $site->id) { $courseselect = $sitecoursefilter; } else { $courseselect = $course->id; } $maxseconds = REPORT_LOG_MAX_DISPLAY * 3600 * 24; // Seconds. if ($timenow - $course->startdate > $maxseconds) { $course->startdate = $timenow - $maxseconds; } if (!empty($CFG->loglifetime)) { $maxseconds = $CFG->loglifetime * 3600 * 24; // Seconds. if ($timenow - $course->startdate > $maxseconds) { $course->startdate = $timenow - $maxseconds; } } $timestart = $coursestart = usergetmidnight($course->startdate); $i = 0; $logs['series'][$i] = 0; $logs['labels'][$i] = 0; while ($timestart < $timenow) { $timefinish = $timestart + 86400; $logs['labels'][$i] = userdate($timestart, "%a %d %b"); $logs['series'][$i] = 0; $i++; $timestart = $timefinish; } $rawlogs = report_log_usercourse($user->id, $courseselect, $coursestart, $logreader); foreach ($rawlogs as $rawlog) { if (isset($logs['labels'][$rawlog->day])) { $logs['series'][$rawlog->day] = $rawlog->num; } } return $logs; } /** * Fetch logs of the current day and structure in series and labels to be sent to Chart API. * * @param stdClass $course the course object * @param stdClass $user user object * @param int $date A time of a day (in GMT). * @param string $logreader the log reader where the logs are. * @param int $sitecoursefilter use a course filter in site context. * @return array $logs structured array to be sent to chart API, split in two indexes (series and labels). */ function report_log_usertoday_data($course, $user, $date, $logreader, $sitecoursefilter = 0) { $site = get_site(); $logs = []; if ($course->id == $site->id) { $courseselect = $sitecoursefilter; } else { $courseselect = $course->id; } if ($date) { $daystart = usergetmidnight($date); } else { $daystart = usergetmidnight(time()); } for ($i = 0; $i <= 23; $i++) { $hour = $daystart + $i * 3600; $logs['series'][$i] = 0; $logs['labels'][$i] = userdate($hour, "%H:00"); } $rawlogs = report_log_userday($user->id, $courseselect, $daystart, $logreader); foreach ($rawlogs as $rawlog) { if (isset($logs['labels'][$rawlog->hour])) { $logs['series'][$rawlog->hour] = $rawlog->num; } } return $logs; } PK{\_ log/index.phpnu[. /** * Displays different views of the logs. * * @package report_log * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\report_helper; require('../../config.php'); require_once($CFG->dirroot.'/course/lib.php'); require_once($CFG->dirroot.'/report/log/locallib.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/lib/tablelib.php'); $id = optional_param('id', 0, PARAM_INT);// Course ID. $group = optional_param('group', 0, PARAM_INT); // Group to display. $user = optional_param('user', 0, PARAM_INT); // User to display. $date = optional_param('date', 0, PARAM_INT); // Date to display. $modid = optional_param('modid', 0, PARAM_ALPHANUMEXT); // Module id or 'site_errors'. $isactivitypage = optional_param('isactivitypage', false, PARAM_BOOL); // Is this a course module page? $modaction = optional_param('modaction', '', PARAM_ALPHAEXT); // An action as recorded in the logs. $page = optional_param('page', '0', PARAM_INT); // Which page to show. $perpage = optional_param('perpage', '100', PARAM_INT); // How many per page. $showcourses = optional_param('showcourses', false, PARAM_BOOL); // Whether to show courses if we're over our limit. $showusers = optional_param('showusers', false, PARAM_BOOL); // Whether to show users if we're over our limit. $chooselog = optional_param('chooselog', false, PARAM_BOOL); $logformat = optional_param('download', '', PARAM_ALPHA); $logreader = optional_param('logreader', '', PARAM_COMPONENT); // Reader which will be used for displaying logs. $edulevel = optional_param('edulevel', -1, PARAM_INT); // Educational level. $origin = optional_param('origin', '', PARAM_TEXT); // Event origin. $params = array(); if (!empty($id)) { $params['id'] = $id; } else { $id = $SITE->id; } if ($group !== 0) { $params['group'] = $group; } if ($user !== 0) { $params['user'] = $user; } if ($date !== 0) { $params['date'] = $date; } if ($modid !== 0) { $params['modid'] = $modid; } if ($modaction !== '') { $params['modaction'] = $modaction; } if ($page !== '0') { $params['page'] = $page; } if ($perpage !== '100') { $params['perpage'] = $perpage; } if ($showcourses) { $params['showcourses'] = $showcourses; } if ($showusers) { $params['showusers'] = $showusers; } if ($chooselog) { $params['chooselog'] = $chooselog; } if ($logformat !== '') { $params['download'] = $logformat; } if ($logreader !== '') { $params['logreader'] = $logreader; } if (($edulevel != -1)) { $params['edulevel'] = $edulevel; } if ($origin !== '') { $params['origin'] = $origin; } $url = new moodle_url("/report/log/index.php", $params); $PAGE->set_url('/report/log/index.php', array('id' => $id)); $PAGE->set_pagelayout('report'); $cminfo = null; if (!is_number($modid)) { $isactivitypage = false; } if ($isactivitypage) { $modinfo = get_fast_modinfo($id); $cminfo = $modinfo->cms[intval($modid)] ?? null; if ($cminfo === null) { throw new moodle_exception('invalidmoduleid', '', '', $modid); } } // Get course details. $sitecoursefilter = 0; if ($id != $SITE->id) { $course = $DB->get_record('course', ['id' => $id], '*'); if ($course) { require_login($course); $context = context_course::instance($course->id); if ($cminfo !== null) { $context = $cminfo->context; $PAGE->set_cm($cminfo); } } else { // Missing courses may have be deleted, so display them in site context. $sitecoursefilter = $id; } } if (empty($course)) { $course = $SITE; require_login(); $context = context_system::instance(); $PAGE->set_context($context); } require_capability('report/log:view', $context); // When user choose to view logs then only trigger event. if ($chooselog) { // Trigger a report viewed event. $event = \report_log\event\report_viewed::create(array('context' => $context, 'relateduserid' => $user, 'other' => array('groupid' => $group, 'date' => $date, 'modid' => $modid, 'modaction' => $modaction, 'logformat' => $logformat))); $event->trigger(); } if (!empty($page)) { $strlogs = get_string('logs'). ": ". get_string('page', 'report_log', $page + 1); } else { $strlogs = get_string('logs'); } $stradministration = get_string('administration'); $strreports = get_string('reports'); // Before we close session, make sure we have editing information in session. $adminediting = optional_param('adminedit', -1, PARAM_BOOL); if ($PAGE->user_allowed_editing() && $adminediting != -1) { $USER->editing = $adminediting; } if ($course->id == $SITE->id) { admin_externalpage_setup('reportlog', '', null, '', array('pagelayout' => 'report')); $PAGE->set_title($strlogs); $PAGE->set_primary_active_tab('siteadminnode'); } else { $contexttitle = $course->shortname . ': '; if ($cminfo !== null) { $contexttitle .= $cminfo->name . ' - '; } $PAGE->set_title($contexttitle . $strlogs); $PAGE->set_heading($course->fullname); } $output = $PAGE->get_renderer('report_log'); if (!report_helper::has_valid_group($context)) { echo $output->header(); echo $output->notification(get_string('notingroup')); echo $output->footer(); exit(); } $reportlog = new report_log_renderable( logreader: $logreader, course: $course, userid: $user, modid: $modid, action: $modaction, groupid: $group, edulevel: $edulevel, showcourses: $showcourses, showusers: $showusers, showreport: $chooselog, showselectorform: true, url: $url, date: $date, logformat: $logformat, page: $page, perpage: $perpage, order: 'timecreated DESC', origin: $origin, isactivitypage: $isactivitypage, sitecoursefilter: $sitecoursefilter, ); $readers = $reportlog->get_readers(); if (empty($readers)) { echo $output->header(); echo $output->heading(get_string('nologreaderenabled', 'report_log')); } else { if (!empty($chooselog)) { // Delay creation of table, till called by user with filter. $reportlog->setup_table(); if (empty($logformat)) { echo $output->header(); // Print selector dropdown. $pluginname = get_string('pluginname', 'report_log'); if (!$isactivitypage) { report_helper::print_report_selector($pluginname); } $userinfo = get_string('allparticipants'); $dateinfo = get_string('alldays'); if ($user) { $u = $DB->get_record('user', array('id' => $user, 'deleted' => 0), '*', MUST_EXIST); $userinfo = fullname($u, has_capability('moodle/site:viewfullnames', $context)); } if ($date) { $dateinfo = userdate($date, get_string('strftimedaydate')); } if (!empty($course) && ($course->id != SITEID)) { $PAGE->navbar->add("$userinfo, $dateinfo"); } echo $output->render($reportlog); } else { \core\session\manager::write_close(); $reportlog->download(); exit(); } } else { echo $output->header(); // Print selector dropdown. $pluginname = get_string('pluginname', 'report_log'); if (!$isactivitypage) { report_helper::print_report_selector($pluginname); } echo $output->heading(get_string('chooselogs') .':', 3); echo $output->render($reportlog); } } echo $output->footer(); PK{\X2alog/UPGRADING.mdnu[# report_log Upgrade notes ## 5.0 ### Removed - Support for the $grouplist public member in the report_log_renderable class has been removed. For more information see [MDL-81155](https://tracker.moodle.org/browse/MDL-81155) ## 4.5 ### Added - The `\report_log_renderable::get_activities_list()` method return values now includes an array of disabled elements, in addition to the array of activities. For more information see [MDL-82146](https://tracker.moodle.org/browse/MDL-82146) PK{\ߋ log/graph.phpnu[. /** * Produces a graph of log accesses for a user * * Generates an image representing the log data in a graphical manner for a user. * * @package report_log * @deprecated since 3.2 * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require("../../config.php"); require_once($CFG->libdir . '/filelib.php'); debugging('This way of generating the chart is deprecated, refer to report_log_print_graph().', DEBUG_DEVELOPER); send_file_not_found(); PK{\,log/tests/lib_test.phpnu[. /** * Tests for report library functions. * * @package report_log * @copyright 2014 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace report_log; defined('MOODLE_INTERNAL') || die(); global $CFG; /** * Class report_log_events_testcase. * * @package report_log * @copyright 2014 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class lib_test extends \advanced_testcase { /** * @var stdClass The user. */ private $user; /** * @var stdClass The course. */ private $course; /** * @var \core_user\output\myprofile\tree The navigation tree. */ private $tree; public function setUp(): void { parent::setUp(); $this->user = $this->getDataGenerator()->create_user(); $this->course = $this->getDataGenerator()->create_course(); $this->tree = new \core_user\output\myprofile\tree(); $this->resetAfterTest(); } /** * Test report_log_supports_logstore. */ public function test_report_log_supports_logstore(): void { $logmanager = get_log_manager(); $allstores = \core_component::get_plugin_list_with_class('logstore', 'log\store'); $supportedstores = array( 'logstore_database' => '\logstore_database\log\store', 'logstore_standard' => '\logstore_standard\log\store' ); // Make sure all supported stores are installed. $expectedstores = array_keys(array_intersect($allstores, $supportedstores)); $stores = $logmanager->get_supported_logstores('report_log'); $stores = array_keys($stores); foreach ($expectedstores as $expectedstore) { $this->assertContains($expectedstore, $stores); } } /** * Tests the report_log_myprofile_navigation() function as an admin viewing the logs for a user. */ public function test_report_log_myprofile_navigation(): void { // Set as the admin. $this->setAdminUser(); $iscurrentuser = false; // Check the node tree is correct. report_log_myprofile_navigation($this->tree, $this->user, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayHasKey('alllogs', $nodes->getValue($this->tree)); $this->assertArrayHasKey('todayslogs', $nodes->getValue($this->tree)); } /** * Tests the report_log_myprofile_navigation() function as a user without permission. */ public function test_report_log_myprofile_navigation_without_permission(): void { // Set to the other user. $this->setUser($this->user); $iscurrentuser = true; // Check the node tree is correct. report_log_myprofile_navigation($this->tree, $this->user, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayNotHasKey('alllogs', $nodes->getValue($this->tree)); $this->assertArrayNotHasKey('todayslogs', $nodes->getValue($this->tree)); } } PK{\ilog/tests/event/events_test.phpnu[. /** * Tests for report log events. * * @package report_log * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace report_log\event; /** * Class report_log_events_testcase * * Class for tests related to log events. * * @package report_log * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class events_test extends \advanced_testcase { /** * Setup testcase. */ public function setUp(): void { parent::setUp(); $this->setAdminUser(); $this->resetAfterTest(); } /** * Test the report viewed event. * * It's not possible to use the moodle API to simulate the viewing of log report, so here we * simply create the event and trigger it. */ public function test_report_viewed(): void { $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Trigger event for log report viewed. $event = \report_log\event\report_viewed::create(array('context' => $context, 'relateduserid' => 0, 'other' => array('groupid' => 0, 'date' => 0, 'modid' => 0, 'modaction' => '', 'logformat' => 'showashtml'))); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $event = reset($events); $this->assertInstanceOf('\report_log\event\report_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $url = new \moodle_url('/report/log/index.php', array('id' => $event->courseid)); $this->assertEquals($url, $event->get_url()); } /** * Test the user report viewed event. * * It's not possible to use the moodle API to simulate the viewing of user log report, so here we * simply create the event and trigger it. */ public function test_user_report_viewed(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Trigger event for user report viewed. $event = \report_log\event\user_report_viewed::create(array('context' => $context, 'relateduserid' => $user->id, 'other' => array('mode' => 'today'))); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $event = reset($events); $this->assertInstanceOf('\report_log\event\user_report_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $url = new \moodle_url('/report/log/user.php', array('course' => $course->id, 'id' => $user->id, 'mode' => 'today')); $this->assertEquals($url, $event->get_url()); } } PK{\yEE(log/tests/behat/group_report_log.featurenu[@report @report_log Feature: In a course with group mode, I can view the group report log page or not depending on the group I am in. Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course separate group | C1 | 0 | 1 | | Course visible group | C2 | 0 | 2 | | Course no group | C3 | 0 | 0 | And 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 "course enrolments" exist: | user | course | role | | teacher1 | C1 | teacher | | teacher2 | C1 | teacher | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C2 | teacher | | teacher2 | C2 | teacher | | student1 | C2 | student | | student2 | C2 | student | | teacher1 | C3 | teacher | | teacher2 | C3 | teacher | | student1 | C3 | student | | student2 | C3 | student | And the following "groups" exist: | course | name | idnumber | | C1 | Group C1.1 | group11 | | C2 | Group C2.1 | group21 | | C3 | Group C3.1 | group31 | And the following "group members" exist: | group | user | | group11 | student1 | | group11 | teacher1 | | group21 | student1 | | group21 | teacher1 | | group31 | student1 | | group31 | teacher1 | And the following "activities" exist: | activity | name | intro | course | idnumber | | page | Page11 | Page11 | C1 | page1 | | page | Page21 | Page21 | C2 | page1 | | page | Page31 | Page31 | C3 | page1 | # Generate logs for the pages. And I am on the "Page11" "page activity" page logged in as student1 And I am on "Course separate group" course homepage And I log out And I am on the "Page11" "page activity" page logged in as student2 And I am on "Course separate group" course homepage And I log out And I am on the "Page21" "page activity" page logged in as student1 And I am on "Course visible group" course homepage And I log out And I am on the "Page21" "page activity" page logged in as student2 And I am on "Course visible group" course homepage And I log out And I am on the "Page31" "page activity" page logged in as student1 And I am on "Course no group" course homepage And I log out And I am on the "Page31" "page activity" page logged in as student2 And I am on "Course no group" course homepage And I log out Scenario Outline: As a user in a course, I can view a link to the report logs if I am in the right group. Given I log in as "" And I am on "" course homepage When I navigate to "Reports" in current page administration And "Logs" "link" And I log out Examples: | course | user | shouldexist | | Course separate group | teacher1 | should exist | | Course separate group | teacher2 | should not exist | | Course visible group | teacher1 | should exist | | Course visible group | teacher2 | should exist | | Course no group | teacher1 | should exist | | Course no group | teacher2 | should exist | Scenario Outline: As a non editing teacher not in a group, I can not view the report logs. Given I log in as "" When I am on the "" "report_log > Logs" page Then I "you need to be part of a group to see this page." Examples: | course | user | shouldsee | | Course separate group | teacher1 | should not see | | Course separate group | teacher2 | should see | | Course visible group | teacher1 | should not see | | Course visible group | teacher2 | should not see | | Course no group | teacher1 | should not see | | Course no group | teacher2 | should not see | PK{\F T)log/tests/behat/course_report_log.featurenu[@report @report_log Feature: In a course administration page, navigate through report page, test for report log page In order to navigate through report page As an admin Go to course administration -> reports Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | admin | C1 | editingteacher | | student1 | C1 | student | @javascript Scenario: Report selector should be available in the report log page Given I log in as "admin" And I am on "Course 1" course homepage When I navigate to "Reports" in current page administration And I click on "Logs" "link" Then "Report" "field" should exist in the "tertiary-navigation" "region" And I should see "Logs" in the "tertiary-navigation" "region" PK{\﫛*log/tests/behat/subsection_reports.featurenu[@report @report_log @mod_subsection Feature: Subsection behavior in Log report. Background: Given the following "courses" exist: | fullname | shortname | category | numsections | initsections | | Course 1 | C1 | 0 | 2 | 1 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | admin | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | section | | subsection | Subsection 1 | C1 | subsection1 | 1 | | page | Page 1 | C1 | page1 | 3 | @javascript Scenario: Visible subsections should be available in the activities selector Given I log in as "admin" And I am on "Course 1" course homepage When I navigate to "Reports" in current page administration And I click on "Logs" "link" Then the "Activities" select box should contain "Subsection 1" And the "Activities" select box should contain "Page 1" @javascript Scenario: Hidden subsections should be available in the activities selector Given I log in as "admin" And I am on "Course 1" course homepage with editing mode on And I hide section "Subsection 1" When I navigate to "Reports" in current page administration And I click on "Logs" "link" Then the "Activities" select box should not contain "Subsection 1" And the "Activities" select box should not contain "Page 1" PK{\(66$log/tests/behat/behat_report_log.phpnu[. require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); /** * Step definition for report_log behat tests. * * @package report_log * @category test * @copyright 2025 Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_report_log extends behat_base { /** * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. * * Recognised page names are: * | pagetype | name meaning | description | * | logs | Course name | The course report logs page | * * @param string $page identifies which type of page this is, e.g. 'Logs'. * @param string $identifier identifies the particular page, e.g. 'C1'. * @return moodle_url the corresponding URL. * @throws Exception with a meaningful error message if the specified page cannot be found. */ protected function resolve_page_instance_url(string $page, string $identifier): moodle_url { switch (strtolower($page)) { case 'logs': $courseid = $this->get_course_id($identifier); return new moodle_url('/report/log/index.php', [ 'id' => $courseid, ]); default: throw new Exception("Unrecognised page type '{$page}'"); } } } PK{\uw"log/tests/behat/filter_log.featurenu[@report @report_log Feature: In a report, admin can filter log data In order to filter log data As an admin I need to log in with different user and go to log and apply filter Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | idnumber | middlename | alternatename | firstnamephonetic | lastnamephonetic | | teacher1 | Teacher | One | teacher1@example.com | t1 | | fred | | | | student1 | Grainne | Beauchamp | student1@example.com | s1 | Ann | Jill | Gronya | Beecham | And the following "course enrolments" exist: | user | course | role | | admin | C1 | editingteacher | | student1 | C1 | student | And the following config values are set as admin: | fullnamedisplay | firstname | | alternativefullnameformat | middlename, alternatename, firstname, lastname | And I log in as "admin" Scenario: Filter log report for standard log reader Given I am on "Course 1" course homepage And I navigate to course participants And I follow "Ann, Jill, Grainne, Beauchamp" And I click on "Log in as" "link" And I press "Continue" And I log out And I log in as "admin" When I navigate to "Reports > Logs" in site administration And I set the field "id" to "Acceptance test site (Site)" And I set the field "user" to "All participants" And I press "Get these logs" Then I should see "User logged in as another user" PK{\*log/tests/behat/filter_log_actions.featurenu[@report @report_log Feature: In a report, admin can filter log data by action In order to filter log data by action As an admin I need to view the logs and apply a filter Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "activity" exists: | activity | assign | | course | C1 | | idnumber | 0001 | | name | Test assignment 1 | | intro | Offline text | | assignsubmission_file_enabled | 0 | | section | 1 | And I log in as "admin" And I am on "Course 1" course homepage with editing mode on # View Action. And I follow "Test assignment 1" # Update Action. And I navigate to "Settings" in current page administration And I press "Save and return to course" # Delete Action. And I delete "Test assignment 1" activity And I log out And I trigger cron Scenario: View only create actions. Given I log in as "admin" When I navigate to "Reports > Logs" in site administration And I set the field "menumodaction" to "Create" And I press "Get these logs" And I should not see "Course module updated" And I should not see "The status of the submission has been viewed." And I should not see "Course module deleted" Scenario: View only update actions. Given I log in as "admin" When I navigate to "Reports > Logs" in site administration And I set the field "menumodaction" to "Update" And I press "Get these logs" Then I should see "Course module updated" And I should not see "Course module created" And I should not see "The status of the submission has been viewed." And I should not see "Course module deleted" Scenario: View only view actions. Given I log in as "admin" When I navigate to "Reports > Logs" in site administration And I set the field "menumodaction" to "View" And I press "Get these logs" Then I should see "The status of the submission has been viewed." And I should not see "Course module created" And I should not see "Course module updated" And I should not see "Course module deleted" Scenario: View only delete actions. Given I log in as "admin" When I navigate to "Reports > Logs" in site administration And I set the field "menumodaction" to "Delete" And I press "Get these logs" Then I should see "Course module deleted" And I should not see "Course module created" And I should not see "Course module updated" And I should not see "The status of the submission has been viewed." Scenario: View only changes. Given I log in as "admin" When I navigate to "Reports > Logs" in site administration And I set the field "menumodaction" to "All changes" And I press "Get these logs" Then I should see "Course module deleted" And I should see "Course module updated" And I should not see "The status of the submission has been viewed." Scenario: View all actions. Given I log in as "admin" When I navigate to "Reports > Logs" in site administration And I set the field "menumodaction" to "All actions" And I press "Get these logs" Then I should see "Course module deleted" And I should see "Course module updated" And I should see "The status of the submission has been viewed." PK{\:66 log/tests/behat/user_log.featurenu[@report @report_log Feature: User can view activity log. In order to view user log As an teacher I need to view user today's and all report Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | idnumber | middlename | alternatename | firstnamephonetic | lastnamephonetic | | teacher1 | Teacher | One | teacher1@example.com | t1 | | fred | | | | student1 | Grainne | Beauchamp | student1@example.com | s1 | Ann | Jill | Gronya | Beecham | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activity" exists: | activity | assign | | course | C1 | | idnumber | 0001 | | name | Test assignment name | | intro | Submit your online text | | section | 1 | | assignsubmission_onlinetext_enabled | 1 | | assignsubmission_file_enabled | 0 | And the following config values are set as admin: | fullnamedisplay | firstname | | alternativefullnameformat | middlename, alternatename, firstname, lastname | And I log in as "student1" And I am on "Course 1" course homepage And I follow "Test assignment name" When I press "Add submission" And I set the following fields to these values: | Online text | I'm the student first submission | And I press "Save changes" And I log out Scenario: View Todays' and all log report for user Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants And I follow "Ann, Jill, Grainne, Beauchamp" When I follow "Today's logs" And I should see "Assignment: Test assignment name" And I follow "Ann, Jill, Grainne, Beauchamp" And I follow "All logs" Then I should see "Assignment: Test assignment name" Scenario: No log reader enabled should be visible when no log store enabled. Given I log in as "admin" And I navigate to "Plugins > Logging > Manage log stores" in site administration And I click on "Disable" "link" in the "Standard log" "table_row" And I log out And I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to course participants And I follow "Ann, Jill, Grainne, Beauchamp" When I follow "Today's logs" And I should see "No log reader enabled" And I am on "Course 1" course homepage And I navigate to course participants And I follow "Ann, Jill, Grainne, Beauchamp" And I follow "All logs" Then I should see "No log reader enabled" Scenario: View Todays' log report for user through Course log report Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to "Reports" in current page administration And I click on "Logs" "link" And I set the field with xpath "//select[@name='user']" to "Ann, Jill, Grainne, Beauchamp" When I click on "Get these logs" "button" Then I should see "Ann, Jill, Grainne, Beauchamp" PK{\C +log/tests/behat/activity_report_log.featurenu[@report @report_log Feature: In a activity page, navigate through the More / Logs menu, test for report log page In order to navigate through report page As an admin Go to the activity page, click on More / Logs menu, and check for the report log page Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "activities" exist: | activity | name | course | section | | page | Test page 1 | C1 | 1 | | page | Test page 2 | C1 | 1 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student2@example.com | And the following "course enrolments" exist: | user | course | role | | admin | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And I log in as "student1" And I am on "Course 1" course homepage And I follow "Test page 1" And I am on "Course 1" course homepage And I follow "Test page 2" And I log out And I log in as "student2" And I am on "Course 1" course homepage And I follow "Test page 1" And I log out Scenario: Report selectors should be targeted toward course module Given I am on the "Test page 1" Activity page logged in as "admin" And I navigate to "Logs" in current page administration And "menuid" "select" should not exist And "modid" "select" should not exist And I should see "All participants" in the "user" "select" And I should see "All days" in the "date" "select" And I should see "All sources" in the "origin" "select" And I should see "All events" in the "edulevel" "select" And I should see "Test page 1" in the "#page-header" "css_element" And I should see "Student 1" in the "user" "select" When I set the field "user" to "Student 1" And I click on "Get these logs" "button" Then I should see "Test page 1" in the "#page-header" "css_element" And I should not see "Student 2" in the "table.reportlog" "css_element" And I should see "Page: Test page 1" in the "table.reportlog" "css_element" Scenario: Report submission stays in the same course module page Given I am on the "Test page 1" Activity page logged in as "admin" When I navigate to "Logs" in current page administration And I click on "Get these logs" "button" Then I should see "Test page 1" in the "#page-header" "css_element" PK{\iBIIlog/tests/renderable_test.phpnu[. namespace report_log; use context_course; use core_user; /** * Class report_log\renderable_test to cover functions in \report_log_renderable. * * @package report_log * @copyright 2023 Stephan Robotta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class renderable_test extends \advanced_testcase { /** * @var int The course with separate groups. */ const COURSE_SEPARATE_GROUP = 0; /** * @var int The course with separate groups. */ const COURSE_VISIBLE_GROUP = 1; /** * @var int The course with separate groups. */ const COURSE_NO_GROUP = 2; /** * @var array The setup of users. */ const SETUP_USER_DEFS = [ // Make student2 also member of group1. 'student' => [ 'student0' => ['group0'], 'student1' => ['group1'], 'student2' => ['group0', 'group1'], 'student3' => [], ], // Make teacher2 also member of group1. 'teacher' => [ 'teacher0' => ['group0'], 'teacher1' => ['group1'], 'teacher2' => ['group0', 'group1'], 'teacher3' => [], ], // Make editingteacher also member of group1. 'editingteacher' => [ 'editingteacher0' => ['group0'], 'editingteacher1' => ['group1'], 'editingteacher2' => ['group0', 'group1'], ], ]; /** * @var array|\stdClass all users indexed by username. */ private $users = []; /** * @var array The groups by courses (array of array). */ private $groupsbycourse = []; /** * @var array The courses. */ private $courses; /** * Get the data provider for test_get_user_list(). * * @return array */ public static function get_user_visibility_list_provider(): array { return [ 'separategroups: student 0' => [ self::COURSE_SEPARATE_GROUP, 'student0', // All users in group 0. [ 'student0', 'student2', 'teacher0', 'teacher2', 'editingteacher0', 'editingteacher2', ], ], 'separategroups: student 1' => [ self::COURSE_SEPARATE_GROUP, 'student1', // All users in group1. [ 'student1', 'student2', 'teacher1', 'teacher2', 'editingteacher1', 'editingteacher2', ], ], 'separategroups: student 2' => [ self::COURSE_SEPARATE_GROUP, 'student2', // All users in group0 and group1. [ 'student0', 'student1', 'student2', 'teacher0', 'teacher1', 'teacher2', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'separategroups: student3' => [ self::COURSE_SEPARATE_GROUP, 'student3', // Student 3 is not in any group so should only see user without a group. [ 'student3', 'teacher3', ], ], 'separategroups: editing teacher 0' => [ self::COURSE_SEPARATE_GROUP, 'editingteacher0', // All users including student 3 as we can see all users (event the one not in a group). [ 'student0', 'student1', 'student2', 'student3', 'teacher0', 'teacher1', 'teacher2', 'teacher3', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'separategroups: teacher 0' => [ self::COURSE_SEPARATE_GROUP, 'teacher0', // All users in group 0. [ 'student0', 'student2', 'teacher0', 'teacher2', 'editingteacher0', 'editingteacher2', ], ], 'separategroups: teacher 2' => [ self::COURSE_SEPARATE_GROUP, 'teacher2', // All users in group0 and group1. [ 'student0', 'student1', 'student2', 'teacher0', 'teacher1', 'teacher2', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'separategroups: teacher 3' => [ self::COURSE_SEPARATE_GROUP, 'teacher3', // Teacher 3 is not in any group so should only see user without a group. [ 'student3', 'teacher3', ], ], 'separategroups: teacher 2 with group set to group 1' => [ self::COURSE_SEPARATE_GROUP, 'teacher2', // All users in group1. [ 'student1', 'student2', 'teacher1', 'teacher2', 'editingteacher1', 'editingteacher2', ], 'group1', ], 'visiblegroup: editing teacher' => [ self::COURSE_VISIBLE_GROUP, 'editingteacher0', // All users. [ 'student0', 'student1', 'student2', 'student3', 'teacher0', 'teacher1', 'teacher2', 'teacher3', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'visiblegroup: student' => [ self::COURSE_VISIBLE_GROUP, 'student0', // All users. [ 'student0', 'student1', 'student2', 'student3', 'teacher0', 'teacher1', 'teacher2', 'teacher3', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'visiblegroup: teacher 0' => [ self::COURSE_VISIBLE_GROUP, 'teacher2', // All users. [ 'student0', 'student1', 'student2', 'student3', 'teacher0', 'teacher1', 'teacher2', 'teacher3', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'visiblegroup: editing teacher 0' => [ self::COURSE_VISIBLE_GROUP, 'editingteacher0', // All users. [ 'student0', 'student1', 'student2', 'student3', 'teacher0', 'teacher1', 'teacher2', 'teacher3', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'visiblegroup: teacher 2 with group set to group 1' => [ self::COURSE_VISIBLE_GROUP, 'teacher2', // All users in group1. [ 'student1', 'student2', 'teacher1', 'teacher2', 'editingteacher1', 'editingteacher2', ], 'group1', ], ]; } /** * Data provider for test_get_group_list(). * * @return array */ public static function get_group_list_provider(): array { return [ // The student sees his own group only. 'separategroup: student in one group' => [self::COURSE_SEPARATE_GROUP, 'student0', 1], 'separategroup: student in two groups' => [self::COURSE_SEPARATE_GROUP, 'student2', 2], // While the teacher is not allowed to see all groups. 'separategroup: teacher in one group' => [self::COURSE_SEPARATE_GROUP, 'teacher0', 1], 'separategroup: teacher in two groups' => [self::COURSE_SEPARATE_GROUP, 'teacher2', 2], // But editing teacher should see all. 'separategroup: editingteacher' => [self::COURSE_SEPARATE_GROUP, 'editingteacher0', 2], // The student sees all groups. 'visiblegroup: student in one group' => [self::COURSE_VISIBLE_GROUP, 'student0', 2], // Same for teacher. 'visiblegroup: teacher in one group' => [self::COURSE_VISIBLE_GROUP, 'teacher0', 2], // And editing teacher. 'visiblegroup: editingteacher' => [self::COURSE_VISIBLE_GROUP, 'editingteacher0', 2], // No group. 'nogroups: student in one group' => [self::COURSE_NO_GROUP, 'student0', 0], // Same for teacher. 'nogroups: teacher in one group' => [self::COURSE_NO_GROUP, 'teacher0', 0], // And editing teacher. 'nogroups: editingteacher' => [self::COURSE_NO_GROUP, 'editingteacher0', 0], ]; } /** * Set up a course with two groups, three students being each in one of the groups, * two teachers each in either group while the second teacher is also member of the other group. * * @return void * @throws \coding_exception */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(); $this->courses[self::COURSE_SEPARATE_GROUP] = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS]); $this->courses[self::COURSE_VISIBLE_GROUP] = $this->getDataGenerator()->create_course(['groupmode' => VISIBLEGROUPS]); $this->courses[self::COURSE_NO_GROUP] = $this->getDataGenerator()->create_course(); foreach ($this->courses as $coursetype => $course) { if ($coursetype == self::COURSE_NO_GROUP) { continue; } $this->groupsbycourse[$coursetype] = []; $this->groupsbycourse[$coursetype]['group0'] = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'group0']); $this->groupsbycourse[$coursetype]['group1'] = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'group1']); } foreach (self::SETUP_USER_DEFS as $role => $userdefs) { foreach ($userdefs as $username => $groups) { $user = $this->getDataGenerator()->create_user( [ 'username' => $username, 'firstname' => "FN{$role}{$username}", 'lastname' => "LN{$role}{$username}", ]); foreach ($this->courses as $coursetype => $course) { $this->getDataGenerator()->enrol_user($user->id, $course->id, $role); foreach ($groups as $groupname) { if ($coursetype == self::COURSE_NO_GROUP) { continue; } $this->getDataGenerator()->create_group_member([ 'groupid' => $this->groupsbycourse[$coursetype][$groupname]->id, 'userid' => $user->id, ]); } } $this->users[$username] = $user; } } } /** * Test report_log_renderable::get_user_list(). * * @param int $courseindex * @param string $username * @param array $expectedusers * @param string|null $groupname * @covers \report_log_renderable::get_user_list * @dataProvider get_user_visibility_list_provider * @return void */ public function test_get_user_list(int $courseindex, string $username, array $expectedusers, ?string $groupname = null): void { global $PAGE, $CFG; $currentcourse = $this->courses[$courseindex]; $PAGE->set_url('/report/log/index.php?id=' . $currentcourse->id); // Fetch all users of group 1 and the guest user. $currentuser = $this->users[$username]; $this->setUser($currentuser->id); $groupid = 0; if ($groupname) { $groupid = $this->groupsbycourse[$courseindex][$groupname]->id; } $renderable = new \report_log_renderable( "", (int) $currentcourse->id, $currentuser->id, 0, '', $groupid); $userlist = $renderable->get_user_list(); unset($userlist[$CFG->siteguest]); // We ignore guest. $usersid = array_keys($userlist); $users = array_map(function($userid) { return core_user::get_user($userid); }, $usersid); // Now check that the users are the expected ones. asort($expectedusers); $userlistbyname = array_column($users, 'username'); asort($userlistbyname); $this->assertEquals(array_values($expectedusers), array_values($userlistbyname)); // Check that users are in order lastname > firstname > id. $sortedusers = $users; // Sort user by lastname > firstname > id. usort($sortedusers, function($a, $b) { if ($a->lastname != $b->lastname) { return $a->lastname <=> $b->lastname; } if ($a->firstname != $b->firstname) { return $a->firstname <=> $b->firstname; } return $a->id <=> $b->id; }); $sortedusernames = array_column($sortedusers, 'username'); $userlistbyname = array_column($users, 'username'); $this->assertEquals($sortedusernames, $userlistbyname); } /** * Test report_log_renderable::get_group_list(). * * @covers \report_log_renderable::get_group_list * @dataProvider get_group_list_provider * @return void */ public function test_get_group_list($courseindex, $username, $expectedcount): void { global $PAGE; $PAGE->set_url('/report/log/index.php?id=' . $this->courses[$courseindex]->id); $this->setUser($this->users[$username]->id); $renderable = new \report_log_renderable("", (int) $this->courses[$courseindex]->id, $this->users[$username]->id); $groups = $renderable->get_group_list(); $this->assertCount($expectedcount, $groups); } /** * Test table_log * * @param int $courseindex * @param string $username * @param array $expectedusers * @param string|null $groupname * @covers \report_log_renderable::get_user_list * @dataProvider get_user_visibility_list_provider * @return void */ public function test_get_table_logs(int $courseindex, string $username, array $expectedusers, ?string $groupname = null): void { global $DB, $PAGE; $this->preventResetByRollback(); // This is to ensure that we can actually trigger event and record them in the log store. // Configure log store. set_config('enabled_stores', 'logstore_standard', 'tool_log'); $manager = get_log_manager(true); $DB->delete_records('logstore_standard_log'); foreach ($this->courses as $course) { foreach ($this->users as $user) { $eventdata = [ 'context' => context_course::instance($course->id), 'userid' => $user->id, ]; $event = \core\event\course_viewed::create($eventdata); $event->trigger(); } } $stores = $manager->get_readers(); $store = $stores['logstore_standard']; // Build the report. $currentuser = $this->users[$username]; $this->setUser($currentuser->id); $groupid = 0; if ($groupname) { $groupid = $this->groupsbycourse[$courseindex][$groupname]->id; } $PAGE->set_url('/report/log/index.php?id=' . $this->courses[$courseindex]->id); $renderable = new \report_log_renderable("", (int) $this->courses[$courseindex]->id, 0, 0, '', $groupid); $renderable->setup_table(); $table = $renderable->tablelog; $store->flush(); $table->query_db(100); $usernames = []; foreach ($table->rawdata as $event) { if (get_class($event) !== \core\event\course_viewed::class) { continue; } $user = core_user::get_user($event->userid, '*', MUST_EXIST); $usernames[] = $user->username; } sort($expectedusers); sort($usernames); $this->assertEquals($expectedusers, $usernames); } /** * Test getting logs for deleted courses. * * @covers \report_log_renderable::setup_table * @return void */ public function test_get_deleted_course_logs(): void { global $DB, $PAGE; $this->preventResetByRollback(); // Ensure events can be recorded in log store. // Configure log store and user. set_config('enabled_stores', 'logstore_standard', 'tool_log'); $manager = get_log_manager(true); $stores = $manager->get_readers(); $store = $stores['logstore_standard']; $this->setUser(get_admin()); // Set and delete course. $course = reset($this->courses); $deletedcourseid = $course->id; delete_course($course, false); $this->assertFalse($DB->record_exists('course', ['id' => $deletedcourseid])); // Test rendering. $PAGE->set_url('/report/log/index.php?id=' . $deletedcourseid); $renderable = new \report_log_renderable('', (int) $deletedcourseid); $renderable->setup_table(); $store->flush(); $table = $renderable->tablelog; $table->query_db(100); // Confirm we have logs for the course deletion and that the filtering is correct. $this->assertNotEmpty($table->totalrows); $expectedrows = count($DB->get_records('logstore_standard_log', ['courseid' => $deletedcourseid])); $this->assertEquals($expectedrows, $table->totalrows); } } PK{\}$ log/user.phpnu[. /** * Display user activity reports for a course (totals) * * @package report_log * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../../config.php'); require_once($CFG->dirroot.'/report/log/locallib.php'); require_once($CFG->dirroot.'/lib/tablelib.php'); $userid = required_param('id', PARAM_INT); $courseid = required_param('course', PARAM_INT); $mode = optional_param('mode', 'today', PARAM_ALPHA); $page = optional_param('page', 0, PARAM_INT); $perpage = optional_param('perpage', 100, PARAM_INT); $logreader = optional_param('logreader', '', PARAM_COMPONENT); // Log reader which will be used for displaying logs. if ($mode !== 'today' and $mode !== 'all') { $mode = 'today'; } $user = $DB->get_record('user', array('id' => $userid, 'deleted' => 0), '*', MUST_EXIST); $course = $DB->get_record('course', ['id' => $courseid], '*'); $sitecoursefilter = 0; if (!$course) { // Missing courses may have be deleted, so display them in site context. $course = $SITE; $sitecoursefilter = $courseid; } $coursecontext = context_course::instance($course->id); $personalcontext = context_user::instance($user->id); if ($course->id == SITEID) { $PAGE->set_context($personalcontext); } if ($USER->id != $user->id and has_capability('moodle/user:viewuseractivitiesreport', $personalcontext) and !is_enrolled($coursecontext, $USER) and is_enrolled($coursecontext, $user)) { //TODO: do not require parents to be enrolled in courses - this is a hack! require_login(); $PAGE->set_course($course); } else { require_login($course); } list($all, $today) = report_log_can_access_user_report($user, $course); if (!$today && !$all) { throw new \moodle_exception('nocapability', 'report_log'); } if ($mode === 'today') { if (!$today) { require_capability('report/log:viewtoday', $coursecontext); } } else { if (!$all) { require_capability('report/log:view', $coursecontext); } } $stractivityreport = get_string('activityreport'); $PAGE->set_pagelayout('report'); $PAGE->set_url('/report/log/user.php', ['id' => $user->id, 'course' => $courseid, 'mode' => $mode]); $PAGE->navigation->extend_for_user($user); $PAGE->navigation->set_userid_for_parent_checks($user->id); // see MDL-25805 for reasons and for full commit reference for reversal when fixed. $PAGE->set_title("$course->shortname: $stractivityreport"); // Create the appropriate breadcrumb. $navigationnode = array( 'url' => new moodle_url('/report/log/user.php', ['id' => $user->id, 'course' => $courseid, 'mode' => $mode]), ); if ($mode === 'today') { $navigationnode['name'] = get_string('todaylogs'); } else { $navigationnode['name'] = get_string('alllogs'); } $PAGE->add_report_nodes($user->id, $navigationnode); if ($course->id == SITEID) { $PAGE->set_heading(fullname($user, has_capability('moodle/site:viewfullnames', $PAGE->context))); } else { $PAGE->set_heading($course->fullname); } // Trigger a user logs viewed event. $event = \report_log\event\user_report_viewed::create(array('context' => $coursecontext, 'relateduserid' => $userid, 'other' => array('mode' => $mode))); $event->trigger(); echo $OUTPUT->header(); if ($course->id != SITEID) { $userheading = array( 'heading' => fullname($user, has_capability('moodle/site:viewfullnames', $PAGE->context)), 'user' => $user, 'usercontext' => $personalcontext, ); echo $OUTPUT->context_header($userheading, 2); if ($mode === 'today') { echo $OUTPUT->heading(get_string('todaylogs', 'moodle'), 2, 'main mt-4 mb-4'); } else { echo $OUTPUT->heading(get_string('alllogs', 'moodle'), 2, 'main mt-4 mb-4'); } } // Time to filter records from. if ($mode === 'today') { $timefrom = usergetmidnight(time()); } else { $timefrom = 0; } $output = $PAGE->get_renderer('report_log'); $reportlog = new report_log_renderable($logreader, $course, $user->id, 0, '', -1, -1, false, false, true, false, $PAGE->url, $timefrom, '', $page, $perpage, 'timecreated DESC', '' , $sitecoursefilter); // Setup table if log reader is enabled. if (!empty($reportlog->selectedlogreader)) { $reportlog->setup_table(); $reportlog->tablelog->is_downloadable(false); } echo $output->reader_selector($reportlog); // Print the graphic chart accordingly to the mode (all, today). echo '
'; report_log_print_graph($course, $user, $mode, 0, $logreader, $sitecoursefilter); echo '
'; echo $output->render($reportlog); echo $OUTPUT->footer(); PK{\3log/settings.phpnu[. /** * Links and settings * * Contains settings used by logs report. * * @package report_log * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; // Just a link to course report. $ADMIN->add('reports', new admin_externalpage('reportlog', get_string('log', 'admin'), $CFG->wwwroot . "/report/log/index.php?id=0", 'report/log:view')); // No report settings. $settings = null; PK{\f.infectedfiles/lang/en/report_infectedfiles.phpnu[. /** * Infected file report * * @package report_infectedfiles * @author Nathan Nguyen * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['author'] = 'Author'; $string['confirmdelete'] = 'Are you sure you want to delete this file?'; $string['confirmdeleteall'] = 'Are you sure you want to delete all files?'; $string['confirmdownload'] = 'Are you sure you want to download this file?'; $string['confirmdownloadall'] = 'Are you sure you want to download all files?'; $string['filename'] = 'File name'; $string['infectedfiles'] = 'Antivirus failures'; $string['privacy:metadata:infected_files'] = 'This table stores information on antivirus failures detected by the system.'; $string['privacy:metadata:infected_files:filename'] = 'The name of the infected file uploaded by the user.'; $string['privacy:metadata:infected_files:timecreated'] = 'The timestamp of when a user uploaded an infected file.'; $string['privacy:metadata:infected_files:userid'] = 'The user ID of the user who uploaded an infected file.'; $string['privacy:metadata:infected_files_subcontext'] = 'Antivirus failures'; $string['pluginname'] = 'Infected files'; $string['quarantinedfile'] = 'Quarantined file'; $string['reason'] = 'Failure reason'; $string['timecreated'] = 'Time created'; PK{\6[P,,infectedfiles/version.phpnu[. /** * Infected file report * * @package report_infectedfiles * @author Nathan Nguyen * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'report_infectedfiles'; PK{\#((*infectedfiles/classes/privacy/provider.phpnu[. /** * Infected file report * * @package report_infectedfiles * @author Nathan Nguyen * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_infectedfiles\privacy; use core_privacy\local\metadata\collection; use core_privacy\local\request; defined('MOODLE_INTERNAL') || die(); /** * Infected file report * * @package report_infectedfiles * @author Nathan Nguyen * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\provider, request\plugin\provider, request\core_userlist_provider { /** * This plugin stores the userid of infected users. * * @param collection $collection the collection object to add data to. * @return collection The populated collection. */ public static function get_metadata(collection $collection): collection { $collection->add_database_table( 'infected_files', [ 'userid' => 'privacy:metadata:infected_files:userid', 'filename' => 'privacy:metadata:infected_files:filename', 'timecreated' => 'privacy:metadata:infected_files:timecreated', ], 'privacy:metadata:infected_files' ); return $collection; } /** * This function gets the contexts containing data for a userid. * * @param int $userid The userid to get contexts for. * @return request\contextlist the context list for the user. */ public static function get_contexts_for_userid(int $userid): request\contextlist { $contextlist = new request\contextlist(); // The system context is the only context where information is stored. $contextlist->add_system_context(); return $contextlist; } /** * This function exports user data on infected files from the contextlist provided. * * @param request\approved_contextlist $contextlist * @return void */ public static function export_user_data(request\approved_contextlist $contextlist) { global $DB; foreach ($contextlist as $context) { // We only export from system context. if ($context->contextlevel === CONTEXT_SYSTEM) { $userid = $contextlist->get_user()->id; $exportdata = []; $records = $DB->get_records('infected_files', ['userid' => $userid]); foreach ($records as $record) { // Export only the data that does not expose internal information. $data = []; $data['userid'] = $record->userid; $data['timecreated'] = $record->timecreated; $data['filename'] = $record->filename; $exportdata[] = $data; } // Now export this data in the infected files table as subcontext. request\writer::with_context($context)->export_data( [get_string('privacy:metadata:infected_files_subcontext', 'report_infectedfiles')], (object) $exportdata ); } } } /** * As this report tracks potential attempted security violations, * This data should not be deleted at request. This would allow for an * avenue for a malicious user to cover their tracks. This function deliberately * does no deletes. * * @param \context $context the context to delete for. * @return void */ public static function delete_data_for_all_users_in_context(\context $context) { return; } /** * As this report tracks potential attempted security violations, * This data should not be deleted at request. This would allow for an * avenue for a malicious user to cover their tracks. This function deliberately * does no deletes. * * @param \core_privacy\local\request\approved_contextlist $contextlist the contextlist to delete for. * @return void */ public static function delete_data_for_user(request\approved_contextlist $contextlist) { return; } /** * This gets the list of users inside of the provided context. In this case, its only system context * which contains users. * * @param \core_privacy\local\request\userlist $userlist * @return void */ public static function get_users_in_context(request\userlist $userlist) { $context = $userlist->get_context(); if ($context->contextlevel === CONTEXT_SYSTEM) { // If we are checking system context, we need to get all distinct userids from the table. $sql = 'SELECT DISTINCT userid FROM {infected_files}'; $userlist->add_from_sql('userid', $sql, []); } } /** * As this report tracks potential attempted security violations, * This data should not be deleted at request. This would allow for an * avenue for a malicious user to cover their tracks. This function deliberately * does no deletes. * * @param request\approved_userlist $userlist * @return void */ public static function delete_data_for_users(request\approved_userlist $userlist) { return; } } PK{\&p)infectedfiles/classes/output/renderer.phpnu[. /** * Infected file report renderer * * @package report_infectedfiles * @author Nathan Nguyen * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_infectedfiles\output; use report_infectedfiles\table\infectedfiles_table; defined('MOODLE_INTERNAL') || die(); /** * Infected file report renderer * * @package report_infectedfiles * @author Nathan Nguyen * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends \plugin_renderer_base { /** * Render the table * * @param infectedfiles_table $table table of infected files * @return false|string return html code of the table * @throws \coding_exception * @throws \moodle_exception */ protected function render_infectedfiles_table(infectedfiles_table $table) { ob_start(); $table->display($table->pagesize, false); $o = ob_get_contents(); ob_end_clean(); return $o; } } PK{\V,U"U"3infectedfiles/classes/table/infectedfiles_table.phpnu[. /** * Infected file report * * @package report_infectedfiles * @author Nathan Nguyen * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_infectedfiles\table; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/tablelib.php'); /** * Infected file report * * @package report_infectedfiles * @author Nathan Nguyen * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class infectedfiles_table extends \table_sql implements \renderable { /** @var int current page. */ protected $page; /** * Table constructor * * @param int $uniqueid table id * @param \moodle_url $url page url * @param int $page current page * @param int $perpage number or record per page * @throws \coding_exception */ public function __construct($uniqueid, \moodle_url $url, $page = 0, $perpage = 30) { parent::__construct($uniqueid); $this->set_attribute('class', 'report_infectedfiles'); // Set protected properties. $this->pagesize = $perpage; $this->page = $page; // Define columns in the table. $this->define_table_columns(); // Define configs. $this->define_table_configs($url); } /** * Table columns and corresponding headers * * @throws \coding_exception */ protected function define_table_columns() { $cols = array( 'filename' => get_string('filename', 'report_infectedfiles'), 'author' => get_string('author', 'report_infectedfiles'), 'reason' => get_string('reason', 'report_infectedfiles'), 'timecreated' => get_string('timecreated', 'report_infectedfiles'), 'actions' => get_string('actions'), ); $this->define_columns(array_keys($cols)); $this->define_headers(array_values($cols)); } /** * Define table configuration * * @param \moodle_url $url */ protected function define_table_configs(\moodle_url $url) { // Set table url. $this->define_baseurl($url); // Set table configs. $this->collapsible(false); $this->sortable(false); $this->pageable(true); } /** * Builds the SQL query. * * @param bool $count When true, return the count SQL. * @return array containing sql to use and an array of params. */ protected function get_sql_and_params($count = false): array { if ($count) { $select = "COUNT(1)"; } else { $select = "*"; } $sql = "SELECT $select FROM {infected_files}"; $params = array(); if (!$count) { $sql .= " ORDER BY timecreated DESC"; } return array($sql, $params); } /** * Get data. * * @param int $pagesize number of records to fetch * @param bool $useinitialsbar initial bar * @throws \dml_exception */ public function query_db($pagesize, $useinitialsbar = true) { global $DB; list($countsql, $countparams) = $this->get_sql_and_params(true); list($sql, $params) = $this->get_sql_and_params(); $total = $DB->count_records_sql($countsql, $countparams); $this->pagesize($pagesize, $total); $this->rawdata = $DB->get_records_sql($sql, $params, $this->get_page_start(), $this->get_page_size()); // Set initial bars. if ($useinitialsbar) { $this->initialbars($total > $pagesize); } } /** * Column to display the authors fullname from userid. * * @param \stdClass $row the row from sql. * @return string the authors name. */ protected function col_author($row): string { // Get user fullname from ID. $user = \core_user::get_user($row->userid); $url = new \moodle_url('/user/profile.php', ['id' => $row->userid]); return \html_writer::link($url, fullname($user)); } /** * Column to display the failure reason. * * @param \stdClass $row the row from sql. * @return string the formatted reason. */ protected function col_reason($row) { return format_text($row->reason); } /** * Custom actions column * * @param \stdClass $row an incident record. * @return string content of action column. * @throws \coding_exception * @throws \moodle_exception */ protected function col_actions($row): string { global $OUTPUT; $filename = $row->quarantinedfile; $fileid = $row->id; // If the file isn't found, we can do nothing in this column. // This shouldn't happen, unless the file is manually deleted from the server externally. if (!\core\antivirus\quarantine::quarantined_file_exists($filename)) { return ''; } $links = ''; $managefilepage = new \moodle_url('/report/infectedfiles/index.php'); // Download. $downloadparams = ['file' => $fileid, 'action' => 'download', 'sesskey' => sesskey()]; $downloadurl = new \moodle_url($managefilepage, $downloadparams); $downloadconfirm = new \confirm_action(get_string('confirmdownload', 'report_infectedfiles')); $links .= $OUTPUT->action_icon( $downloadurl, new \pix_icon('t/download', get_string('download')), $downloadconfirm ); // Delete. $deleteparams = ['file' => $fileid, 'action' => 'delete', 'sesskey' => sesskey()]; $deleteurl = new \moodle_url($managefilepage, $deleteparams); $deleteconfirm = new \confirm_action(get_string('confirmdelete', 'report_infectedfiles')); $links .= $OUTPUT->action_icon( $deleteurl, new \pix_icon('t/delete', get_string('delete')), $deleteconfirm ); return $links; } /** * Custom time column. * * @param \stdClass $row an incident record. * @return string time created in user-friendly format. */ protected function col_timecreated($row): string { return userdate($row->timecreated); } /** * Display table with download all and delete all buttons * * @param int $pagesize number or records perpage * @param bool $useinitialsbar use the bar or not * @param string $downloadhelpbutton help button * @return void * @throws \coding_exception * @throws \moodle_exception */ public function display($pagesize, $useinitialsbar, $downloadhelpbutton='') { global $OUTPUT; // Output the table, and then display buttons. $this->out($pagesize, $useinitialsbar, $downloadhelpbutton); $managefilepage = new \moodle_url('/report/infectedfiles/index.php'); // If there are no rows, dont bother rendering extra buttons. if (empty($this->rawdata)) { return; } // Delete All. $deleteallparams = ['action' => 'deleteall', 'sesskey' => sesskey()]; $deleteallurl = new \moodle_url($managefilepage, $deleteallparams); $deletebutton = new \single_button($deleteallurl, get_string('deleteall'), 'post', \single_button::BUTTON_PRIMARY); $deletebutton->add_confirm_action(get_string('confirmdeleteall', 'report_infectedfiles')); echo $OUTPUT->render($deletebutton); echo " "; // Download All. $downloadallparams = ['action' => 'downloadall', 'sesskey' => sesskey()]; $downloadallurl = new \moodle_url($managefilepage, $downloadallparams); $downloadbutton = new \single_button($downloadallurl, get_string('downloadall'), 'post', \single_button::BUTTON_PRIMARY); $downloadbutton->add_confirm_action(get_string('confirmdownloadall', 'report_infectedfiles')); echo $OUTPUT->render($downloadbutton); } } PK{\6. infectedfiles/index.phpnu[. /** * Infected file report * * @package report_infectedfiles * @author Nathan Nguyen * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../config.php'); require_once($CFG->libdir.'/adminlib.php'); use \core\antivirus\quarantine; admin_externalpage_setup('reportinfectedfiles', '', null, '', array('pagelayout' => 'report')); $action = optional_param('action', '', PARAM_TEXT); // If action exists, we need to process the actions safely. if (!empty($action)) { // Nothing can be done without a valid sesskey. require_sesskey(); // For any cancel actions, just reload the page clean. $cancel = $PAGE->url; // Decide on page action. switch ($action) { case 'download': $fileid = required_param('file', PARAM_INT); quarantine::download_quarantined_file($fileid); break; case 'downloadall': quarantine::download_all_quarantined_files(); break; case 'delete': $fileid = required_param('file', PARAM_INT); quarantine::delete_quarantined_file($fileid); break; case 'deleteall': // Remove file until current time. quarantine::clean_up_quarantine_folder(time()); break; } // Reload page cleanly once actions are processed. redirect($PAGE->url); } // Once actions are dealt with, display the page. $page = optional_param('page', 0, PARAM_INT); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('infectedfiles', 'report_infectedfiles')); $table = new \report_infectedfiles\table\infectedfiles_table('report-infectedfiles-report-table', $PAGE->url, $page); $table->define_baseurl($PAGE->url); echo $PAGE->get_renderer('report_infectedfiles')->render($table); echo $OUTPUT->footer(); PK{\infectedfiles/settings.phpnu[. /** * Infected file report * * @package report_infectedfiles * @author Nathan Nguyen * @copyright Catalyst IT * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $ADMIN->add('reports', new admin_externalpage('reportinfectedfiles', get_string('infectedfiles', 'report_infectedfiles'), "$CFG->wwwroot/report/infectedfiles/index.php")); $settings = null; PK{\~t??(themeusage/lang/en/report_themeusage.phpnu[. /** * Lang strings for theme usage report. * * @package report_themeusage * @copyright 2023 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['getreport'] = 'Get report'; $string['invalidparametertheme'] = 'Invalid parameter set for theme.'; $string['pluginname'] = 'Theme usage'; $string['privacy:metadata'] = 'The theme report plugin does not store any personal data.'; $string['themename'] = 'Theme name'; $string['themeusage'] = 'Theme usage'; $string['themeusagereport'] = 'Theme usage report'; $string['themeusagereportall'] = 'All uses of {$a}'; $string['themeusagereportcategory'] = 'Categories using {$a}'; $string['themeusagereportcohort'] = 'Cohorts using {$a}'; $string['themeusagereportcourse'] = 'Courses using {$a}'; $string['themeusagereportuser'] = 'Users using {$a}'; $string['usagetype'] = 'Usage type'; PK{\themeusage/version.phpnu[. /** * Version info. * * @package report_themeusage * @copyright 2023 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_themeusage'; // Full name of the plugin (used for diagnostics). PK{\3y9themeusage/classes/reportbuilder/local/entities/theme.phpnu[. namespace report_themeusage\reportbuilder\local\entities; use core_reportbuilder\local\entities\base; use core_reportbuilder\local\report\column; use lang_string; /** * Theme entity. * * Defines all the columns and filters that can be added to reports that use this entity. * * @package report_themeusage * @copyright 2023 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class theme extends base { /** * Database tables that this entity uses. * * @return array */ protected function get_default_tables(): array { return [ 'config_plugins', ]; } /** * The default title for this entity. * * @return lang_string */ protected function get_default_entity_title(): lang_string { return new lang_string('theme'); } /** * Initialize the entity. * * @return base */ public function initialise(): base { $columns = $this->get_all_columns(); foreach ($columns as $column) { $this->add_column($column); } return $this; } /** * Returns list of all available columns. * * @return column[] */ protected function get_all_columns(): array { global $DB; $themealias = $this->get_table_alias('config_plugins'); $sqlsubstring = $DB->sql_substr("{$themealias}.plugin", 7); $courselabel = get_string('course'); $cohortlabel = get_string('cohort', 'cohort'); $userlabel = get_string('user'); $categorylabel = get_string('category'); // Force theme column. $columns[] = (new column( 'forcetheme', new lang_string('forcetheme'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->set_type(column::TYPE_TEXT) ->add_fields("{$themealias}.plugin") ->add_callback(static function(?string $theme): string { $theme = get_string('pluginname', $theme); return format_text($theme, FORMAT_PLAIN); }); // Usage type column. $columns[] = (new column( 'usagetype', new lang_string('usagetype', 'report_themeusage'), $this->get_entity_name() )) ->add_joins($this->get_joins()) ->add_join("LEFT JOIN ( SELECT '{$courselabel}' AS usagetype, theme, COUNT(theme) AS themecount FROM {course} WHERE " . $DB->sql_isnotempty('course', 'theme', false, false) . " GROUP BY theme UNION SELECT '{$userlabel}' AS usagetype, theme, COUNT(theme) AS themecount FROM {user} WHERE " . $DB->sql_isnotempty('user', 'theme', false, false) . " GROUP BY theme UNION SELECT '{$cohortlabel}' AS usagetype, theme, COUNT(theme) AS themecount FROM {cohort} WHERE " . $DB->sql_isnotempty('cohort', 'theme', false, false) . " GROUP BY theme UNION SELECT '{$categorylabel}' AS usagetype, theme, COUNT(theme) AS themecount FROM {course_categories} WHERE " . $DB->sql_isnotempty('course_categories', 'theme', false, false) . " GROUP BY theme ) tuse ON tuse.theme={$sqlsubstring}") ->set_type(column::TYPE_TEXT) ->add_fields("tuse.usagetype, tuse.themecount") ->add_callback(static function(?string $usagetype, \stdClass $row): string { $count = $row->themecount ?? 0; return format_text($usagetype . ' ('. $count . ')', FORMAT_PLAIN); }); return $columns; } } PK{\Leב!!Kthemeusage/classes/reportbuilder/local/systemreports/theme_usage_report.phpnu[. namespace report_themeusage\reportbuilder\local\systemreports; use context_system; use core_reportbuilder\local\entities\{course, user}; use core_cohort\reportbuilder\local\entities\cohort; use core_course\reportbuilder\local\entities\course_category; use core_reportbuilder\local\helpers\database; use core_reportbuilder\system_report; use core\output\theme_usage; use report_themeusage\reportbuilder\local\entities\theme; /** * Config changes system report class implementation * * @package report_themeusage * @copyright 2023 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class theme_usage_report extends system_report { /** * Initialise report, we need to set the main table, load our entities and set columns/filters. */ protected function initialise(): void { // Show results depending on the theme and type chosen. $themechoice = $this->get_parameter('themechoice', '', PARAM_TEXT); $typechoice = $this->get_parameter('typechoice', '', PARAM_TEXT); $themename = get_string('pluginname', 'theme_'.$themechoice); $themeentity = new theme(); $themealias = $themeentity->get_table_alias('config_plugins'); $this->set_main_table('config_plugins', $themealias); $this->add_entity($themeentity); $param1 = database::generate_param_name(); $param2 = database::generate_param_name(); $params = [$param1 => 'theme_' . $themechoice, $param2 => 'version']; $this->add_base_condition_sql("{$themealias}.plugin = :{$param1} AND {$themealias}.name = :{$param2}", $params); switch ($typechoice) { case theme_usage::THEME_USAGE_TYPE_ALL: $this->add_columns_theme(); $this->set_downloadable(true, get_string('themeusagereportall', 'report_themeusage', $themename)); break; case theme_usage::THEME_USAGE_TYPE_USER: $userentity = new user(); $useralias = $userentity->get_table_alias('user'); $this->add_entity($userentity->add_join( "JOIN {user} {$useralias} ON $useralias.theme = '{$themechoice}' AND {$themealias}.plugin = 'theme_{$themechoice}'")); $this->add_columns_user(); $this->add_filters_user(); $this->set_downloadable(true, get_string('themeusagereportuser', 'report_themeusage', $themename)); break; case theme_usage::THEME_USAGE_TYPE_COURSE: $courseentity = new course(); $coursealias = $courseentity->get_table_alias('course'); $this->add_entity($courseentity->add_join( "JOIN {course} {$coursealias} ON $coursealias.theme = '{$themechoice}' AND {$themealias}.plugin = 'theme_{$themechoice}'")); $this->add_columns_course(); $this->add_filters_course(); $this->set_downloadable(true, get_string('themeusagereportcourse', 'report_themeusage', $themename)); break; case theme_usage::THEME_USAGE_TYPE_COHORT: $cohortentity = new cohort(); $cohortalias = $cohortentity->get_table_alias('cohort'); $this->add_entity($cohortentity->add_join( "JOIN {cohort} {$cohortalias} ON $cohortalias.theme = '{$themechoice}' AND {$themealias}.plugin = 'theme_{$themechoice}'")); $this->add_columns_cohort(); $this->add_filters_cohort(); $this->set_downloadable(true, get_string('themeusagereportcohort', 'report_themeusage', $themename)); break; case theme_usage::THEME_USAGE_TYPE_CATEGORY: $categoryentity = new course_category(); $categoryalias = $categoryentity->get_table_alias('course_categories'); $this->add_entity($categoryentity->add_join( "JOIN {course_categories} {$categoryalias} ON $categoryalias.theme = '{$themechoice}' AND {$themealias}.plugin = 'theme_{$themechoice}'")); $this->add_columns_category(); $this->add_filters_category(); $this->set_downloadable(true, get_string('themeusagereportcategory', 'report_themeusage', $themename)); break; default: break; } } /** * Validates access to view this report. * * @return bool */ protected function can_view(): bool { return has_capability('moodle/site:config', context_system::instance()); } /** * Adds the columns we want to display in the report for 'theme'. */ protected function add_columns_theme(): void { $columns = [ 'theme:usagetype', 'theme:forcetheme', ]; $this->add_columns_from_entities($columns); $this->set_initial_sort_column('theme:forcetheme', SORT_ASC); } /** * Adds the columns we want to display in the report for 'user'. */ protected function add_columns_user(): void { $columns = [ 'user:firstname', 'user:lastname', 'theme:forcetheme', ]; $this->add_columns_from_entities($columns); $this->set_initial_sort_column('user:firstname', SORT_ASC); } /** * Adds the filters we want to display in the report for 'user'. */ protected function add_filters_user(): void { $filters = [ 'user:firstname', 'user:lastname', ]; $this->add_filters_from_entities($filters); } /** * Adds the columns we want to display in the report for 'course'. */ protected function add_columns_course(): void { $columns = [ 'course:fullname', 'course:shortname', 'theme:forcetheme', ]; $this->add_columns_from_entities($columns); $this->set_initial_sort_column('course:fullname', SORT_ASC); } /** * Adds the filters we want to display in the report for 'course'. */ protected function add_filters_course(): void { $filters = [ 'course:fullname', 'course:shortname', ]; $this->add_filters_from_entities($filters); } /** * Adds the columns we want to display in the report for 'cohort'. */ protected function add_columns_cohort(): void { $columns = [ 'cohort:name', 'cohort:context', 'theme:forcetheme', ]; $this->add_columns_from_entities($columns); $this->set_initial_sort_column('cohort:name', SORT_ASC); } /** * Adds the filters we want to display in the report for 'cohort'. */ protected function add_filters_cohort(): void { $filters = [ 'cohort:name', 'cohort:context', ]; $this->add_filters_from_entities($filters); } /** * Adds the columns we want to display in the report for 'category'. */ protected function add_columns_category(): void { $columns = [ 'course_category:name', 'course_category:coursecount', 'theme:forcetheme', ]; $this->add_columns_from_entities($columns); $this->set_initial_sort_column('course_category:name', SORT_ASC); } /** * Adds the filters we want to display in the report for 'category'. */ protected function add_filters_category(): void { $filters = [ 'course_category:name', ]; $this->add_filters_from_entities($filters); } } PK{\'aa,themeusage/classes/form/theme_usage_form.phpnu[. namespace report_themeusage\form; use moodleform; use core\output\theme_usage; /** * Defines the form for generating theme usage report data. * * @package report_themeusage * @copyright 2023 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL Juv3 or later */ class theme_usage_form extends moodleform { /** * Build the form definition. */ protected function definition() { $mform = $this->_form; // Theme choices (e.g. boost, classic). $themechoice = $this->_customdata['themechoice']; $themechoices = array_merge(['' => get_string('select') . '...'], self::get_theme_choices()); $mform->addElement('select', 'themechoice', get_string('themename', 'report_themeusage'), $themechoices); $mform->setType('themechoice', PARAM_TEXT); $mform->addRule('themechoice', get_string('required'), 'required', null, 'client'); if (!empty($themechoice)) { $mform->setDefault('themechoice', $themechoice); } // Theme usage types (e.g. user, course, cohort, category). $typechoices = self::get_type_choices(); $mform->addElement('select', 'typechoice', get_string('usagetype', 'report_themeusage'), $typechoices); $mform->setType('typechoice', PARAM_TEXT); $mform->addRule('typechoice', get_string('required'), 'required', null, 'client'); $mform->setDefault(theme_usage::THEME_USAGE_TYPE_ALL, $themechoice); // Submit button. $mform->addElement('submit', 'submit', get_string('getreport', 'report_themeusage')); } /** * Get a list of available theme usage types. * * @return array */ public static function get_type_choices(): array { return [ theme_usage::THEME_USAGE_TYPE_ALL => get_string('all'), theme_usage::THEME_USAGE_TYPE_USER => get_string('user'), theme_usage::THEME_USAGE_TYPE_COURSE => get_string('course'), theme_usage::THEME_USAGE_TYPE_COHORT => get_string('cohort', 'cohort'), theme_usage::THEME_USAGE_TYPE_CATEGORY => get_string('category'), ]; } /** * Get a list of available themes. * * @return array */ public static function get_theme_choices(): array { $themes = \core_component::get_plugin_list('theme'); foreach ($themes as $themename => $themedir) { $themechoices[$themename] = get_string('pluginname', 'theme_'.$themename); } return $themechoices; } /** * Check the requested theme is in a list of available themes. * * @param string $themechoice The theme name. * @return bool */ public static function validate_theme_choice_param(string $themechoice): bool { if (!empty($themechoice) && !array_key_exists($themechoice, self::get_theme_choices())) { return false; } return true; } } PK{\-;HT'themeusage/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_themeusage. * * @package report_themeusage * @copyright 2023 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_themeusage\privacy; /** * Privacy Subsystem for report_themeusage implementing null_provider. * * @package report_themeusage * @copyright 2023 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK{\پ themeusage/index.phpnu[. /** * Display usage information about themes. * * @package report_themeusage * @copyright 2023 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core_reportbuilder\system_report_factory; use report_themeusage\form\theme_usage_form; use report_themeusage\reportbuilder\local\systemreports\theme_usage_report; require(__DIR__.'/../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_login(); admin_externalpage_setup('reportthemeusage'); // Get URL parameters. $themechoice = optional_param('themechoice', '', PARAM_TEXT); // Check the requested theme is a valid one. if (!theme_usage_form::validate_theme_choice_param($themechoice)) { throw new \moodle_exception(get_string('invalidparametertheme', 'report_themeusage')); } // Set up the page. $pageurl = new moodle_url($CFG->wwwroot . '/report/themeusage/index.php'); $PAGE->set_url($pageurl); $PAGE->set_context(context_system::instance()); $PAGE->set_pagelayout('report'); $PAGE->set_primary_active_tab('siteadminnode'); echo $OUTPUT->header(); // Show heading. $heading = get_string('themeusage', 'report_themeusage'); echo $OUTPUT->heading($heading); // Build form with prepared data. $cutomdata['themechoice'] = $themechoice; $form = new theme_usage_form($pageurl, $cutomdata); $form->display(); if ($data = $form->get_data()) { // Build report using submitted form data. $themechoice = $data->themechoice; $typechoice = $data->typechoice; } else if (!empty($themechoice)) { // Build report with incoming theme choice and set the type to 'all'. $typechoice = 'all'; } if (!empty($themechoice) && !empty($typechoice)) { // Show a heading that explains what the report is showing. $themename = get_string('pluginname', 'theme_' . $themechoice); $reportheading = get_string('themeusagereport' . $typechoice, 'report_themeusage', $themename); echo $OUTPUT->heading($reportheading, 3, 'mt-4'); // Build the report. $reportparams = ['themechoice' => $themechoice, 'typechoice' => $typechoice]; $report = system_report_factory::create(theme_usage_report::class, context_system::instance(), '', '', 0, $reportparams); echo $report->output(); } // Show footer. echo $OUTPUT->footer(); PK{\AM%themeusage/tests/theme_usage_test.phpnu[. namespace report_themeusage; use testing_data_generator; use core\output\theme_usage; /** * Unit tests for theme usage. * * @package report_themeusage * @copyright 2023 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class theme_usage_test extends \advanced_testcase { /** @var testing_data_generator Data generator. */ private testing_data_generator $generator; /** * Set up function for tests. */ protected function setUp(): void { parent::setUp(); $this->resetAfterTest(); $this->generator = $this->getDataGenerator(); } /** * Test is_theme_used_in_any_context method. * * @covers ::is_theme_used_in_any_context * @covers ::theme_purge_used_in_context_caches */ public function test_is_theme_used_in_any_context(): void { // Enable theme overrides. set_config('allowuserthemes', 1); set_config('allowcoursethemes', 1); set_config('allowcohortthemes', 1); set_config('allowcategorythemes', 1); $theme = 'boost'; // Check there are no contexts using 'boost' as their preferred theme yet. $usedinanycontext = theme_usage::is_theme_used_in_any_context($theme); $this->assertEquals(theme_usage::THEME_IS_NOT_USED, $usedinanycontext); // Create a user and set its theme preference to 'boost'. // The outcome of this test should be the same if we use a cohort/course/category. $this->generator->create_user(['theme' => $theme]); // Because we have already checked and cached a response, purge this cache. theme_purge_used_in_context_caches(); $usedinanycontext = theme_usage::is_theme_used_in_any_context($theme); $this->assertEquals(theme_usage::THEME_IS_USED, $usedinanycontext); // Double-check the the cache is set for the theme. $cache = \cache::make('core', 'theme_usedincontext')->get($theme); $this->assertEquals(theme_usage::THEME_IS_USED, $cache); } /** * Test the deleting of cache using theme_delete_used_in_context_cache. * * @covers ::theme_delete_used_in_context_cache */ public function test_theme_delete_used_in_context_cache(): void { // Enable theme override. set_config('allowuserthemes', 1); // Create a user and set its theme preference to 'boost'. $theme = 'boost'; $user = $this->generator->create_user(['theme' => $theme]); // Check for theme usage. This will create a cached result. theme_usage::is_theme_used_in_any_context($theme); $cache = \cache::make('core', 'theme_usedincontext')->get($theme); $this->assertEquals(theme_usage::THEME_IS_USED, $cache); // Delete the cache by switching themes. theme_delete_used_in_context_cache('classic', $user->theme); $cache = \cache::make('core', 'theme_usedincontext')->get($theme); $this->assertFalse($cache); } } PK{\ю*themeusage/tests/behat/theme_usage.featurenu[@report @report_themeusage Feature: Navigate to a theme usage report In order to see a theme usage report As an admin I need to set a theme for user/course/category/cohort and view the report Background: Given the following config values are set as admin: | allowuserthemes | 1 | | allowcoursethemes | 1 | | allowcategorythemes | 1 | | allowcohortthemes | 1 | And I log in as "admin" Scenario: I am able to see theme usage report for all contexts overriding the default theme Given the following "courses" exist: | fullname | shortname | theme | | Course 1 | course1 | boost | | Course 2 | course2 | boost | And the following "user" exists: | username | student1 | | firstname | Student | | lastname | One | | theme | boost | And I navigate to "Reports > Theme usage" in site administration And I set the field "Theme name" to "boost" And I set the field "Usage type" to "all" When I press "Get report" Then the following should exist in the "reportbuilder-table" table: | Usage type | Force theme | | Course (2) | Boost | | User (1) | Boost | Scenario: I am able to see theme usage report for courses overriding the default theme Given the following "course" exists: | fullname | Course 1 | | shortname | course1 | | theme | boost | And I navigate to "Reports > Theme usage" in site administration And I set the field "Theme name" to "boost" And I set the field "Usage type" to "course" When I press "Get report" Then the following should exist in the "reportbuilder-table" table: | Course full name | Course short name | Force theme | | Course 1 | course1 | Boost | Scenario: I am able to see theme usage report for users overriding the default theme Given the following "user" exists: | username | student1 | | firstname | Student | | lastname | One | | theme | boost | And I navigate to "Reports > Theme usage" in site administration And I set the field "Theme name" to "boost" And I set the field "Usage type" to "user" When I press "Get report" Then the following should exist in the "reportbuilder-table" table: | First name | Last name | Force theme | | Student | One | Boost | Scenario: I am able to see theme usage report for cohorts overriding the default theme Given the following "cohort" exists: | name | Cohort 1 | | idnumber | cohort1 | | context | system | And I navigate to "Users > Accounts > Cohorts" in site administration And I press "Edit" action in the "cohort1" report row And I set the field "theme" to "boost" And I press "Save changes" And I navigate to "Reports > Theme usage" in site administration And I set the field "Theme name" to "boost" And I set the field "Usage type" to "cohort" When I press "Get report" Then the following should exist in the "reportbuilder-table" table: | Name | Category | Force theme | | Cohort 1 | System | Boost | Scenario: I am able to see theme usage report for categories overriding the default theme Given the following "categories" exist: | name | category | idnumber | | Category 1 | 0 | category1 | And I navigate to "Courses > Manage courses and categories" in site administration And I click on "edit" action for "Category 1" in management category listing And I set the field "theme" to "boost" And I press "Save changes" And I navigate to "Reports > Theme usage" in site administration And I set the field "Theme name" to "boost" And I set the field "Usage type" to "category" When I press "Get report" Then the following should exist in the "reportbuilder-table" table: | Category name | Course count | Force theme | | Category 1 | 0 | Boost | PK{\themeusage/settings.phpnu[. /** * Settings and links. * * @package report_themeusage * @copyright 2023 David Woloszyn * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $ADMIN->add('reports', new admin_externalpage( 'reportthemeusage', get_string('pluginname', 'report_themeusage'), "$CFG->wwwroot/report/themeusage/index.php", 'moodle/site:config', ), ); // No report settings. $settings = null; PK{\&classes/privacy/provider.phpnu[. namespace core_report\privacy; /** * Privacy Subsystem implementation for core_report. * * @package core_report * @copyright 2024 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK{\s6'6'"classes/output/coursestructure.phpnu[. namespace core_report\output; use core\output\local\properties\iconsize; use core_course\output\activity_icon; /** * Course sections, subsections and activities structure for reports. * * @package core_report * @copyright 2024 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class coursestructure implements \renderable, \templatable { /** * Constructor * * @param \course_modinfo $modinfo */ public function __construct( /** @var \course_modinfo $modinfo */ protected \course_modinfo $modinfo ) { } /** * Exports the data. * * @param \renderer_base $output * @return array|\stdClass */ public function export_for_template(\renderer_base $output) { $headers = $this->export_headers($output); return [ 'class' => 'generaltable boxaligncenter', 'headers' => $headers, 'headerscount' => count($headers), 'activities' => $this->export_activities($output), ]; } /** * Exports activities data. * * @param \renderer_base $output * @return array|\stdClass */ public function export_activities(\renderer_base $output) { $activities = []; $delegatedsections = $this->modinfo->get_sections_delegated_by_cm(); $allsections = $this->modinfo->get_sections(); foreach ($allsections as $sectionnum => $sectionmodules) { // Add the section row. if ($sectionnum > 0) { $sectioninfo = $this->modinfo->get_section_info($sectionnum); // Don't show subsections here. We are showing them in the corresponding module. if ($sectioninfo->get_component_instance()) { continue; } if (!$sectioninfo->uservisible) { continue; } $activities[] = $this->export_section_data($output, $sectioninfo, false); } // Add section modules and possibly subsections. foreach ($sectionmodules as $cmid) { $cm = $this->modinfo->cms[$cmid]; // Check if the module is delegating a section. if (array_key_exists($cm->id, $delegatedsections)) { $subsectioninfo = $delegatedsections[$cm->id]; // Only non-empty are listed in allsections. We don't show empty sections. if (!array_key_exists($subsectioninfo->sectionnum, $allsections)) { continue; } $activities[] = $this->export_section_data($output, $subsectioninfo, true); // Show activities inside the section. $subsectionmodules = $allsections[$subsectioninfo->sectionnum]; foreach ($subsectionmodules as $subsectioncmid) { $cm = $this->modinfo->cms[$subsectioncmid]; $activities[] = $this->export_activity_data($output, $cm, true); } } else { // It's simply a module. $activities[] = $this->export_activity_data($output, $cm); } } } return $activities; } /** * Exports course sections, sections delegated by modules and modules data in a hierarchical format. * * @param \renderer_base $output * @return array|\stdClass */ public function export_hierarchy(\renderer_base $output) { $sections = []; $allsections = $this->modinfo->get_sections(); foreach ($allsections as $sectionnum => $sectionmodules) { // Add the section row. $sectioninfo = $this->modinfo->get_section_info($sectionnum); // Don't show subsections here. We are showing them in the corresponding module. if ($sectioninfo->get_component_instance()) { continue; } if (!$sectioninfo->uservisible) { continue; } $section = $this->export_section_data($output, $sectioninfo, false); if (empty($sectioninfo) || empty($sectioninfo->sequence)) { continue; } $activities = $this->export_hierarchy_section_activities_data($output, $sectioninfo, $allsections); if (!empty($activities)) { $section['activities'] = $activities; } $sections[] = $section; } return $sections; } /** * Exports activities data for a section in a hierarchical format. * @param \renderer_base $output * @param \section_info $sectioninfo * @param array $allsections * @return array */ private function export_hierarchy_section_activities_data( \renderer_base $output, \section_info $sectioninfo, array $allsections ): array { $allsections = $this->modinfo->get_sections(); $sectionmodules = explode(",", $sectioninfo->sequence); $activities = []; // Add section modules and possibly subsections. foreach ($sectionmodules as $cmid) { $activity = $this->export_hierarchy_activity_data($output, $this->modinfo->cms[$cmid], $allsections); if (!empty($activity)) { $activities[] = $activity; } } return $activities; } /** * Exports activity data for a section in a hierarchical format. * @param \renderer_base $output * @param \cm_info $cm * @param array $allsections * @return array|null */ private function export_hierarchy_activity_data( \renderer_base $output, \cm_info $cm, array $allsections ): ?array { $delegatedsections = $this->modinfo->get_sections_delegated_by_cm(); // Subsections has a special export. if (array_key_exists($cm->id, $delegatedsections)) { $subsectioninfo = $delegatedsections[$cm->id]; // Only non-empty are listed in allsections. We don't show empty sections. if (!array_key_exists($subsectioninfo->sectionnum, $allsections)) { return null; } $subsection = $this->export_section_data($output, $subsectioninfo, true); if (empty($subsection)) { return null; } // Show activities inside the section. $subsectionmodules = $allsections[$subsectioninfo->sectionnum]; $subactivities = []; foreach ($subsectionmodules as $subsectioncmid) { $cm = $this->modinfo->cms[$subsectioncmid]; $activity = $this->export_activity_data($output, $cm, true); if (!empty($activity)) { $subactivities[] = $activity; } } if (!empty($subactivities)) { $subsection['activities'] = $subactivities; } return $subsection; } return $this->export_activity_data($output, $cm); } /** * Exports the headers for report table. * * @param \renderer_base $output * @return array */ protected function export_headers(\renderer_base $output): array { return [get_string('activity')]; } /** * Exports the data for a single section. * * @param \renderer_base $output * @param \section_info $sectioninfo Section to export information from. * @param bool $isdelegated Whether the section is a delegated subsection or not. * @return array */ public function export_section_data(\renderer_base $output, \section_info $sectioninfo, bool $isdelegated = false): array { $datasection = [ 'issection' => !$isdelegated, 'isdelegated' => $isdelegated, 'visible' => $sectioninfo->visible, 'class' => 'section', 'text' => get_section_name($sectioninfo->course, $sectioninfo->sectionnum), ]; return $datasection; } /** * Exports the data for a single activity. * * @param \renderer_base $output * @param \cm_info $cm * @param bool $indelegated Whether the activity is part of a delegated section or not. * @return array */ public function export_activity_data(\renderer_base $output, \cm_info $cm, bool $indelegated = false): array { global $CFG; if (!$cm->has_view()) { return []; } if (!$cm->uservisible) { return []; } $dataactivity = [ 'isactivity' => true, 'indelegated' => $indelegated, 'visible' => $cm->visible, 'cells' => [], ]; $activityicon = activity_icon::from_cm_info($cm) ->set_icon_size(iconsize::SIZE4); $dataactivity['activitycolumn'] = [ 'activityicon' => $output->render($activityicon), 'link' => "$CFG->wwwroot/mod/$cm->modname/view.php?id=$cm->id", 'text' => $cm->get_formatted_name(), ]; return $dataactivity; } } PK{\ly3"backups/lang/en/report_backups.phpnu[. /** * Strings for component 'report_backups' * * @package report * @subpackage backups * @copyright 2007 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['backupofcourselogs'] = 'Backup logs of {$a}'; $string['logsofbackupexecutedon'] = 'Logs of the backup executed on {$a}'; $string['nobackupsfound'] = 'There were no backups found.'; $string['nologsfound'] = 'There were no logs found for this backup.'; $string['pluginname'] = 'Backups report'; $string['strftimetime'] = '%I:%M:%S %p'; $string['viewlogs'] = 'View logs'; $string['privacy:metadata'] = 'The Backups reports plugin does not store any personal data.'; PK{\8backups/version.phpnu[. /** * Version details. * * @package report * @subpackage backups * @copyright 2007 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_backups'; // Full name of the plugin (used for diagnostics) PK{\b$backups/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_backups. * * @package report_backups * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_backups\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_backups implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\DzJ5""backups/index.phpnu[. /** * A report to display the outcome of scheduled backups * * @package report * @subpackage backups * @copyright 2007 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->libdir . '/adminlib.php'); // Required for backup::xxx constants. require_once($CFG->dirroot . '/backup/util/interfaces/checksumable.class.php'); require_once($CFG->dirroot . '/backup/backup.class.php'); $courseid = optional_param('courseid', 0, PARAM_INT); $page = optional_param('page', 0, PARAM_INT); // This represents which backup we are viewing. // Required for constants in backup_cron_automated_helper require_once($CFG->dirroot.'/backup/util/helper/backup_cron_helper.class.php'); admin_externalpage_setup('reportbackups', '', null, '', array('pagelayout'=>'report')); $strftimedatetime = get_string('strftimerecent'); $strerror = get_string('error'); $strok = get_string('statusok'); $strunfinished = get_string('unfinished'); $strskipped = get_string('skipped'); $strwarning = get_string('warning'); $strnotyetrun = get_string('backupnotyetrun'); $strqueued = get_string('queued'); if ($courseid) { $course = $DB->get_record('course', array('id' => $courseid), 'id, fullname', MUST_EXIST); // Get the automated backups that have been performed for this course. $params = array('operation' => backup::OPERATION_BACKUP, 'type' => backup::TYPE_1COURSE, 'itemid' => $course->id, 'interactive' => backup::INTERACTIVE_NO); if ($backups = $DB->get_records('backup_controllers', $params, 'timecreated DESC', 'id, backupid, status, timecreated', $page, 1)) { // Get the backup we want to use. $backup = reset($backups); // Get the backup status. if ($backup->status == backup::STATUS_FINISHED_OK) { $status = $strok; $statusclass = 'table-success'; // Green. } else if ($backup->status == backup::STATUS_AWAITING || $backup->status == backup::STATUS_EXECUTING) { $status = $strunfinished; $statusclass = 'table-danger'; // Red. } else { // Else show error. $status = $strerror; $statusclass = 'table-danger'; // Red. } $table = new html_table(); $table->head = array(''); $table->data = array(); $statusrow = get_string('status') . ' - ' . html_writer::tag('span', $status, array('class' => $statusclass)); $table->data[] = array($statusrow); // Get the individual logs for this backup. if ($logs = $DB->get_records('backup_logs', array('backupid' => $backup->backupid), 'timecreated ASC', 'id, message, timecreated')) { foreach ($logs as $log) { $table->data[] = array(userdate($log->timecreated, get_string('strftimetime', 'report_backups')) . ' - ' . $log->message); } } else { $table->data[] = array(get_string('nologsfound', 'report_backups')); } } // Set the course name to display. $coursename = format_string($course->fullname, true, array('context' => context_course::instance($course->id))); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('backupofcourselogs', 'report_backups', $coursename)); if (isset($backup)) { // We put this logic down here as we may be viewing a backup that was performed which there were no logs // recorded for. We still want to display the pagination so the user can still navigate to other backups, // and we also display a message so they are aware that the backup happened but there were no logs. $baseurl = new moodle_url('/report/backups/index.php', array('courseid' => $courseid)); $numberofbackups = $DB->count_records('backup_controllers', $params); $pagingbar = new paging_bar($numberofbackups, $page, 1, $baseurl); echo $OUTPUT->render($pagingbar); echo $OUTPUT->heading(get_string('logsofbackupexecutedon', 'report_backups', userdate($backup->timecreated)), 3); echo html_writer::table($table); echo $OUTPUT->render($pagingbar); } else { echo $OUTPUT->box(get_string('nobackupsfound', 'report_backups'), 'center'); } echo $OUTPUT->footer(); exit(); } $table = new html_table; $table->head = array( get_string("course"), get_string("timetaken", "backup"), get_string("status"), get_string("backupnext") ); $table->headspan = array(1, 3, 1, 1); $table->attributes = array('class' => 'generaltable backup-report'); $table->data = array(); $select = ', ' . context_helper::get_preload_record_columns_sql('ctx'); $join = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; $sql = "SELECT bc.*, c.id as courseid, c.fullname $select FROM {backup_courses} bc JOIN {course} c ON c.id = bc.courseid $join"; $rs = $DB->get_recordset_sql($sql, array('contextlevel' => CONTEXT_COURSE)); foreach ($rs as $backuprow) { // Cache the course context context_helper::preload_from_record($backuprow); // Prepare a cell to display the status of the entry. if ($backuprow->laststatus == backup_cron_automated_helper::BACKUP_STATUS_OK) { $status = $strok; $statusclass = 'table-success'; // Green. } else if ($backuprow->laststatus == backup_cron_automated_helper::BACKUP_STATUS_UNFINISHED) { $status = $strunfinished; $statusclass = 'table-danger'; // Red. } else if ($backuprow->laststatus == backup_cron_automated_helper::BACKUP_STATUS_SKIPPED) { $status = $strskipped; $statusclass = 'table-success'; // Green. } else if ($backuprow->laststatus == backup_cron_automated_helper::BACKUP_STATUS_WARNING) { $status = $strwarning; $statusclass = 'table-warning'; // Orange. } else if ($backuprow->laststatus == backup_cron_automated_helper::BACKUP_STATUS_NOTYETRUN) { $status = $strnotyetrun; $statusclass = 'table-success'; } else if ($backuprow->laststatus == backup_cron_automated_helper::BACKUP_STATUS_QUEUED) { $status = $strqueued; $statusclass = 'table-success'; } else { $status = $strerror; $statusclass = 'table-danger'; // Red. } $status = new html_table_cell($status); $status->attributes = array('class' => $statusclass); // Create the row and add it to the table $backuprowname = format_string($backuprow->fullname, true, array('context' => context_course::instance($backuprow->courseid))); $backuplogsurl = new moodle_url('/report/backups/index.php', array('courseid' => $backuprow->courseid)); $backuplogsicon = new pix_icon('t/viewdetails', get_string('viewlogs', 'report_backups')); $cells = array( $backuprowname . ' ' . $OUTPUT->action_icon($backuplogsurl, $backuplogsicon), userdate($backuprow->laststarttime, $strftimedatetime), '-', userdate($backuprow->lastendtime, $strftimedatetime), $status, userdate($backuprow->nextstarttime, $strftimedatetime) ); $table->data[] = new html_table_row($cells); } $rs->close(); // Check if we have any results and if not add a no records notification if (empty($table->data)) { $cell = new html_table_cell($OUTPUT->notification(get_string('nologsfound'))); $cell->colspan = 6; $table->data[] = new html_table_row(array($cell)); } $automatedbackupsenabled = get_config('backup', 'backup_auto_active'); // Display the backup report echo $OUTPUT->header(); echo $OUTPUT->heading(get_string("backuploglaststatus")); echo $OUTPUT->box_start(); if (empty($automatedbackupsenabled)) { // Automated backups aren't active, display a notification. // Not we don't stop because of this as perhaps scheduled backups are being run // automatically, or were enabled in the page. echo $OUTPUT->notification(get_string('automatedbackupsinactive', 'backup')); } echo html_writer::table($table); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); PK{\-ƣbackups/settings.phpnu[. /** * Settings for the backups report * * @package report * @subpackage backups * @copyright 2007 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $ADMIN->add('reports', new admin_externalpage('reportbackups', get_string('backups', 'admin'), "$CFG->wwwroot/report/backups/index.php",'moodle/backup:backupcourse')); // no report settings $settings = null;PK{\[,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 /report/* - plugins, information provided here is intended especially for developers. === 4.2 === * The method report_progress\local\helper::get_activities_to_show() has an additional parameter $activitysection to support filtering by sections. === 4.0 === * The method report_helper::save_selected_report() has been been deprecated because it is no longer used. === 3.11 === * The new report plugin's can have drop down, which can be included by calling static methods save_selected_report and print_report_selector in lib/classes/report_helper.php. The save_selected_report helps to remember the most recently accessed report plugin. print_report_selector would help to show the dropdown, on the report page. Make sure to call print_report_selector after the header is printed/echoed. === 3.6 === * The final deprecation of xxx_delete_course callback means that this function will no longer be called. Please use the observer for event \core\event\course_content_deleted instead. === 3.2 === * Callback delete_course is deprecated and should be replaced with observer for event \core\event\course_content_deleted * The report_log_print_graph signature and behaviour has changed to generate charts using the new Chart API. The third argument has been renamed to $typeormode and it accepts the type (usercourse.png, userday.png) or the mode (today, all). === 2.7 === * How to migrate reports accessing table 'log': http://docs.moodle.org/dev/Migrating_log_access_in_reports * All reports that use logstores must implement a callback report_reportname_supports_logstore($storeinstance) in lib.php of the report. Refer MDL-44596 for details. === 2.2 === API changes: * reports were moved from /admin/report/ to /report/ * new support for report settings How to migrate existing admin reports: # move all files to new /report/yourplugin/ location # if settings.php exists add $settings=null; # if settings.php does not exist create it and link the report, index.php is not linked automatically any more # update require('../../config.php'); - remove one ../ # update all others includes and requires # update all links to report pages by removing /admin/ or /$CFG->admin/ # add language pack with at least 'pluginname' string # update CSS selectors How to migrate existing course reports (optional): # move all files to new /report/yourplugin/ location # update require('../../config.php'); - remove one ../ # update all others includes and requires # update all links to report pages by removing /course/ part # update all language strings (use 'report_yourplugin' instead of 'coursereport_yourplugin') - use AMOS hints in commit message # update all capability names # grep the plugin codebase and look for any remaining 'coursereport' occurrences # add new navigation hooks in lib.php - ex: report_stats_extend_navigation_course(), report_stats_extend_navigation_user() # add new page types in lib.php # create db/install.php migration script - delete old settings and capabilities (see converted plugins for examples) # update CSS selectors See http://docs.moodle.org/dev/General_report_plugins for more details and explanation. PK{\wusersessions/db/access.phpnu[. /** * Capabilities for this report. * * @package report_usersessions * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Petr Skoda */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'report/usersessions:manageownsessions' => array( 'captype' => 'write', 'contextlevel' => CONTEXT_USER, 'archetypes' => array( 'user' => CAP_ALLOW, ), // NOTE: shared accounts usually do not allow changing // of own passwords, this is not very accurate but safer. 'clonepermissionsfrom' => 'moodle/user:changeownpassword' ), ); PK{\ ,usersessions/lang/en/report_usersessions.phpnu[. /** * Lang strings. * * @package report_usersessions * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Petr Skoda */ $string['logoutothersessions'] = 'Log out all other browser sessions'; $string['logoutothersessions_help'] = 'Log out of all browser sessions, except for this one. This does not affect web apps.'; $string['logoutothersessionssuccess'] = 'You are logged out of all your other sessions.'; $string['logoutsinglesessionsuccess'] = 'You are logged out of the session at {$a}.'; $string['mysessions'] = 'My active browser sessions'; $string['navigationlink'] = 'Browser sessions'; $string['pluginname'] = 'User sessions report'; $string['thissession'] = 'Current session'; $string['usersessions:manageownsessions'] = 'Manage own browser sessions'; $string['privacy:metadata'] = 'The User sessions report plugin does not store any personal data.'; PK{\>  usersessions/lib.phpnu[. /** * Lib API functions. * * @package report_usersessions * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Petr Skoda */ defined('MOODLE_INTERNAL') || die; /** * This function extends the course navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $user * @param stdClass $course The course to object for the report */ function report_usersessions_extend_navigation_user($navigation, $user, $course) { global $USER; if (isguestuser() or !isloggedin()) { return; } if (\core\session\manager::is_loggedinas() or $USER->id != $user->id) { // No peeking at somebody else's sessions! return; } $context = context_user::instance($USER->id); if (has_capability('report/usersessions:manageownsessions', $context)) { $navigation->add(get_string('navigationlink', 'report_usersessions'), new moodle_url('/report/usersessions/user.php'), $navigation::TYPE_SETTING); } } /** * Add nodes to myprofile page. * * @param \core_user\output\myprofile\tree $tree Tree object * @param stdClass $user user object * @param bool $iscurrentuser * @param stdClass $course Course object * * @return bool */ function report_usersessions_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) { global $USER; if (isguestuser() or !isloggedin()) { return; } if (\core\session\manager::is_loggedinas() or $USER->id != $user->id) { // No peeking at somebody else's sessions! return; } $context = context_user::instance($USER->id); if (has_capability('report/usersessions:manageownsessions', $context)) { $node = new core_user\output\myprofile\node('reports', 'usersessions', get_string('navigationlink', 'report_usersessions'), null, new moodle_url('/report/usersessions/user.php')); $tree->add_node($node); } return true; } PK{\lKRusersessions/version.phpnu[. /** * Version info. * * @package report_usersessions * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Petr Skoda */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_usersessions'; // Full name of the plugin (used for diagnostics). PK{\~I)usersessions/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_usersessions. * * @package report_usersessions * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_usersessions\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_usersessions implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\= usersessions/locallib.phpnu[. /** * Lib API functions. * * @package report_usersessions * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Petr Skoda */ defined('MOODLE_INTERNAL') || die; require_once(__DIR__ . '/lib.php'); /** * Show user friendly duration since last activity. * * @param int $duration in seconds * @return string */ function report_usersessions_format_duration($duration) { // NOTE: The session duration is not accurate thanks to // $CFG->session_update_timemodified_frequency setting. // Also there is no point in showing days here because // the session cleanup should purge all stale sessions // regularly. if ($duration < 60) { return get_string('now'); } if ($duration < 60 * 60 * 2) { $minutes = (int)($duration / 60); $ago = $minutes . ' ' . get_string('minutes'); return get_string('ago', 'core_message', $ago); } $hours = (int)($duration / (60 * 60)); $ago = $hours . ' ' . get_string('hours'); return get_string('ago', 'core_message', $ago); } /** * Show some user friendly IP address info. * * @param string $ip * @return string */ function report_usersessions_format_ip($ip) { if (strpos($ip, ':') !== false) { // For now ipv6 is not supported yet. return $ip; } $url = new moodle_url('/iplookup/index.php', array('ip' => $ip)); return html_writer::link($url, $ip); } /** * Kill user session. * * @param int $id * @return void */ function report_usersessions_kill_session(int $id): void { global $USER; $sessions = \core\session\manager::get_sessions_by_userid($USER->id); $filteredsessions = array_filter($sessions, fn ($session) => $session->id === $id); foreach ($filteredsessions as $session) { if ($session->sid !== session_id()) { \core\session\manager::destroy($session->sid); } } } PK{\3B3??usersessions/index.phpnu[. /** * Listing of all sessions for current user. * * @package report_usersessions * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Petr Skoda */ require(__DIR__ . '/../../config.php'); redirect(new moodle_url('/report/usersessions/user.php')); PK{\h{*usersessions/tests/lib_test.phpnu[. /** * Tests for report library functions. * * @package report_usersessions * @copyright 2015 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace report_usersessions; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->dirroot. '/report/usersessions/lib.php'); /** * Class report_stats_lib_testcase * * @package report_usersessions * @copyright 2014 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class lib_test extends \advanced_testcase { /** * @var stdClass The user. */ private $user; /** * @var stdClass The course. */ private $course; /** * @var \core_user\output\myprofile\tree The navigation tree. */ private $tree; public function setUp(): void { parent::setUp(); $this->user = $this->getDataGenerator()->create_user(); $this->course = $this->getDataGenerator()->create_course(); $this->tree = new \core_user\output\myprofile\tree(); $this->resetAfterTest(); } /** * Tests the report_userssesions_myprofile_navigation() function as an admin. */ public function test_report_usersessions_myprofile_navigation_as_admin(): void { $this->setAdminUser(); $iscurrentuser = false; // Not even admins allowed to pick at other user's sessions. report_usersessions_myprofile_navigation($this->tree, $this->user, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayNotHasKey('usersessions', $nodes->getValue($this->tree)); } /** * Tests the report_userssesions_myprofile_navigation() function as the currently logged in user. */ public function test_report_usersessions_myprofile_navigation_as_current_user(): void { $this->setUser($this->user); $iscurrentuser = true; report_usersessions_myprofile_navigation($this->tree, $this->user, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayHasKey('usersessions', $nodes->getValue($this->tree)); } /** * Tests the report_userssesions_myprofile_navigation() function as a guest. */ public function test_report_usersessions_myprofile_navigation_as_guest(): void { $this->setGuestUser(); $iscurrentuser = true; report_usersessions_myprofile_navigation($this->tree, $this->user, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayNotHasKey('usersessions', $nodes->getValue($this->tree)); } /** * Tests the report_userssesions_myprofile_navigation() function as a user without permission. */ public function test_report_usersessions_myprofile_navigation_without_permission(): void { // Try to see as a user without permission. $user2 = $this->getDataGenerator()->create_user(); $this->setUser($user2); $iscurrentuser = false; report_usersessions_myprofile_navigation($this->tree, $this->user, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayNotHasKey('usersessions', $nodes->getValue($this->tree)); } } PK{\R4usersessions/tests/behat/usersessions_report.featurenu[@report @report_usersessions Feature: In a report, admin can see current sessions In order see usersession data As a admin I need to view usersessions report and see if the current session is listed @javascript Scenario: Check usersessions report shows current session Given I log in as "admin" And I follow "Profile" in the user menu When I follow "Browser sessions" Then I should see "Current session" PK{\??usersessions/user.phpnu[. /** * Listing of all sessions for current user. * * @package report_usersessions * @copyright 2014 Totara Learning Solutions Ltd {@link http://www.totaralms.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @author Petr Skoda */ require(__DIR__ . '/../../config.php'); require_once(__DIR__ . '/locallib.php'); require_login(null, false); if (isguestuser()) { // No guests here! redirect(new moodle_url('/')); die; } if (\core\session\manager::is_loggedinas()) { // No login-as users. redirect(new moodle_url('/user/index.php')); die; } $context = context_user::instance($USER->id); require_capability('report/usersessions:manageownsessions', $context); $delete = optional_param('delete', 0, PARAM_INT); $deleteall = optional_param('deleteall', false, PARAM_BOOL); $lastip = cleanremoteaddr(optional_param('lastip', '', PARAM_TEXT)); $PAGE->set_url('/report/usersessions/user.php'); $PAGE->set_context($context); $PAGE->set_title(get_string('navigationlink', 'report_usersessions')); $PAGE->set_heading(fullname($USER)); $PAGE->set_pagelayout('admin'); // Delete a specific session. if ($delete && confirm_sesskey()) { report_usersessions_kill_session($delete); redirect( url: $PAGE->url, message: get_string('logoutsinglesessionsuccess', 'report_usersessions', $lastip), messagetype: \core\output\notification::NOTIFY_SUCCESS, ); } // Delete all sessions except current. if ($deleteall && confirm_sesskey()) { \core\session\manager::destroy_user_sessions($USER->id, session_id()); redirect( url: $PAGE->url, message: get_string('logoutothersessionssuccess', 'report_usersessions'), messagetype: \core\output\notification::NOTIFY_SUCCESS, ); } // Create the breadcrumb. $PAGE->add_report_nodes($USER->id, array( 'name' => get_string('navigationlink', 'report_usersessions'), 'url' => new moodle_url('/report/usersessions/user.php') )); echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('mysessions', 'report_usersessions')); $data = array(); $sessions = \core\session\manager::get_sessions_by_userid($USER->id); // Order records by timemodified DESC. usort($sessions, function($a, $b){ return $b->timemodified <=> $a->timemodified; }); foreach ($sessions as $session) { if ($session->sid === session_id()) { $lastaccess = get_string('thissession', 'report_usersessions'); $deletelink = ''; } else { $lastaccess = report_usersessions_format_duration(time() - $session->timemodified); $url = new moodle_url($PAGE->url, ['delete' => $session->id, 'sesskey' => sesskey(), 'lastip' => $session->lastip]); $deletelink = html_writer::link($url, get_string('logout')); } $data[] = array(userdate($session->timecreated), $lastaccess, report_usersessions_format_ip($session->lastip), $deletelink); } $table = new html_table(); $table->head = array(get_string('login'), get_string('lastaccess'), get_string('lastip'), get_string('action')); $table->align = array('left', 'left', 'left', 'right'); $table->data = $data; echo html_writer::table($table); // Provide button to log out all other sessions. if (count($sessions) > 1) { $url = new moodle_url($PAGE->url, ['deleteall' => true]); echo $OUTPUT->single_button($url, get_string('logoutothersessions', 'report_usersessions')); } echo $OUTPUT->footer(); PK{\@Ҕstatus/db/access.phpnu[. /** * System status capabilities * * @package report_status * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = [ 'report/status:view' => [ 'riskbitmask' => RISK_CONFIG, 'captype' => 'read', 'contextlevel' => CONTEXT_SYSTEM, 'archetypes' => [ 'manager' => CAP_ALLOW ], ] ]; PK{\8V-<< status/lang/en/report_status.phpnu[. /** * Strings for component 'tool_task', language 'en' * * @package report_status * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'System status'; $string['status:view'] = 'View system status'; $string['privacy:metadata'] = 'This plugin does not store any personal data.'; PK{\status/version.phpnu[. /** * Plugin version info * * @package report_status * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_status'; // Full name of the plugin (used for diagnostics). PK{\̌#status/classes/privacy/provider.phpnu[. /** * Privacy provider. * * @package report_status * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_status\privacy; defined('MOODLE_INTERNAL') || die; /** * Privacy provider. * * @package report_status * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK{\6status/index.phpnu[. /** * System Status report * * @package report_status * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('NO_OUTPUT_BUFFERING', true); require('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); admin_externalpage_setup('reportstatus', '', null, '', ['pagelayout' => 'report']); $detail = optional_param('detail', '', PARAM_TEXT); // Show detailed info about one check only. $url = '/report/status/index.php'; $table = new core\check\table('status', $url, $detail); if (!empty($table->detail)) { $PAGE->set_docs_path($url . '?detail=' . $detail); $PAGE->navbar->add($table->detail->get_name()); } echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('pluginname', 'report_status')); echo $table->render($OUTPUT); echo $OUTPUT->footer(); PK{\;P__status/settings.phpnu[. /** * Plugin version info * * @package report_status * @copyright 2020 Brendan Heywood (brendan@catalyst-au.net) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $ADMIN->add('reports', new admin_externalpage('reportstatus', get_string('pluginname', 'report_status'), "$CFG->wwwroot/report/status/index.php", 'report/status:view')); $settings = null; PK{\[nparticipation/styles.cssnu[#page-report-participation-index .participationselectform { margin: 10px auto; } #page-report-participation-index .participationselectform label { margin-left: 15px; margin-right: 5px; } /* Die to css conflicts with form-inline, we have to create a specific class to fix submit button alignment on clean */ #page-report-participation-index .participationselectform input[type="submit"] { margin-bottom: 0; } PK{\1participation/db/install.phpnu[. /** * Post installation and migration code. * * @package report * @subpackage participation * @copyright 2011 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; function xmldb_report_participation_install() { global $DB; } PK{\7R\FFparticipation/db/access.phpnu[. /** * Capabilities * * @package report_participation * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'report/participation:view' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'coursereport/participation:view', ) ); PK{\dpp.participation/lang/en/report_participation.phpnu[. /** * Lang strings * * @package report * @subpackage participation * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['eventreportviewed'] = 'Participation report viewed'; $string['nologreaderenabled'] = 'No log reader enabled'; $string['participation:view'] = 'View course participation report'; $string['page-report-participation-x'] = 'Any participation report'; $string['page-report-participation-index'] = 'Course participation report'; $string['pluginname'] = 'Course participation'; $string['privacy:metadata'] = 'The Course participation plugin does not store any personal data.'; PK{\[ participation/lib.phpnu[. /** * This file contains functions used by the participation report * * @package report * @subpackage participation * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * This function extends the navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $course The course to object for the report * @param stdClass $context The context of the course */ function report_participation_extend_navigation_course($navigation, $course, $context) { global $CFG, $OUTPUT; if (has_capability('report/participation:view', $context)) { $url = new moodle_url('/report/participation/index.php', array('id'=>$course->id)); $navigation->add(get_string('pluginname', 'report_participation'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/report', '')); } } /** * Return a list of page types * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block * @return array */ function report_participation_page_type_list($pagetype, $parentcontext, $currentcontext) { $array = array( '*' => get_string('page-x', 'pagetype'), 'report-*' => get_string('page-report-x', 'pagetype'), 'report-participation-*' => get_string('page-report-participation-x', 'report_participation'), 'report-participation-index' => get_string('page-report-participation-index', 'report_participation'), ); return $array; } /** * Callback to verify if the given instance of store is supported by this report or not. * * @param string $instance store instance. * * @return bool returns true if the store is supported by the report, false otherwise. */ function report_participation_supports_logstore($instance) { if ($instance instanceof \core\log\sql_internal_table_reader) { return true; } return false; } PK{\gYparticipation/version.phpnu[. /** * Version info * * @package report * @subpackage participation * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_participation'; // Full name of the plugin (used for diagnostics) PK{\G**participation/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_participation. * * @package report_participation * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_participation\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_participation implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\"0rr-participation/classes/event/report_viewed.phpnu[. /** * The report_participation report viewed event. * * @package report_participation * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_participation\event; defined('MOODLE_INTERNAL') || die(); /** * The report_participation report viewed event class. * * @property-read array $other { * Extra information about the event. * * - int instanceid: Id of instance. * - int roleid: Role id for whom report is viewed. * - int groupid: (optional) group id. * - int timefrom: (optional) time from which report is viewed. * - string action: (optional) action viewed. * } * * @package report_participation * @since Moodle 2.7 * @copyright 2013 Ankit Agarwal * @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() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventreportviewed', 'report_participation'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the course participation report for the course with id '$this->courseid'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/participation/index.php', array('id' => $this->courseid, 'instanceid' => $this->other['instanceid'], 'roleid' => $this->other['roleid'])); } /** * 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.'); } if (empty($this->other['roleid'])) { throw new \coding_exception('The \'roleid\' value must be set in other.'); } if (!isset($this->other['groupid'])) { throw new \coding_exception('The \'groupid\' value must be set in other.'); } if (!isset($this->other['timefrom'])) { throw new \coding_exception('The \'timefrom\' value must be set in other.'); } if (!isset($this->other['action'])) { throw new \coding_exception('The \'action\' value must be set in other.'); } } public static function get_other_mapping() { $othermapped = array(); // This is a badly named variable - it represents "cm->id" not "cm->instance". $othermapped['instanceid'] = array('db' => 'course_modules', 'restore' => 'course_module'); $othermapped['roleid'] = array('db' => 'role', 'restore' => 'role'); $othermapped['groupid'] = array('db' => 'groups', 'restore' => 'group'); return $othermapped; } } PK{\ظ participation/locallib.phpnu[. /** * This file contains functions used by the participation reports * * @package report_participation * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Returns log table name of preferred reader, if leagcy then return empty string. * * @return string table name */ function report_participation_get_log_table_name() { // Get prefered sql_internal_table_reader reader (if enabled). $logmanager = get_log_manager(); $readers = $logmanager->get_readers(); $logtable = ''; // Get preferred reader. if (!empty($readers)) { foreach ($readers as $readerpluginname => $reader) { // If sql_internal_table_reader is preferred reader. if ($reader instanceof \core\log\sql_internal_table_reader) { $logtable = $reader->get_internal_log_table_name(); break; } } } return $logtable; } /** * Return time options, which should be shown for record filtering. * * @param int $minlog Time of first log record available. * @return array time options. */ function report_participation_get_time_options($minlog) { $timeoptions = array(); $now = usergetmidnight(time()); // Days. for ($i = 1; $i < 7; $i++) { if (strtotime('-'.$i.' days',$now) >= $minlog) { $timeoptions[strtotime('-'.$i.' days',$now)] = get_string('numdays','moodle',$i); } } // Weeks. for ($i = 1; $i < 10; $i++) { if (strtotime('-'.$i.' weeks',$now) >= $minlog) { $timeoptions[strtotime('-'.$i.' weeks',$now)] = get_string('numweeks','moodle',$i); } } // Months. for ($i = 2; $i < 12; $i++) { if (strtotime('-'.$i.' months',$now) >= $minlog) { $timeoptions[strtotime('-'.$i.' months',$now)] = get_string('nummonths','moodle',$i); } } // Try a year. if (strtotime('-1 year',$now) >= $minlog) { $timeoptions[strtotime('-1 year',$now)] = get_string('lastyear'); } return $timeoptions; } /** * Return action sql and params. * * @param string $action action to be filtered. * @param string $modname module name. * @return array actionsql and actionparams. */ function report_participation_get_action_sql($action, $modname) { global $CFG, $DB; $actionsql = ''; $actionparams = array(); $viewnames = array(); $postnames = array(); include_once($CFG->dirroot.'/mod/' . $modname . '/lib.php'); $viewfun = $modname.'_get_view_actions'; $postfun = $modname.'_get_post_actions'; if (function_exists($viewfun)) { $viewnames = $viewfun(); } if (function_exists($postfun)) { $postnames = $postfun(); } switch ($action) { case 'view': $actions = $viewnames; break; case 'post': $actions = $postnames; break; default: // Some modules have stuff we want to hide, ie mail blocked etc so do actually need to limit here. $actions = array_merge($viewnames, $postnames); } if (!empty($actions)) { list($actionsql, $actionparams) = $DB->get_in_or_equal($actions, SQL_PARAMS_NAMED, 'action'); $actionsql = " AND action $actionsql"; } return array($actionsql, $actionparams); } /** * Return crud sql and params. * * @param string $action action to be filtered. * @return array crudsql and crudparams. */ function report_participation_get_crud_sql($action) { global $DB; switch ($action) { case 'view': $crud = 'r'; break; case 'post': $crud = array('c', 'u', 'd'); break; default: $crud = array('c', 'r', 'u', 'd'); } list($crudsql, $crudparams) = $DB->get_in_or_equal($crud, SQL_PARAMS_NAMED, 'crud'); $crudsql = " AND crud " . $crudsql; return array($crudsql, $crudparams); } /** * List of action filters. * * @return array */ function report_participation_get_action_options() { return array('' => get_string('allactions'), 'view' => get_string('view'), 'post' => get_string('post'),); } /** * Print filter form. * * @param stdClass $course course object. * @param int $timefrom Time from which records should be fetched. * @param int $minlog Time of first record present in log store. * @param string $action action to be filtered. * @param int $roleid Role to be filtered. * @param int $instanceid Instance id of module. */ function report_participation_print_filter_form($course, $timefrom, $minlog, $action, $roleid, $instanceid) { global $DB; $timeoptions = report_participation_get_time_options($minlog); $actionoptions = report_participation_get_action_options(); $context = context_course::instance($course->id); $roles = get_roles_used_in_context($context); $rolesviewable = get_viewable_roles($context); $guestrole = get_guest_role(); $roleoptions = array_intersect_key($rolesviewable, $roles) + [ $guestrole->id => role_get_name($guestrole, $context), ]; $modinfo = get_fast_modinfo($course); $modules = $DB->get_records_select('modules', "visible = 1", null, 'name ASC'); $instanceoptions = array(); foreach ($modules as $module) { if (empty($modinfo->instances[$module->name])) { continue; } $instances = array(); foreach ($modinfo->instances[$module->name] as $cm) { // Skip modules such as label which do not actually have links; // this means there's nothing to participate in. if (!$cm->has_view()) { continue; } $instances[$cm->id] = format_string($cm->name); } if (count($instances) == 0) { continue; } $instanceoptions[] = array(get_string('modulenameplural', $module->name)=>$instances); } echo '
'."\n". ''."\n"; echo ''."\n"; echo html_writer::select($instanceoptions, 'instanceid', $instanceid); echo ''."\n"; echo html_writer::select($timeoptions,'timefrom',$timefrom); echo ''."\n"; echo html_writer::select($roleoptions,'roleid',$roleid,false); echo ''."\n"; echo html_writer::select($actionoptions, 'action', $action, false, ['class' => 'me-1']); echo ''."\n
\n"; } PK{\d?d?participation/index.phpnu[. /** * Participation report * * @package report * @subpackage participation * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\report_helper; require('../../config.php'); require_once($CFG->dirroot.'/lib/tablelib.php'); require_once($CFG->dirroot.'/notes/lib.php'); require_once($CFG->dirroot.'/report/participation/locallib.php'); $participantsperpage = intval(get_config('moodlecourse', 'participantsperpage')); define('DEFAULT_PAGE_SIZE', (!empty($participantsperpage) ? $participantsperpage : 20)); define('SHOW_ALL_PAGE_SIZE', 5000); $id = required_param('id', PARAM_INT); // course id. $roleid = optional_param('roleid', 0, PARAM_INT); // which role to show $instanceid = optional_param('instanceid', 0, PARAM_INT); // instance we're looking at. $timefrom = optional_param('timefrom', 0, PARAM_INT); // how far back to look... $action = optional_param('action', '', PARAM_ALPHA); $page = optional_param('page', 0, PARAM_INT); // which page to show $perpage = optional_param('perpage', DEFAULT_PAGE_SIZE, PARAM_INT); // how many per page $currentgroup = optional_param('group', null, PARAM_INT); // Get the active group. $url = new moodle_url('/report/participation/index.php', array('id'=>$id)); if ($roleid !== 0) $url->param('roleid'); if ($instanceid !== 0) $url->param('instanceid'); if ($timefrom !== 0) $url->param('timefrom'); if ($action !== '') $url->param('action'); if ($page !== 0) $url->param('page'); if ($perpage !== DEFAULT_PAGE_SIZE) $url->param('perpage'); $PAGE->set_url($url); $PAGE->set_pagelayout('admin'); if ($action != 'view' and $action != 'post') { $action = ''; // default to all (don't restrict) } if (!$course = $DB->get_record('course', array('id'=>$id))) { throw new \moodle_exception('invalidcourse'); } if ($roleid != 0 and !$role = $DB->get_record('role', array('id'=>$roleid))) { throw new \moodle_exception('invalidrole'); } require_login($course); $context = context_course::instance($course->id); require_capability('report/participation:view', $context); $strparticipation = get_string('participationreport'); $strviews = get_string('views'); $strposts = get_string('posts'); $strreports = get_string('reports'); $actionoptions = report_participation_get_action_options(); if (!array_key_exists($action, $actionoptions)) { $action = ''; } $PAGE->set_title(format_string($course->shortname, true, array('context' => $context)) .': '. $strparticipation); $PAGE->set_heading(format_string($course->fullname, true, array('context' => $context))); echo $OUTPUT->header(); // Print the selector dropdown. $pluginname = get_string('pluginname', 'report_participation'); report_helper::print_report_selector($pluginname); // Logs will not have been recorded before the course timecreated time. $minlog = $course->timecreated; $onlyuselegacyreader = false; // Use only legacy log table to aggregate records. $logtable = report_participation_get_log_table_name(); // Log table to use for fetaching records. // If no log table, then use legacy records. if (empty($logtable)) { $onlyuselegacyreader = true; } $modinfo = get_fast_modinfo($course); // Print first controls. report_participation_print_filter_form($course, $timefrom, $minlog, $action, $roleid, $instanceid); $baseurl = new moodle_url('/report/participation/index.php', array( 'id' => $course->id, 'roleid' => $roleid, 'instanceid' => $instanceid, 'timefrom' => $timefrom, 'action' => $action, 'perpage' => $perpage, 'group' => $currentgroup )); $select = groups_allgroups_course_menu($course, $baseurl, true, $currentgroup); // User cannot see any group. if (empty($select)) { echo $OUTPUT->heading(get_string("notingroup")); echo $OUTPUT->footer(); exit; } else { echo $select; } // Fetch current active group. $groupmode = groups_get_course_groupmode($course); $currentgroup = $SESSION->activegroup[$course->id][$groupmode][$course->defaultgroupingid]; if (!empty($instanceid) && !empty($roleid)) { $uselegacyreader = $DB->record_exists('log', ['course' => $course->id]); // Trigger a report viewed event. $event = \report_participation\event\report_viewed::create(array('context' => $context, 'other' => array('instanceid' => $instanceid, 'groupid' => $currentgroup, 'roleid' => $roleid, 'timefrom' => $timefrom, 'action' => $action))); $event->trigger(); // from here assume we have at least the module we're using. $cm = $modinfo->cms[$instanceid]; // Group security checks. if (!groups_group_visible($currentgroup, $course, $cm)) { echo $OUTPUT->heading(get_string("notingroup")); echo $OUTPUT->footer(); exit; } $table = new flexible_table('course-participation-'.$course->id.'-'.$cm->id.'-'.$roleid); $actionheader = !empty($action) ? get_string($action) : get_string('allactions'); if (empty($CFG->messaging)) { $table->define_columns(array('fullname', 'count')); $table->define_headers(array(get_string('user'), $actionheader)); } else { $table->define_columns(array('fullname', 'count', 'select')); $mastercheckbox = new \core\output\checkbox_toggleall('participants-table', true, [ 'id' => 'select-all-participants', 'name' => 'select-all-participants', 'label' => get_string('select'), // Consistent labels to prevent select column from resizing. 'selectall' => get_string('select'), 'deselectall' => get_string('select'), ]); $table->define_headers(array(get_string('user'), $actionheader, $OUTPUT->render($mastercheckbox))); } $table->define_baseurl($baseurl); $table->set_attribute('class', 'generaltable generalbox reporttable'); $table->sortable(true,'lastname','ASC'); $table->no_sorting('select'); $table->set_control_variables(array( TABLE_VAR_SORT => 'ssort', TABLE_VAR_HIDE => 'shide', TABLE_VAR_SHOW => 'sshow', TABLE_VAR_IFIRST => 'sifirst', TABLE_VAR_ILAST => 'silast', TABLE_VAR_PAGE => 'spage' )); $table->setup(); // Unlock the session only after outputting the table, since the table writes to the session. \core\session\manager::write_close(); // We want to query both the current context and parent contexts. list($relatedctxsql, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'relatedctx'); $params['roleid'] = $roleid; $params['instanceid'] = $instanceid; $params['timefrom'] = $timefrom; $groupsql = ""; if (!empty($currentgroup)) { $groupsql = "JOIN {groups_members} gm ON (gm.userid = u.id AND gm.groupid = :groupid)"; $params['groupid'] = $currentgroup; } $countsql = "SELECT COUNT(DISTINCT(ra.userid)) FROM {role_assignments} ra JOIN {user} u ON u.id = ra.userid $groupsql WHERE ra.contextid $relatedctxsql AND ra.roleid = :roleid"; $totalcount = $DB->count_records_sql($countsql, $params); list($twhere, $tparams) = $table->get_sql_where(); if ($twhere) { $params = array_merge($params, $tparams); $matchcount = $DB->count_records_sql($countsql.' AND '.$twhere, $params); } else { $matchcount = $totalcount; } $modulename = get_string('modulename', $cm->modname); echo '
' . "\n"; echo '

' . $modulename . ' ' . $strviews . '
'."\n" . $modulename . ' ' . $strposts . '

'."\n"; $table->initialbars($totalcount > $perpage); $table->pagesize($perpage, $matchcount); if ($uselegacyreader || $onlyuselegacyreader) { list($actionsql, $actionparams) = report_participation_get_action_sql($action, $cm->modname); $params = array_merge($params, $actionparams); } if (!$onlyuselegacyreader) { list($crudsql, $crudparams) = report_participation_get_crud_sql($action); $params = array_merge($params, $crudparams); } $userfieldsapi = \core_user\fields::for_name(); $usernamefields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $users = array(); // If using legacy log then get users from old table. if ($uselegacyreader || $onlyuselegacyreader) { $sql = "SELECT ra.userid, $usernamefields, u.idnumber, l.actioncount AS count FROM (SELECT DISTINCT userid FROM {role_assignments} WHERE contextid $relatedctxsql AND roleid = :roleid ) ra JOIN {user} u ON u.id = ra.userid $groupsql LEFT JOIN ( SELECT userid, COUNT(action) AS actioncount FROM {log} WHERE cmid = :instanceid AND time > :timefrom " . $actionsql . " GROUP BY userid) l ON (l.userid = ra.userid)"; if ($twhere) { $sql .= ' WHERE '.$twhere; // Initial bar. } if ($table->get_sql_sort()) { $sql .= ' ORDER BY '.$table->get_sql_sort(); } if (!$users = $DB->get_records_sql($sql, $params, $table->get_page_start(), $table->get_page_size())) { $users = array(); // Tablelib will handle saying 'Nothing to display' for us. } } // Get record from sql_internal_table_reader and merge with records got from legacy log (if needed). if (!$onlyuselegacyreader) { $anonymoussql = !has_capability('moodle/site:viewanonymousevents', $context) ? 'AND l.anonymous = 0' : ''; $sql = "SELECT ra.userid, $usernamefields, u.idnumber, COUNT(DISTINCT l.timecreated) AS count FROM {user} u JOIN {role_assignments} ra ON u.id = ra.userid AND ra.contextid $relatedctxsql AND ra.roleid = :roleid $groupsql LEFT JOIN {" . $logtable . "} l ON l.contextinstanceid = :instanceid AND l.timecreated > :timefrom" . $crudsql ." AND l.edulevel = :edulevel " . $anonymoussql . " AND l.contextlevel = :contextlevel AND (l.origin = 'web' OR l.origin = 'ws') AND l.userid = ra.userid"; // We add this after the WHERE statement that may come below. $groupbysql = " GROUP BY ra.userid, $usernamefields, u.idnumber"; $params['edulevel'] = core\event\base::LEVEL_PARTICIPATING; $params['contextlevel'] = CONTEXT_MODULE; if ($twhere) { $sql .= ' WHERE '.$twhere; // Initial bar. } $sql .= $groupbysql; if ($table->get_sql_sort()) { $sql .= ' ORDER BY '.$table->get_sql_sort(); } if ($u = $DB->get_records_sql($sql, $params, $table->get_page_start(), $table->get_page_size())) { if (empty($users)) { $users = $u; } else { // Merge two users array. foreach ($u as $key => $value) { if (isset($users[$key]) && !empty($users[$key]->count)) { if ($value->count) { $users[$key]->count += $value->count; } } else { $users[$key] = $value; } } } unset($u); $u = null; } } $data = array(); $a = new stdClass(); $a->count = $totalcount; $a->items = format_string($role->name, true, array('context' => $context)); if ($matchcount != $totalcount) { $a->count = $matchcount.'/'.$a->count; } echo '

'.get_string('counteditems', '', $a).'

'."\n"; if (!empty($CFG->messaging)) { echo '
'."\n"; echo '
'."\n"; echo ''."\n"; echo ''."\n"; echo ''."\n"; } foreach ($users as $u) { $data = array(); $data[] = html_writer::link(new moodle_url('/user/view.php', array('id' => $u->userid, 'course' => $course->id)), fullname($u, true)); $data[] = !empty($u->count) ? get_string('yes').' ('.$u->count.') ' : get_string('no'); if (!empty($CFG->messaging)) { $togglegroup = 'participants-table'; if (empty($u->count)) { $togglegroup .= ' no'; } $checkbox = new \core\output\checkbox_toggleall($togglegroup, false, [ 'classes' => 'usercheckbox', 'name' => 'user' . $u->userid, 'value' => $u->count, ]); $data[] = $OUTPUT->render($checkbox); } $table->add_data($data); } $table->print_html(); if ($perpage == SHOW_ALL_PAGE_SIZE) { $perpageurl = new moodle_url($baseurl, array('perpage' => DEFAULT_PAGE_SIZE)); echo html_writer::start_div('', array('id' => 'showall')); echo html_writer::link($perpageurl, get_string('showperpage', '', DEFAULT_PAGE_SIZE)); echo html_writer::end_div(); } else if ($matchcount > 0 && $perpage < $matchcount) { $perpageurl = new moodle_url($baseurl, array('perpage' => SHOW_ALL_PAGE_SIZE)); echo html_writer::start_div('', array('id' => 'showall')); echo html_writer::link($perpageurl, get_string('showall', '', $matchcount)); echo html_writer::end_div(); } if (!empty($CFG->messaging)) { echo '
'; if ($perpage >= $matchcount) { $checknos = new \core\output\checkbox_toggleall('participants-table no', true, [ 'id' => 'select-nos', 'name' => 'select-nos', 'label' => get_string('selectnos'), 'selectall' => get_string('selectnos'), 'deselectall' => get_string('deselectnos'), ], true); echo $OUTPUT->render($checknos); } echo '
'; echo '
'; echo html_writer::label(get_string('withselectedusers'), 'formactionid'); $displaylist['#messageselect'] = get_string('messageselectadd'); $withselectedparams = array( 'id' => 'formactionid', 'data-action' => 'toggle', 'data-togglegroup' => 'participants-table', 'data-toggle' => 'action', 'disabled' => true ); echo html_writer::select($displaylist, 'formaction', '', array('' => 'choosedots'), $withselectedparams); echo '
'; echo '
'."\n"; echo '
'."\n"; $PAGE->requires->js_call_amd('report_participation/participants', 'init'); } echo '
'."\n"; } echo $OUTPUT->footer(); PK{\׾ %participation/amd/src/participants.jsnu[// 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 . /** * Some UI stuff for participants page. * This is also used by the report/participants/index.php because it has the same functionality. * * @module report_participation/participants * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import jQuery from 'jquery'; import CustomEvents from 'core/custom_interaction_events'; import ModalEvents from 'core/modal_events'; import Notification from 'core/notification'; import {showSendMessage} from 'core_user/local/participants/bulkactions'; const Selectors = { bulkActionSelect: "#formactionid", bulkUserSelectedCheckBoxes: "input[data-togglegroup^='participants-table'][data-toggle='slave']:checked", participantsForm: '#participantsform', }; export const init = () => { const root = document.querySelector(Selectors.participantsForm); /** * Private method. * * @method registerEventListeners * @private */ const registerEventListeners = () => { CustomEvents.define(Selectors.bulkActionSelect, [CustomEvents.events.accessibleChange]); jQuery(Selectors.bulkActionSelect).on(CustomEvents.events.accessibleChange, e => { const action = e.target.value; const checkboxes = root.querySelectorAll(Selectors.bulkUserSelectedCheckBoxes); if (action.indexOf('#') !== -1) { e.preventDefault(); const ids = []; checkboxes.forEach(checkbox => { ids.push(checkbox.getAttribute('name').replace('user', '')); }); if (action === '#messageselect') { showSendMessage(ids) .then(modal => { modal.getRoot().on(ModalEvents.hidden, () => { // Focus on the action select when the dialog is closed. const bulkActionSelector = root.querySelector(Selectors.bulkActionSelect); resetBulkAction(bulkActionSelector); bulkActionSelector.focus(); }); return modal; }) .catch(Notification.exception); } } else if (action !== '' && checkboxes.length) { e.target.form().submit(); } resetBulkAction(e.target); }); }; const resetBulkAction = bulkActionSelect => { bulkActionSelect.value = ''; }; registerEventListeners(); }; PK{\oڤ+participation/amd/build/participants.min.jsnu[define("report_participation/participants",["exports","jquery","core/custom_interaction_events","core/modal_events","core/notification","core_user/local/participants/bulkactions"],(function(_exports,_jquery,_custom_interaction_events,_modal_events,_notification,_bulkactions){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Some UI stuff for participants page. * This is also used by the report/participants/index.php because it has the same functionality. * * @module report_participation/participants * @copyright 2017 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_modal_events=_interopRequireDefault(_modal_events),_notification=_interopRequireDefault(_notification);const Selectors_bulkActionSelect="#formactionid",Selectors_bulkUserSelectedCheckBoxes="input[data-togglegroup^='participants-table'][data-toggle='slave']:checked",Selectors_participantsForm="#participantsform";_exports.init=()=>{const root=document.querySelector(Selectors_participantsForm),resetBulkAction=bulkActionSelect=>{bulkActionSelect.value=""};_custom_interaction_events.default.define(Selectors_bulkActionSelect,[_custom_interaction_events.default.events.accessibleChange]),(0,_jquery.default)(Selectors_bulkActionSelect).on(_custom_interaction_events.default.events.accessibleChange,(e=>{const action=e.target.value,checkboxes=root.querySelectorAll(Selectors_bulkUserSelectedCheckBoxes);if(-1!==action.indexOf("#")){e.preventDefault();const ids=[];checkboxes.forEach((checkbox=>{ids.push(checkbox.getAttribute("name").replace("user",""))})),"#messageselect"===action&&(0,_bulkactions.showSendMessage)(ids).then((modal=>(modal.getRoot().on(_modal_events.default.hidden,(()=>{const bulkActionSelector=root.querySelector(Selectors_bulkActionSelect);resetBulkAction(bulkActionSelector),bulkActionSelector.focus()})),modal))).catch(_notification.default.exception)}else""!==action&&checkboxes.length&&e.target.form().submit();resetBulkAction(e.target)}))}})); //# sourceMappingURL=participants.min.js.mapPK{\8/participation/amd/build/participants.min.js.mapnu[{"version":3,"file":"participants.min.js","sources":["../src/participants.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Some UI stuff for participants page.\n * This is also used by the report/participants/index.php because it has the same functionality.\n *\n * @module report_participation/participants\n * @copyright 2017 Damyon Wiese\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\nimport CustomEvents from 'core/custom_interaction_events';\nimport ModalEvents from 'core/modal_events';\nimport Notification from 'core/notification';\nimport {showSendMessage} from 'core_user/local/participants/bulkactions';\n\nconst Selectors = {\n bulkActionSelect: \"#formactionid\",\n bulkUserSelectedCheckBoxes: \"input[data-togglegroup^='participants-table'][data-toggle='slave']:checked\",\n participantsForm: '#participantsform',\n};\n\nexport const init = () => {\n const root = document.querySelector(Selectors.participantsForm);\n\n /**\n * Private method.\n *\n * @method registerEventListeners\n * @private\n */\n const registerEventListeners = () => {\n CustomEvents.define(Selectors.bulkActionSelect, [CustomEvents.events.accessibleChange]);\n jQuery(Selectors.bulkActionSelect).on(CustomEvents.events.accessibleChange, e => {\n const action = e.target.value;\n const checkboxes = root.querySelectorAll(Selectors.bulkUserSelectedCheckBoxes);\n\n if (action.indexOf('#') !== -1) {\n e.preventDefault();\n\n const ids = [];\n checkboxes.forEach(checkbox => {\n ids.push(checkbox.getAttribute('name').replace('user', ''));\n });\n\n if (action === '#messageselect') {\n showSendMessage(ids)\n .then(modal => {\n modal.getRoot().on(ModalEvents.hidden, () => {\n // Focus on the action select when the dialog is closed.\n const bulkActionSelector = root.querySelector(Selectors.bulkActionSelect);\n resetBulkAction(bulkActionSelector);\n bulkActionSelector.focus();\n });\n\n return modal;\n })\n .catch(Notification.exception);\n }\n } else if (action !== '' && checkboxes.length) {\n e.target.form().submit();\n }\n\n resetBulkAction(e.target);\n });\n };\n\n const resetBulkAction = bulkActionSelect => {\n bulkActionSelect.value = '';\n };\n\n registerEventListeners();\n};\n"],"names":["Selectors","root","document","querySelector","resetBulkAction","bulkActionSelect","value","define","CustomEvents","events","accessibleChange","on","e","action","target","checkboxes","querySelectorAll","indexOf","preventDefault","ids","forEach","checkbox","push","getAttribute","replace","then","modal","getRoot","ModalEvents","hidden","bulkActionSelector","focus","catch","Notification","exception","length","form","submit"],"mappings":";;;;;;;;sTA8BMA,2BACgB,gBADhBA,qCAE0B,6EAF1BA,2BAGgB,kCAGF,WACVC,KAAOC,SAASC,cAAcH,4BA4C9BI,gBAAkBC,mBACpBA,iBAAiBC,MAAQ,uCApCZC,OAAOP,2BAA4B,CAACQ,mCAAaC,OAAOC,uCAC9DV,4BAA4BW,GAAGH,mCAAaC,OAAOC,kBAAkBE,UAClEC,OAASD,EAAEE,OAAOR,MAClBS,WAAad,KAAKe,iBAAiBhB,0CAEZ,IAAzBa,OAAOI,QAAQ,KAAa,CAC5BL,EAAEM,uBAEIC,IAAM,GACZJ,WAAWK,SAAQC,WACfF,IAAIG,KAAKD,SAASE,aAAa,QAAQC,QAAQ,OAAQ,QAG5C,mBAAXX,yCACgBM,KACfM,MAAKC,QACFA,MAAMC,UAAUhB,GAAGiB,sBAAYC,QAAQ,WAE7BC,mBAAqB7B,KAAKE,cAAcH,4BAC9CI,gBAAgB0B,oBAChBA,mBAAmBC,WAGhBL,SAEVM,MAAMC,sBAAaC,eAEN,KAAXrB,QAAiBE,WAAWoB,QACnCvB,EAAEE,OAAOsB,OAAOC,SAGpBjC,gBAAgBQ,EAAEE"}PK{\a`d participation/tests/lib_test.phpnu[. /** * Tests for report library functions. * * @package report_participation * @copyright 2014 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace report_participation; defined('MOODLE_INTERNAL') || die(); /** * Class report_participation_lib_testcase * * @package report_participation * @copyright 2014 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class lib_test extends \advanced_testcase { /** * Test report_log_supports_logstore. */ public function test_report_participation_supports_logstore(): void { $logmanager = get_log_manager(); $allstores = \core_component::get_plugin_list_with_class('logstore', 'log\store'); $supportedstores = array( 'logstore_standard' => '\logstore_standard\log\store' ); // Make sure all supported stores are installed. $expectedstores = array_keys(array_intersect($allstores, $supportedstores)); $stores = $logmanager->get_supported_logstores('report_participation'); $stores = array_keys($stores); foreach ($expectedstores as $expectedstore) { $this->assertContains($expectedstore, $stores); } } } PK{\ ]!!6participation/tests/behat/filter_participation.featurenu[@report @report_participation Feature: In a participation report, admin can filter student actions In order to filter participation data As a student I need to log action and then log in as admin to view participation report Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | manager1 | Manager | 1 | manager1@example.com | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | manager1 | C1 | manager | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activity" exists: | course | C1 | | activity | forum | | name | Test forum name | | idnumber | forum1 | Scenario: Filter participation report when standard log reader is enabled later Given I log in as "admin" And I navigate to "Plugins > Logging > Manage log stores" in site administration And I click on "Disable" "link" in the "Standard log" "table_row" And I log out And I am on the "Test forum name" "forum activity" page logged in as student1 And I log out And I log in as "admin" And I navigate to "Plugins > Logging > Manage log stores" in site administration And I click on "Enable" "link" in the "Standard log" "table_row" And I am on the "Test forum name" "forum activity" page logged in as student1 And I am on the "Course 1" course page logged in as admin When I navigate to "Reports" in current page administration And I click on "Course participation" "link" And I set the following fields to these values: | Activity module | Test forum name | | Show only | Student | And I press "Go" Then the following should exist in the "reporttable" table: | -1- | All actions | | Student 1 | Yes (1) | Scenario: Filter participation report when only standard log reader is enabled by default Given I am on the "Test forum name" "forum activity" page logged in as student1 When I am on the "Course 1" course page logged in as admin And I navigate to "Reports" in current page administration And I click on "Course participation" "link" And I set the following fields to these values: | Activity module | Test forum name | | Show only | Student | And I press "Go" Then the following should exist in the "reporttable" table: | -1- | All actions | | Student 1 | Yes (1) | Scenario Outline: Filter participation report by viewable roles Given I am on the "Course 1" course page logged in as "teacher1" When I navigate to "Reports" in current page administration And I click on "Course participation" "link" # Teacher role cannot see Manager by default. Then "Manager" "option" should not exist in the "Show only" "select" And I set the following fields to these values: | Activity module | Test forum name | | Show only | | And I press "Go" And I should see "" in the "reporttable" "table" And I should not see "" in the "reporttable" "table" Examples: | role | uservisible | usernonvisible | | Student | Student 1 | Teacher 1 | | Teacher | Teacher 1 | Student 1 | PK{\Al\\=participation/tests/behat/course_report_participation.featurenu[@report @report_participation Feature: In a course administration page, navigate through report page, test for course participation page In order to navigate through report page As an admin Go to course administration -> Reports -> Course participation Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | admin | C1 | editingteacher | | student1 | C1 | student | Scenario: Selector should be available in the course participation page Given I log in as "admin" And I am on "Course 1" course homepage When I navigate to "Reports" in current page administration And I click on "Course participation" "link" Then "Report" "field" should exist in the "tertiary-navigation" "region" And I should see "Course participation" in the "tertiary-navigation" "region" PK{\8Dn$S S 6participation/tests/behat/message_participants.featurenu[@report @report_participation Feature: Use the participation report to message groups of students In order to engage with students based on participation As a teacher I need to be able to message students who have not participated in an activity Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | | teacher1 | Teacher | 1 | | student1 | Student | 1 | | student2 | Student | 2 | | student3 | Student | 3 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | student3 | C1 | student | And the following "activity" exists: | course | C1 | | activity | book | | name | Test book name | | idnumber | Test book name | | idnumber | book1 | And I am on the "Test book name" "book activity" page logged in as student1 @javascript Scenario: Message all students from the participation report Given I am on the "Course 1" course page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Course participation" "link" And I set the field "instanceid" to "Test book name" And I set the field "roleid" to "Student" And I press "Go" When I click on "select-all-participants" "checkbox" And I choose "Send a message" from the participants page bulk action menu Then "Send message to 3 people" "dialogue" should exist And I set the field "Message" to "Hi there" And I press "Send message to 3 people" And I should see "Message sent to 3 people" @javascript Scenario: Message students who have not participated in book Given I am on the "Course 1" course page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Course participation" "link" And I set the field "instanceid" to "Test book name" And I set the field "roleid" to "Student" And I press "Go" And I should see "Yes (1)" in the "Student 1" "table_row" And I should see "No" in the "Student 2" "table_row" And I should see "No" in the "Student 3" "table_row" When I press "Select all 'No'" And I choose "Send a message" from the participants page bulk action menu Then "Send message to 2 people" "dialogue" should exist And I set the field "Message" to "Hi there" And I press "Send message to 2 people" And I should see "Message sent to 2 people" Scenario: When messaging is disabled no message options should be displayed Given the following config values are set as admin: | messaging | 0 | And I am on the "Course 1" course page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Course participation" "link" When I set the field "instanceid" to "Test book name" And I set the field "roleid" to "Student" And I press "Go" Then I should not see "With selected users..." And "select-all-participants" "checkbox" should not exist PK{\Ǵ}noutline/styles.cssnu[#page-report-outline-user table tr td { padding: 10px 3px; min-width: 20px; } #page-report-outline-user .subsection .icon { padding-left: 10px; } PK{\ioutline/db/install.phpnu[. /** * Post installation and migration code. * * @package report * @subpackage outline * @copyright 2011 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; function xmldb_report_outline_install() { global $DB; } PK{\_(իoutline/db/access.phpnu[. /** * Capabilities * * @package report_outline * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'report/outline:view' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'coursereport/outline:view', ), 'report/outline:viewuserreport' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'report/outline:view', ) ); PK{\LHΨ"outline/lang/en/report_outline.phpnu[. /** * Strings * * @package report * @subpackage outline * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['eventactivityreportviewed'] = 'Activity report viewed'; $string['eventoutlinereportviewed'] = 'Outline report viewed'; $string['neverseen'] = 'Never seen'; $string['nocapability'] = 'Can not access user outline report'; $string['nologreaderenabled'] = 'No log reader enabled'; $string['numviews'] = '{$a->numviews} views by {$a->distinctusers} users'; $string['outline:view'] = 'View activity report'; $string['outline:viewuserreport'] = 'View user activity report'; $string['page-report-outline-x'] = 'Any outline report'; $string['page-report-outline-index'] = 'Course outline report'; $string['page-report-outline-user'] = 'User course outline report'; $string['pluginname'] = 'Activity report'; $string['privacy:metadata'] = 'The Activity report plugin does not store any personal data.'; PK{\+VZoutline/lib.phpnu[. /** * This file contains public API of outline report * * @package report * @subpackage outline * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * This function extends the course navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $course The course to object for the report * @param stdClass $context The context of the course */ function report_outline_extend_navigation_course($navigation, $course, $context) { if (has_capability('report/outline:view', $context)) { $url = new moodle_url('/report/outline/index.php', array('id'=>$course->id)); $navigation->add(get_string('pluginname', 'report_outline'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/report', '')); } } /** * This function extends the course navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $user * @param stdClass $course The course to object for the report */ function report_outline_extend_navigation_user($navigation, $user, $course) { if (report_outline_can_access_user_report($user, $course)) { $url = new moodle_url('/report/outline/user.php', array('id'=>$user->id, 'course'=>$course->id, 'mode'=>'outline')); $navigation->add(get_string('outlinereport'), $url); $url = new moodle_url('/report/outline/user.php', array('id'=>$user->id, 'course'=>$course->id, 'mode'=>'complete')); $navigation->add(get_string('completereport'), $url); } } /** * Is current user allowed to access this report * * @private defined in lib.php for performance reasons * * @param stdClass $user * @param stdClass $course * @return bool */ function report_outline_can_access_user_report($user, $course) { global $USER; $coursecontext = context_course::instance($course->id); $personalcontext = context_user::instance($user->id); if ($user->id == $USER->id) { if ($course->showreports and (is_viewing($coursecontext, $USER) or is_enrolled($coursecontext, $USER))) { return true; } } else if (has_capability('moodle/user:viewuseractivitiesreport', $personalcontext)) { if ($course->showreports and (is_viewing($coursecontext, $user) or is_enrolled($coursecontext, $user))) { return true; } } // Check if $USER shares group with $user (in case separated groups are enabled and 'moodle/site:accessallgroups' is disabled). if (!groups_user_groups_visible($course, $user->id)) { return false; } if (has_capability('report/outline:viewuserreport', $coursecontext)) { return true; } return false; } /** * Return a list of page types * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block * @return array */ function report_outline_page_type_list($pagetype, $parentcontext, $currentcontext) { $array = array( '*' => get_string('page-x', 'pagetype'), 'report-*' => get_string('page-report-x', 'pagetype'), 'report-outline-*' => get_string('page-report-outline-x', 'report_outline'), 'report-outline-index' => get_string('page-report-outline-index', 'report_outline'), 'report-outline-user' => get_string('page-report-outline-user', 'report_outline') ); return $array; } /** * Callback to verify if the given instance of store is supported by this report or not. * * @param string $instance store instance. * * @return bool returns true if the store is supported by the report, false otherwise. */ function report_outline_supports_logstore($instance) { if ($instance instanceof \core\log\sql_internal_table_reader) { return true; } return false; } /** * Add nodes to myprofile page. * * @param \core_user\output\myprofile\tree $tree Tree object * @param stdClass $user user object * @param bool $iscurrentuser * @param stdClass $course Course object * * @return bool */ function report_outline_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) { if (empty($course)) { // We want to display these reports under the site context. $course = get_fast_modinfo(SITEID)->get_course(); } if (report_outline_can_access_user_report($user, $course)) { $url = new moodle_url('/report/outline/user.php', array('id' => $user->id, 'course' => $course->id, 'mode' => 'outline')); $node = new core_user\output\myprofile\node('reports', 'outline', get_string('outlinereport'), null, $url); $tree->add_node($node); $url = new moodle_url('/report/outline/user.php', array('id' => $user->id, 'course' => $course->id, 'mode' => 'complete')); $node = new core_user\output\myprofile\node('reports', 'complete', get_string('completereport'), null, $url); $tree->add_node($node); } } PK{\䝲outline/version.phpnu[. /** * Version info * * @package report * @subpackage outline * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_outline'; // Full name of the plugin (used for diagnostics) PK{\. /** * Form to filter the outline report * * @package report_outline * @copyright 2017 Davo Smith, Synergy Learning * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_outline; defined('MOODLE_INTERNAL') || die(); global $CFG; require_once($CFG->libdir.'/formslib.php'); /** * Class filter_form form to filter the results by date * @package report_outline */ class filter_form extends \moodleform { /** * Form definition * @throws \HTML_QuickForm_Error * @throws \coding_exception */ protected function definition() { $mform = $this->_form; $mform->addElement('hidden', 'id'); $mform->setType('id', PARAM_INT); $mform->addElement('header', 'filterheader', get_string('filter')); $opts = ['optional' => true]; $mform->addElement('date_selector', 'filterstartdate', get_string('fromdate'), $opts); $mform->addElement('date_selector', 'filterenddate', get_string('todate'), $opts); $mform->setExpanded('filterheader', false); // Add the filter/cancel buttons (without 'closeHeaderBefore', so they collapse with the filter). $buttonarray = [ $mform->createElement('submit', 'submitbutton', get_string('filter')), $mform->createElement('cancel'), ]; $mform->addGroup($buttonarray, 'buttonar', '', [' '], false); } /** * Expand the form contents if the filter is in use. * @throws \HTML_QuickForm_Error */ public function definition_after_data() { $mform = $this->_form; $filterstartdate = $mform->getElement('filterstartdate')->getValue(); $filterenddate = $mform->getElement('filterenddate')->getValue(); if (!empty($filterstartdate['enabled']) || !empty($filterenddate['enabled'])) { $mform->setExpanded('filterheader', true); } } } PK{\|?$outline/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_outline. * * @package report_outline * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_outline\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_outline implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\D )outline/classes/output/activitieslist.phpnu[. namespace report_outline\output; use core_report\output\coursestructure; use course_modinfo; /** * Activities list page. * * @package report_outline * @copyright 2024 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activitieslist extends coursestructure { /** * Constructor * * @param course_modinfo $modinfo * @param array $views Views information for activity and users. * @param bool $showlastaccess Whether the last access should be shown or not. * @param int $minlog The minimum log time is computed. * @param bool $showblogs Whether related blog entries should be shown or not. */ public function __construct( course_modinfo $modinfo, /** @var array $views Views information for activity and users. */ protected array $views, /** @var bool $showlastaccess Whether the last access should be shown or not. */ protected bool $showlastaccess = true, /** @var int $minlog The minimum log time is computed. */ protected int $minlog = 0, /** @var bool $showblogs Whether related blog entries should be shown or not. */ protected bool $showblogs = true, ) { $this->modinfo = $modinfo; } /** * Exports the data. * * @param \renderer_base $output * @return array|\stdClass */ public function export_for_template(\renderer_base $output) { $table = parent::export_for_template($output); $table['id'] = 'outlinereport'; $data = [ 'minlog' => userdate($this->minlog), 'table' => $table, ]; return $data; } /** * Exports the headers for report table. * * @param \renderer_base $output * @return array */ protected function export_headers(\renderer_base $output): array { $headers = parent::export_headers($output); $headers[] = get_string('views'); if ($this->showblogs) { $headers[] = get_string('relatedblogentries', 'blog'); } if ($this->showlastaccess) { $headers[] = get_string('lastaccess'); } return $headers; } /** * Exports the data for a single activity. * * @param \renderer_base $output * @param \cm_info $cm * @param bool $indelegated Whether the activity is part of a delegated section or not. * @return array */ public function export_activity_data(\renderer_base $output, \cm_info $cm, bool $indelegated = false): array { global $CFG; $data = parent::export_activity_data($output, $cm, $indelegated); if (empty($data) || !in_array('cells', $data)) { return []; } if (!empty($this->views[$cm->id]->numviews)) { $numviewscell = get_string('numviews', 'report_outline', $this->views[$cm->id]); } else { $numviewscell = '-'; } $data['cells'][] = [ 'activityclass' => 'numviews', 'text' => $numviewscell, ]; if ($this->showblogs) { $cell = ['activityclass' => 'blog']; require_once($CFG->dirroot.'/blog/lib.php'); if ($blogcount = blog_get_associated_count($cm->get_course()->id, $cm->id)) { $blogurl = new \moodle_url('/blog/index.php', ['modid' => $cm->id]); $cell['link'] = new \action_link($blogurl, $blogcount); } else { $cell['text'] = '-'; } $data['cells'][] = $cell; } if ($this->showlastaccess) { if (isset($this->views[$cm->id]->lasttime)) { $timeago = format_time(time() - $this->views[$cm->id]->lasttime); $lastaccesscell = userdate($this->views[$cm->id]->lasttime)." ($timeago)"; } else { $lastaccesscell = ''; } $data['cells'][] = [ 'activityclass' => 'lastaccess', 'text' => $lastaccesscell, ]; } return $data; } } PK{\n  1outline/classes/output/hierarchicalactivities.phpnu[. namespace report_outline\output; use core_report\output\coursestructure; use course_modinfo; /** * Activities list page in a hierarchical format. * * @package report_outline * @copyright 2024 Amaia Anabitarte * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class hierarchicalactivities extends coursestructure { /** * Exports the data for a single activity. * * @param \renderer_base $output * @param \cm_info $cm * @param bool $indelegated Whether the activity is part of a delegated section or not. * @return array */ public function export_activity_data(\renderer_base $output, \cm_info $cm, bool $indelegated = false): array { if (!$cm->has_view()) { return []; } if (!$cm->uservisible) { return []; } return [ 'isactivity' => true, 'isdelegated' => false, 'indelegated' => $indelegated, 'visible' => $cm->visible, 'id' => $cm->id, ]; } /** * Print activity data. * * @param \renderer_base $output * @param string $mode * @param \cm_info $mod * @param \stdClass $user * @param \stdClass $course */ public function print_activity( \renderer_base $output, string $mode, \cm_info $mod, \stdClass $user, \stdClass $course, ): void { global $CFG, $DB; $instance = $DB->get_record($mod->modname, ['id' => $mod->instance]); $libfile = "$CFG->dirroot/mod/$mod->modname/lib.php"; if (!file_exists($libfile)) { return; } require_once($libfile); switch ($mode) { case "outline": $useroutline = $mod->modname . "_user_outline"; if (function_exists($useroutline)) { $toprint = $useroutline($course, $user, $mod, $instance); } else { $toprint = report_outline_user_outline($user->id, $mod->id, $mod->modname, $mod->instance); } if (!$toprint) { $toprint = (object) ['info' => '-']; } report_outline_print_row($mod, $instance, $toprint); break; case "complete": $usercomplete = $mod->modname . "_user_complete"; $image = $output->pix_icon('monologo', $mod->modfullname, 'mod_' . $mod->modname, ['class' => 'icon']); echo "

$image $mod->modfullname: " . "wwwroot/mod/$mod->modname/view.php?id=$mod->id\">" . format_string($instance->name, true) . "

"; ob_start(); echo "
    "; if (function_exists($usercomplete)) { $usercomplete($course, $user, $mod, $instance); } else { echo report_outline_user_complete($user->id, $mod->id, $mod->modname, $mod->instance); } echo "
"; $toprint = ob_get_contents(); ob_end_clean(); if (str_replace(' ', '', $toprint) != '
    ') { echo $toprint; } break; } } } PK{\q[ll0outline/classes/event/activity_report_viewed.phpnu[. /** * The report_outline activity report viewed event. * * @package report_outline * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_outline\event; defined('MOODLE_INTERNAL') || die(); /** * The report_outline activity report viewed event class. * * @package report_outline * @since Moodle 2.7 * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class activity_report_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventactivityreportviewed', 'report_outline'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the outline activity report for the course with id '$this->courseid'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/outline/index.php', ['id' => $this->courseid]); } } PK{\)pl l 'outline/classes/event/report_viewed.phpnu[. /** * The report_outline outline report viewed event. * * @package report_outline * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_outline\event; defined('MOODLE_INTERNAL') || die(); /** * The report_outline outline report viewed event class. * * @property-read array $other { * Extra information about the event. * * - string mode: display mode. * } * * @package report_outline * @since Moodle 2.7 * @copyright 2013 Ankit Agarwal * @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() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventoutlinereportviewed', 'report_outline'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the outline report for the user with id '$this->relateduserid' " . "for the course with id '$this->courseid'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/outline/user.php', array('course' => $this->courseid, 'id' => $this->relateduserid, 'mode' => $this->other['mode'])); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (empty($this->other['mode'])) { throw new \coding_exception('The \'mode\' value must be set in other.'); } if (empty($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } } public static function get_other_mapping() { // Nothing to map. return false; } } PK{\ +outline/templates/delegatedsection.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 report_outline/delegatedsection Template for Activity report sections. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { "visible": true, "class": "section", "text": "Section 1" } }}

    {{{ text }}}

    {{^visible}}
    {{#pix}}i/show, core{{/pix}}{{#str}}hiddenfromstudents{{/str}}
    {{/visible}} PK{\d"outline/templates/section.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 report_outline/section Template for Activity report sections. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { "visible": true, "class": "section", "text": "Section 1" } }}

    {{{ text }}}

    {{^visible}}
    {{#pix}}i/show, core{{/pix}}{{#str}}hiddenfromstudents{{/str}}
    {{/visible}} PK{\[WW#outline/templates/activity.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 report_outline/activity Template for Activity report activities. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { "indelegated": true, "visible": true, "class": "activity", "text": "Announcements", "cells": [{ "activityclass": "lastaccess", "text": "Friday, 14 June 2024, 5:17 PM" }] } }} {{#activitycolumn}}
    {{{ activityicon }}} {{#link}}{{/link}}{{{ text }}}{{#link}}{{/link}}
    {{^visible}}
    {{#pix}}i/show, core{{/pix}}{{#str}}hiddenfromstudents{{/str}}
    {{/visible}} {{/activitycolumn}} {{#cells}} {{#text}}{{{ text }}}{{/text}} {{#link}} {{> core/action_link}} {{/link}} {{/cells}} PK{\6fh !outline/templates/report.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 report_outline/report Template for Activity report. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { "minlog": "Monday, 29 April 2024, 4:18 PM", "table": { "class": "generaltable boxaligncenter", "id": "outlinereport", "headers": ["Activity", "Views", "Last access"], "headerscount": 3, "actitivities": [{ "isactivity": false, "indelegated": true, "visible": true, "class": "activity", "text": "Announcements", "cells": [{ "activityclass": "lastaccess", "text": "Friday, 14 June 2024, 5:17 PM" }] }, { "issection": true, "isdelegated": true, "visible": true, "class": "section", "text": "Section 1" }] } } }}
    {{#str}} computedfromlogs, admin, {{{ minlog }}} {{/str}}
    {{#table}} {{#headers}} {{/headers}} {{#activities}} {{#issection}} {{>report_outline/section }} {{/issection}} {{#isdelegated}} {{>report_outline/delegatedsection }} {{/isdelegated}} {{#isactivity}} {{>report_outline/activity }} {{/isactivity}} {{/activities}}
    {{{ . }}}
    {{/table}}
    PK{\eHE)E)outline/locallib.phpnu[. /** * This file contains functions used by the outline reports * * @package report * @subpackage outline * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\output\local\properties\iconsize; use core_course\output\activity_icon; defined('MOODLE_INTERNAL') || die; require_once(__DIR__.'/lib.php'); require_once($CFG->dirroot.'/course/lib.php'); function report_outline_print_row($mod, $instance, $result) { global $OUTPUT, $CFG; $activityicon = activity_icon::from_cm_info($mod) ->set_icon_size(iconsize::SIZE4); $image = $OUTPUT->render($activityicon); echo ""; echo "$image"; echo ""; echo " modfullname\""; echo " href=\"$CFG->wwwroot/mod/$mod->modname/view.php?id=$mod->id\">".format_string($instance->name,true).""; echo "   "; echo ""; if (isset($result->info)) { echo "$result->info"; } else { echo "

    -

    "; } echo ""; echo "   "; if (!empty($result->time)) { $timeago = format_time(time() - $result->time); echo "".userdate($result->time)." ($timeago)"; } echo ""; } /** * Returns an array of the commonly used log variables by the outline report. * * @return array the array of variables used */ function report_outline_get_common_log_variables() { global $DB; static $uselegacyreader; static $useinternalreader; static $minloginternalreader; static $logtable = null; if (isset($uselegacyreader) && isset($useinternalreader) && isset($minloginternalreader)) { return array($uselegacyreader, $useinternalreader, $minloginternalreader, $logtable); } $uselegacyreader = false; // Flag to determine if we should use the legacy reader. $useinternalreader = false; // Flag to determine if we should use the internal reader. $minloginternalreader = 0; // Set this to 0 for now. // Get list of readers. $logmanager = get_log_manager(); $readers = $logmanager->get_readers(); // Get preferred reader. if (!empty($readers)) { foreach ($readers as $readerpluginname => $reader) { // If sql_internal_table_reader is preferred reader. if ($reader instanceof \core\log\sql_internal_table_reader) { $useinternalreader = true; $logtable = $reader->get_internal_log_table_name(); $minloginternalreader = $DB->get_field_sql('SELECT min(timecreated) FROM {' . $logtable . '}'); } } } return array($uselegacyreader, $useinternalreader, $minloginternalreader, $logtable); } /** * Return the most commonly used user outline information. * * @param int $userid the id of the user * @param int $cmid the course module id * @param string $module the name of the module (eg. 'book') * @param int $instanceid (eg. the 'id' in the 'book' table) * @return stdClass|null if any information is found then a stdClass containing * this info is returned, else null is. */ function report_outline_user_outline($userid, $cmid, $module, $instanceid) { global $DB; list($uselegacyreader, $useinternalreader, $minloginternalreader, $logtable) = report_outline_get_common_log_variables(); // If using legacy log then get users from old table. if ($uselegacyreader) { // Create the params for the query. $params = array('userid' => $userid, 'module' => $module, 'action' => 'view', 'info' => $instanceid); // If we are going to use the internal (not legacy) log table, we should only get records // from the legacy table that exist before we started adding logs to the new table. $limittime = ''; if (!empty($minloginternalreader)) { $limittime = ' AND time < :timeto '; $params['timeto'] = $minloginternalreader; } $select = "SELECT COUNT(id) "; $from = "FROM {log} "; $where = "WHERE userid = :userid AND module = :module AND action = :action AND info = :info "; if ($legacylogcount = $DB->count_records_sql($select . $from . $where . $limittime, $params)) { $numviews = $legacylogcount; // Get the time for the last log. $select = "SELECT MAX(time) "; $lastlogtime = $DB->get_field_sql($select . $from . $where, $params); $result = new stdClass(); $result->info = get_string('numviews', '', $numviews); $result->time = $lastlogtime; } } // Get record from sql_internal_table_reader and combine with the number of views from the legacy log table (if needed). if ($useinternalreader) { $params = array('userid' => $userid, 'contextlevel' => CONTEXT_MODULE, 'contextinstanceid' => $cmid, 'crud' => 'r', 'edulevel1' => core\event\base::LEVEL_PARTICIPATING, 'edulevel2' => core\event\base::LEVEL_TEACHING, 'edulevel3' => core\event\base::LEVEL_OTHER, 'anonymous' => 0); $select = "SELECT COUNT(*) as count "; $from = "FROM {" . $logtable . "} "; $where = "WHERE userid = :userid AND contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND crud = :crud AND edulevel IN (:edulevel1, :edulevel2, :edulevel3) AND anonymous = :anonymous"; if ($internalreadercount = $DB->count_records_sql($select . $from . $where, $params)) { if (!empty($numviews)) { $numviews = $numviews + $internalreadercount; } else { $numviews = $internalreadercount; } // Get the time for the last log. $select = "SELECT MAX(timecreated) "; $lastlogtime = $DB->get_field_sql($select . $from . $where, $params); $result = new stdClass(); $result->info = get_string('numviews', '', $numviews); $result->time = $lastlogtime; } } if (!empty($result)) { return $result; } return null; } /** * Display the most commonly used user complete information. * * @param int $userid the id of the user * @param int $cmid the course module id * @param string $module the name of the module (eg. 'book') * @param int $instanceid (eg. the 'id' in the 'book' table) * @return string */ function report_outline_user_complete($userid, $cmid, $module, $instanceid) { global $DB; list($uselegacyreader, $useinternalreader, $minloginternalreader, $logtable) = report_outline_get_common_log_variables(); // If using legacy log then get users from old table. if ($uselegacyreader) { // Create the params for the query. $params = array('userid' => $userid, 'module' => $module, 'action' => 'view', 'info' => $instanceid); // If we are going to use the internal (not legacy) log table, we should only get records // from the legacy table that exist before we started adding logs to the new table. $limittime = ''; if (!empty($minloginternalreader)) { $limittime = ' AND time < :timeto '; $params['timeto'] = $minloginternalreader; } $select = "SELECT COUNT(id) "; $from = "FROM {log} "; $where = "WHERE userid = :userid AND module = :module AND action = :action AND info = :info "; if ($legacylogcount = $DB->count_records_sql($select . $from . $where . $limittime, $params)) { $numviews = $legacylogcount; // Get the time for the last log. $select = "SELECT MAX(time) "; $lastlogtime = $DB->get_field_sql($select . $from . $where, $params); $strnumviews = get_string('numviews', '', $numviews); } } // Get record from sql_internal_table_reader and combine with the number of views from the legacy log table (if needed). if ($useinternalreader) { $params = array('userid' => $userid, 'contextlevel' => CONTEXT_MODULE, 'contextinstanceid' => $cmid, 'crud' => 'r', 'edulevel1' => core\event\base::LEVEL_PARTICIPATING, 'edulevel2' => core\event\base::LEVEL_TEACHING, 'edulevel3' => core\event\base::LEVEL_OTHER, 'anonymous' => 0); $select = "SELECT COUNT(*) as count "; $from = "FROM {" . $logtable . "} "; $where = "WHERE userid = :userid AND contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid AND crud = :crud AND edulevel IN (:edulevel1, :edulevel2, :edulevel3) AND anonymous = :anonymous"; if ($internalreadercount = $DB->count_records_sql($select . $from . $where, $params)) { if (!empty($numviews)) { $numviews = $numviews + $internalreadercount; } else { $numviews = $internalreadercount; } // Get the time for the last log. $select = "SELECT MAX(timecreated) "; $lastlogtime = $DB->get_field_sql($select . $from . $where, $params); $strnumviews = get_string('numviews', '', $numviews); } } if (!empty($strnumviews) && (!empty($lastlogtime))) { return $strnumviews . ' - ' . get_string('mostrecently') . ' ' . userdate($lastlogtime); } else { return get_string('neverseen', 'report_outline'); } } PK{\@Q  outline/index.phpnu[. /** * Display user activity reports for a course (totals) * * @package report * @subpackage outline * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\report_helper; require('../../config.php'); require_once($CFG->dirroot.'/report/outline/locallib.php'); $id = required_param('id',PARAM_INT); // course id $startdate = optional_param('startdate', null, PARAM_INT); $enddate = optional_param('enddate', null, PARAM_INT); $course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST); $pageparams = array('id' => $id); if ($startdate) { $pageparams['startdate'] = $startdate; } if ($enddate) { $pageparams['enddate'] = $enddate; } $PAGE->set_url('/report/outline/index.php', $pageparams); $PAGE->set_pagelayout('report'); require_login($course); $context = context_course::instance($course->id); require_capability('report/outline:view', $context); // Handle form to filter access logs by date. $filterform = new \report_outline\filter_form(); $filterform->set_data(['id' => $course->id, 'filterstartdate' => $startdate, 'filterenddate' => $enddate]); if ($filterform->is_cancelled()) { $redir = $PAGE->url; $redir->remove_params(['startdate', 'enddate']); redirect($redir); } if ($filter = $filterform->get_data()) { $redir = $PAGE->url; if ($filter->filterstartdate) { $redir->param('startdate', $filter->filterstartdate); } if ($filter->filterenddate) { $redir->param('enddate', $filter->filterenddate); } redirect($redir); } // Trigger an activity report viewed event. $event = \report_outline\event\activity_report_viewed::create(array('context' => $context)); $event->trigger(); $showlastaccess = true; $showblogs = !empty($CFG->enableblogs) && $CFG->useblogassociations; $hiddenfields = explode(',', $CFG->hiddenuserfields); if (array_search('lastaccess', $hiddenfields) !== false and !has_capability('moodle/user:viewhiddendetails', $context)) { $showlastaccess = false; } $stractivityreport = get_string('pluginname', 'report_outline'); $PAGE->set_title($course->shortname .': '. $stractivityreport); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); // Print selector drop down. $pluginname = get_string('pluginname', 'report_outline'); report_helper::print_report_selector($pluginname); list($uselegacyreader, $useinternalreader, $minloginternalreader, $logtable) = report_outline_get_common_log_variables(); // If no legacy and no internal log then don't proceed. if (!$uselegacyreader && !$useinternalreader) { echo $OUTPUT->box_start('generalbox', 'notice'); echo $OUTPUT->notification(get_string('nologreaderenabled', 'report_outline')); echo $OUTPUT->box_end(); echo $OUTPUT->footer(); die(); } // We want to display the time we are beginning to get logs from in the heading. // If we are using the legacy reader check the minimum time in that log table. if ($uselegacyreader) { $minlog = $DB->get_field_sql('SELECT min(time) FROM {log}'); } // If we are using the internal reader check the minimum time in that table. if ($useinternalreader) { // If new log table has older data then don't use the minimum time obtained from the legacy table. if (empty($minlog) || ($minloginternalreader <= $minlog)) { $minlog = $minloginternalreader; } } $filterform->display(); $modinfo = get_fast_modinfo($course); // If using legacy log then get users from old table. if ($uselegacyreader) { // If we are going to use the internal (not legacy) log table, we should only get records // from the legacy table that exist before we started adding logs to the new table. $params = array('courseid' => $course->id, 'action' => 'view%', 'visible' => 1); $limittime = ''; if (!empty($minloginternalreader)) { $limittime = ' AND time < :timeto '; $params['timeto'] = $minloginternalreader; } if ($startdate) { $limittime .= ' AND time >= :startdate '; $params['startdate'] = $startdate; } if ($enddate) { $limittime .= ' AND time < :enddate '; $params['enddate'] = $enddate; } // Check if we need to show the last access. $sqllasttime = ''; if ($showlastaccess) { $sqllasttime = ", MAX(time) AS lasttime"; } $logactionlike = $DB->sql_like('l.action', ':action'); $sql = "SELECT cm.id, COUNT('x') AS numviews, COUNT(DISTINCT userid) AS distinctusers $sqllasttime FROM {course_modules} cm JOIN {modules} m ON m.id = cm.module JOIN {log} l ON l.cmid = cm.id WHERE cm.course = :courseid AND $logactionlike AND m.visible = :visible $limittime GROUP BY cm.id"; $views = $DB->get_records_sql($sql, $params); } // Get record from sql_internal_table_reader and merge with records obtained from legacy log (if needed). if ($useinternalreader) { // Check if we need to show the last access. $sqllasttime = ''; if ($showlastaccess) { $sqllasttime = ", MAX(timecreated) AS lasttime"; } $params = array('courseid' => $course->id, 'contextmodule' => CONTEXT_MODULE); $limittime = ''; if ($startdate) { $limittime .= ' AND timecreated >= :startdate '; $params['startdate'] = $startdate; } if ($enddate) { $limittime .= ' AND timecreated < :enddate '; $params['enddate'] = $enddate; } $sql = "SELECT contextinstanceid as cmid, COUNT('x') AS numviews, COUNT(DISTINCT userid) AS distinctusers $sqllasttime FROM {" . $logtable . "} l WHERE courseid = :courseid AND anonymous = 0 AND crud = 'r' AND contextlevel = :contextmodule $limittime GROUP BY contextinstanceid"; $v = $DB->get_records_sql($sql, $params); if (empty($views)) { $views = $v; } else { // Merge two view arrays. foreach ($v as $key => $value) { if (isset($views[$key]) && !empty($views[$key]->numviews)) { $views[$key]->numviews += $value->numviews; if ($value->lasttime > $views[$key]->lasttime) { $views[$key]->lasttime = $value->lasttime; } } else { $views[$key] = $value; } } } } $activitieslist = new report_outline\output\activitieslist($modinfo, $views, $showlastaccess, $minlog, $showblogs); echo $OUTPUT->render_from_template('report_outline/report', $activitieslist->export_for_template($OUTPUT)); echo $OUTPUT->footer(); PK{\Ѭ}UUoutline/tests/lib_test.phpnu[. /** * Tests for report library functions. * * @package report_outline * @copyright 2014 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace report_outline; defined('MOODLE_INTERNAL') || die(); global $CFG; /** * Class report_outline_lib_testcase * * @package report_outline * @copyright 2014 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class lib_test extends \advanced_testcase { /** * @var stdClass The user. */ private $user; /** * @var stdClass The course. */ private $course; /** * @var context_course Course context. */ private $coursecontext; /** * @var \core_user\output\myprofile\tree The navigation tree. */ private $tree; /** * @var int Dummy role for testing. */ private $roleid; public function setUp(): void { parent::setUp(); $this->user = $this->getDataGenerator()->create_user(); $this->course = $this->getDataGenerator()->create_course(); $this->tree = new \core_user\output\myprofile\tree(); $this->coursecontext = \context_course::instance($this->course->id); $this->roleid = create_role('Dummy role', 'dummyrole', 'dummy role description'); $this->resetAfterTest(); } /** * Test report_log_supports_logstore. */ public function test_report_participation_supports_logstore(): void { $logmanager = get_log_manager(); $allstores = \core_component::get_plugin_list_with_class('logstore', 'log\store'); $supportedstores = array( 'logstore_standard' => '\logstore_standard\log\store' ); // Make sure all supported stores are installed. $expectedstores = array_keys(array_intersect($allstores, $supportedstores)); $stores = $logmanager->get_supported_logstores('report_outline'); $stores = array_keys($stores); foreach ($expectedstores as $expectedstore) { $this->assertContains($expectedstore, $stores); } } /** * Tests the report_outline_myprofile_navigation() function as an admin user. */ public function test_report_outline_myprofile_navigation(): void { $this->setAdminUser(); $iscurrentuser = false; report_outline_myprofile_navigation($this->tree, $this->user, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayHasKey('outline', $nodes->getValue($this->tree)); $this->assertArrayHasKey('complete', $nodes->getValue($this->tree)); } /** * Tests the report_outline_myprofile_navigation() function as a user without permission. */ public function test_report_outline_myprofile_navigation_without_permission(): void { $this->setUser($this->user); $iscurrentuser = true; report_outline_myprofile_navigation($this->tree, $this->user, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayNotHasKey('outline', $nodes->getValue($this->tree)); $this->assertArrayNotHasKey('complete', $nodes->getValue($this->tree)); } /** * Test that the current user can not access user report without report/outline:viewuserreport permission. */ public function test_report_outline_can_not_access_user_report_without_viewuserreport_permission(): void { $this->getDataGenerator()->role_assign($this->roleid, $this->user->id, $this->coursecontext->id); $this->setUser($this->user); $this->assertFalse(report_outline_can_access_user_report($this->user, $this->course)); } /** * Test that the current user can access user report with report/outline:viewuserreport permission. */ public function test_report_outline_can_access_user_report_with_viewuserreport_permission(): void { assign_capability('report/outline:viewuserreport', CAP_ALLOW, $this->roleid, $this->coursecontext->id, true); $this->getDataGenerator()->role_assign($this->roleid, $this->user->id, $this->coursecontext->id); $this->setUser($this->user); $this->assertTrue(report_outline_can_access_user_report($this->user, $this->course)); } } PK{\i~ ~ outline/tests/behat/user.featurenu[@report @report_outline Feature: View the user page for the outline report In order to ensure the user page for the outline report works as expected As a student I need to log in as a student and view the user page for the outline report Background: Given the following "courses" exist: | fullname | shortname | format | showreports | | Course 1 | C1 | topics | 1 | And the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | description | course | idnumber | | folder | Folder name | Folder description | C1 | folder1 | And the following "activities" exist: | activity | name | description | course | idnumber | externalurl | | url | URL name | URL description | C1 | folder1 | http://www.google.com | When I log in as "admin" Scenario: View the user page when only the standard log reader is enabled Given I navigate to "Plugins > Logging > Manage log stores" in site administration And "Disable" "link" should exist in the "Standard log" "table_row" And I log out And I log in as "student1" And I am on "Course 1" course homepage # We want to view this multiple times, to make sure the count is working. And I follow "Folder name" And I reload the page And I reload the page And I reload the page And I am on "Course 1" course homepage # We want to view this multiple times, to make sure the count is working. And I follow "URL name" And I reload the page And I reload the page And I follow "Profile" in the user menu And I click on "Course 1" "link" in the "region-main" "region" When I follow "Outline report" Then I should see "4 views" in the "Folder name" "table_row" And I should see "3 views" in the "URL name" "table_row" And I follow "Profile" in the user menu And I click on "Course 1" "link" in the "region-main" "region" When I follow "Complete report" And I should see "4 views" And I should see "3 views" Scenario: View the user complete report page when there is a no-grade forum Given the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1 | And I am on "Course 1" course homepage When I follow "Participants" And I follow "Student 1" And I follow "Outline report" Then I should see "Activity report" When I follow "Participants" And I follow "Student 1" And I follow "Complete report" Then I should see "Activity report" PK{\ C ,outline/tests/behat/behat_report_outline.phpnu[. /** * Custom behat functions * * @package report_outline * @copyright 2017 Davo Smith, Synergy Learning * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); /** * Class behat_report_outline custom Behat steps for report_outline. */ class behat_report_outline extends behat_base { /** * This is a horrible, horrible hack, but it is not clear how else a range of log entries can be produced to test the * filtering of the log entries. * * @Given /^the log timestamp for "(?P(?:[^"]|\\")*)" and "(?P(?:[^"]|\\")*)" is set to "(?P(?:[^"]|\\")*)"$/ * @param string $username * @param string $activityidnumber * @param string $date */ public function the_log_timestamp_for_and_is_set_to($username, $activityidnumber, $date) { global $DB; // Get the name of the log table. $lm = get_log_manager(); $readers = $lm->get_readers('\\core\\log\\sql_internal_table_reader'); $reader = reset($readers); $table = $reader->get_internal_log_table_name(); // Find the log entry. $cmrec = $DB->get_record('course_modules', ['idnumber' => $activityidnumber], '*', MUST_EXIST); $modname = $DB->get_field('modules', 'name', ['id' => $cmrec->module], MUST_EXIST); $userid = $DB->get_field('user', 'id', ['username' => $username], MUST_EXIST); $cond = [ 'userid' => $userid, 'component' => 'mod_'.$modname, 'target' => 'course_module', 'action' => 'viewed', 'contextinstanceid' => $cmrec->id, ]; $logentries = $DB->get_records($table, $cond, 'timecreated DESC', 'id', 0, 1); $logentry = reset($logentries); // Update the timecreated for the entry. $timestamp = strtotime($date); $DB->set_field($table, 'timecreated', $timestamp, ['id' => $logentry->id]); } }PK{\%[@@.outline/tests/behat/subsection_reports.featurenu[@mod @mod_subsection @report Feature: Subsections are shown in reports In order to use reports As an teacher I need to see sections and subsections structure in reports Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | category | numsections | initsections | | Course 1 | C1 | 0 | 1 | 1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "activities" exist: | activity | name | course | idnumber | section | | page | First page | C1 | page1 | 1 | | subsection | Subsection 1 | C1 | subsection1 | 1 | | page | Last page | C1 | last | 1 | | page | Page in Subsection 1 | C1 | subpage | 2 | And I log in as "teacher1" @report_outline Scenario: Course Activity report show subsections' information Given I am on "Course 1" course homepage When I navigate to "Reports > Activity report" in current page administration Then I should see "First page" in the "generaltable" "table" And "Subsection" "table_row" should appear before "Last page" "table_row" And "Page in Subsection 1" "table_row" should appear before "Last page" "table_row" PK{\X0//#outline/tests/behat/outline.featurenu[@report @report_outline Feature: View an outline report In order to ensure the outline report works as expected As a teacher I need to log in as a teacher and view the outline report Background: Given the following "courses" exist: | fullname | shortname | format | numsections | | Course 1 | C1 | topics | 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 "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | | forum | Forum name | C1 | forum1 | | book | Book name | C1 | book1 | When I am on the "Course 1" course page logged in as admin Scenario: View the outline report when only the standard log reader is enabled Given I navigate to "Plugins > Logging > Manage log stores" in site administration And "Disable" "link" should exist in the "Standard log" "table_row" And I am on the "Course 1" course page logged in as student1 And I follow "Forum name" And I am on "Course 1" course homepage And I follow "Book name" And I am on the "Course 1" course page logged in as student2 And I follow "Book name" And I am on the "Course 1" course page logged in as admin When I navigate to "Reports" in current page administration And I click on "Activity report" "link" Then I should see "2 views by 2 users" in the "Book name" "table_row" And I should see "1 views by 1 users" in the "Forum name" "table_row" Scenario: View the outline report when no log reader is enabled Given I navigate to "Plugins > Logging > Manage log stores" in site administration And I click on "Disable" "link" in the "Standard log" "table_row" And I am on "Course 1" course homepage When I navigate to "Reports" in current page administration And I click on "Activity report" "link" Then I should see "No log reader enabled" Scenario: Multiple views from a single user are identified as not distinct Given I am on the "Course 1" course page logged in as student1 And I follow "Forum name" And I am on "Course 1" course homepage And I follow "Forum name" And I am on "Course 1" course homepage And I follow "Forum name" And I am on site homepage When I am on the "Course 1" course page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Activity report" "link" Then I should see "3 views by 1 users" in the "Forum name" "table_row" And I should see "-" in the "Book name" "table_row" Scenario: Multiple views from multiple users are identified as not distinct Given I am on the "Course 1" course page logged in as student1 And I follow "Forum name" And I am on "Course 1" course homepage And I follow "Forum name" And I am on "Course 1" course homepage And I follow "Forum name" And I am on site homepage Given I am on the "Course 1" course page logged in as student2 And I follow "Forum name" And I am on "Course 1" course homepage And I follow "Forum name" And I am on "Course 1" course homepage And I follow "Forum name" And I am on site homepage When I am on the "Course 1" course page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Activity report" "link" Then I should see "6 views by 2 users" in the "Forum name" "table_row" And I should see "-" in the "Book name" "table_row" Scenario: No views from any users When I am on the "Course 1" course page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Activity report" "link" Then I should see "-" in the "Forum name" "table_row" And I should see "-" in the "Book name" "table_row" Scenario: The outline report can represent courses with subsections Given the following "activities" exist: | activity | name | course | section | visible | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled | | assign | Activity1 | C1 | 1 | 1 | 1 | 0 | And the following "activities" exist: | activity | name | course | section | visible | | subsection | Subsection1 | C1 | 1 | 1 | And the following "activities" exist: | activity | name | course | section | visible | assignsubmission_onlinetext_enabled | assignsubmission_file_enabled | | assign | Subactivity1.1 | C1 | 2 | 1 | 1 | 0 | | assign | Subactivity1.2 | C1 | 2 | 0 | 1 | 0 | | assign | Activity2 | C1 | 1 | 1 | 1 | 0 | When I am on the "Course 1" course page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Activity report" "link" Then "Subactivity1.1" "table_row" should appear after "Activity1" "table_row" And "Subactivity1.2" "table_row" should appear after "Subactivity1.1" "table_row" And "Activity2" "table_row" should appear after "Subactivity1.2" "table_row" And I navigate to "Participants" in current page administration And I click on "Student 1" "link" And I click on "Outline report" "link" And "Subactivity1.1" "table_row" should appear after "Activity1" "table_row" And I should not see "Subactivity1.2" in the "page-content" "region" And "Activity2" "table_row" should appear after "Subactivity1.1" "table_row" PK{\"outline/tests/behat/filter.featurenu[@report @report_outline Feature: Filter an outline report In order to ensure the outline report works as expected As a teacher I need to log in as a teacher and view the outline report with various filters in place Background: Given the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | 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 "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | section | | forum | Forum name | C1 | FORUM01 | 1 | | book | Book name | C1 | BOOK01 | 1 | When I am on the "Course 1" course page logged in as admin Scenario: Filter the outline report by start date Given I navigate to "Plugins > Logging > Manage log stores" in site administration And "Disable" "link" should exist in the "Standard log" "table_row" And I am on the "Forum name" "forum activity" page logged in as student1 And the log timestamp for "student1" and "FORUM01" is set to "12 June 2017 12:49:00" And I am on "Course 1" course homepage And I follow "Book name" And the log timestamp for "student1" and "BOOK01" is set to "10 June 2017 14:01:00" And I am on the "Book name" "book activity" page logged in as student2 And the log timestamp for "student2" and "BOOK01" is set to "14 June 2017 11:02:00" And I am on the "Course 1" course page logged in as admin And I navigate to "Reports" in current page administration And I click on "Activity report" "link" And I should see "2 views by 2 users" in the "Book name" "table_row" And I should see "1 views by 1 users" in the "Forum name" "table_row" When I set the following fields to these values: | filterstartdate[enabled] | 1 | | filterstartdate[day] | 12 | | filterstartdate[month] | June | | filterstartdate[year] | 2017 | And I click on "Filter" "button" in the "#fgroup_id_buttonar" "css_element" Then I should see "1 views by 1 users" in the "Book name" "table_row" And I should see "1 views by 1 users" in the "Forum name" "table_row" Scenario: Filter the outline report by end date Given I navigate to "Plugins > Logging > Manage log stores" in site administration And "Disable" "link" should exist in the "Standard log" "table_row" And I am on the "Forum name" "forum activity" page logged in as student1 And the log timestamp for "student1" and "FORUM01" is set to "12 June 2017 12:49:00" And I am on "Course 1" course homepage And I follow "Book name" And the log timestamp for "student1" and "BOOK01" is set to "10 June 2017 14:01:00" And I am on the "Book name" "book activity" page logged in as student2 And the log timestamp for "student2" and "BOOK01" is set to "14 June 2017 11:02:00" And I am on the "Course 1" course page logged in as admin And I navigate to "Reports" in current page administration And I click on "Activity report" "link" And I should see "2 views by 2 users" in the "Book name" "table_row" And I should see "1 views by 1 users" in the "Forum name" "table_row" When I set the following fields to these values: | filterenddate[enabled] | 1 | | filterenddate[day] | 11 | | filterenddate[month] | June | | filterenddate[year] | 2017 | And I click on "Filter" "button" in the "#fgroup_id_buttonar" "css_element" Then I should see "1 views by 1 users" in the "Book name" "table_row" And I should not see "views by" in the "Forum name" "table_row" PK{\)}MM1outline/tests/behat/course_report_outline.featurenu[@report @report_outline Feature: In a course administration page, navigate through report page, test for activity report page In order to navigate through report page As an admin Go to course administration -> Reports -> Activity report Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | admin | C1 | editingteacher | | student1 | C1 | student | @javascript Scenario: Selector should be available in course activiy report page Given I log in as "admin" And I am on "Course 1" course homepage When I navigate to "Reports" in current page administration And I click on "Activity report" "link" Then "Report" "field" should exist in the "tertiary-navigation" "region" And I should see "Activity report" in the "tertiary-navigation" "region" PK{\Koutline/user.phpnu[. /** * Display user activity reports for a course (totals) * * @package report * @subpackage outline * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use report_outline\output\hierarchicalactivities; require('../../config.php'); require_once($CFG->dirroot.'/report/outline/locallib.php'); require_once($CFG->dirroot.'/report/outline/lib.php'); $userid = required_param('id', PARAM_INT); $courseid = required_param('course', PARAM_INT); $mode = optional_param('mode', 'outline', PARAM_ALPHA); if ($mode !== 'complete' and $mode !== 'outline') { $mode = 'outline'; } $user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0), '*', MUST_EXIST); $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST); $coursecontext = context_course::instance($course->id); $personalcontext = context_user::instance($user->id); if ($courseid == SITEID) { $PAGE->set_context($personalcontext); } if ($USER->id != $user->id and has_capability('moodle/user:viewuseractivitiesreport', $personalcontext) and !is_enrolled($coursecontext, $USER) and is_enrolled($coursecontext, $user)) { //TODO: do not require parents to be enrolled in courses - this is a hack! require_login(); $PAGE->set_course($course); } else { require_login($course); } $PAGE->set_url('/report/outline/user.php', array('id'=>$userid, 'course'=>$courseid, 'mode'=>$mode)); if (!report_outline_can_access_user_report($user, $course)) { throw new \moodle_exception('nocapability', 'report_outline'); } $stractivityreport = get_string('activityreport'); $PAGE->set_pagelayout('report'); $PAGE->set_url('/report/outline/user.php', array('id'=>$user->id, 'course'=>$course->id, 'mode'=>$mode)); $PAGE->navigation->extend_for_user($user); $PAGE->navigation->set_userid_for_parent_checks($user->id); // see MDL-25805 for reasons and for full commit reference for reversal when fixed. $PAGE->set_title("$course->shortname: $stractivityreport"); // Create the appropriate breadcrumb. $navigationnode = array( 'url' => new moodle_url('/report/outline/user.php', array('id' => $user->id, 'course' => $course->id, 'mode' => $mode)) ); if ($mode === 'complete') { $navigationnode['name'] = get_string('completereport'); } else { $navigationnode['name'] = get_string('outlinereport'); } $PAGE->add_report_nodes($user->id, $navigationnode); if ($courseid == SITEID) { $PAGE->set_heading(fullname($user)); } else { $PAGE->set_heading($course->fullname); } // Trigger a report viewed event. $event = \report_outline\event\report_viewed::create(array('context' => context_course::instance($course->id), 'relateduserid' => $userid, 'other' => array('mode' => $mode))); $event->trigger(); echo $OUTPUT->header(); if ($courseid != SITEID) { $backurl = new moodle_url('/user/view.php', ['id' => $userid, 'course' => $courseid]); echo $OUTPUT->single_button($backurl, get_string('back'), 'get', ['class' => 'mb-3']); echo $OUTPUT->context_header( array( 'heading' => fullname($user), 'user' => $user, 'usercontext' => $personalcontext ), 2); if ($mode === 'outline') { echo $OUTPUT->heading(get_string('outlinereport', 'moodle'), 2, 'main mt-4 mb-4'); } else { echo $OUTPUT->heading(get_string('completereport', 'moodle'), 2, 'main mt-4 mb-4'); } } $modinfo = get_fast_modinfo($course, $user->id); $itemsprinted = false; $coursestructure = new hierarchicalactivities($modinfo); $sections = $coursestructure->export_hierarchy($OUTPUT); foreach ($sections as $i => $section) { if (!$section['visible']) { continue; } $section = (object) $section; $itemsprinted = true; echo '
    '; echo '

    '; echo $section->text; echo "

    "; echo '
    '; if ($mode == "outline") { echo ""; } foreach ($section->activities as $cm) { if (!$cm['visible']) { continue; } $cm = (object) $cm; if ($cm->isdelegated) { // Print subsection box. if ($mode == "outline") { echo "
    "; } echo '
    '; echo '

    '; echo $cm->text; echo "

    "; echo '
    '; if ($mode == "outline") { echo ""; } foreach ($cm->activities as $subcm) { if (!$subcm['visible']) { continue; } $subcm = (object) $subcm; $mod = $modinfo->cms[$subcm->id]; $coursestructure->print_activity($OUTPUT, $mode, $mod, $user, $course); } if ($mode == "outline") { echo "
    "; } echo "
    "; // Content. echo "
    "; // Subsection. if ($mode == "outline") { echo ""; } continue; } $mod = $modinfo->cms[$cm->id]; $coursestructure->print_activity($OUTPUT, $mode, $mod, $user, $course); } if ($mode == "outline") { echo "
    "; } echo '
    '; // Content. echo '
    '; // Section. } if (!$itemsprinted) { echo $OUTPUT->notification(get_string('nothingtodisplay'), 'info', false); } echo $OUTPUT->footer(); PK{\rperformance/db/access.phpnu[. /** * Capabilities * * @package report_performance * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'report/performance:view' => array( 'riskbitmask' => RISK_CONFIG, 'captype' => 'read', 'contextlevel' => CONTEXT_SYSTEM, 'archetypes' => array( 'manager' => CAP_ALLOW ), ) ); PK{\Ϸ*performance/lang/en/report_performance.phpnu[. /** * Lang strings * * @package report_performance * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['check_backup'] = 'Automated backup'; $string['check_backup_comment_disable'] = 'Performance may be affected during the backup process. If enabled, backups should be scheduled for off-peak times.'; $string['check_backup_comment_enable'] = 'Performance may be affected during the backup process. Backups should be scheduled for off-peak times.'; $string['check_backup_details'] = 'Enabling automated backup will automatically create archives of all the courses on the server at the time you specified.

    During this process, it will consume more server resources and may affect performance.

    '; $string['check_cachejs_comment_disable'] = 'If enabled, page loading performance is improved.'; $string['check_cachejs_comment_enable'] = 'If disabled, page might load slow.'; $string['check_cachejs_details'] = 'Javascript caching and compression greatly improves page loading performance. It is strongly recommended for production sites.'; $string['check_dbschema_name'] = 'Database schema check'; $string['check_dbschema_ok'] = 'Database schema is correct.'; $string['check_dbschema_errors'] = 'Database schema is not aligned.'; $string['check_debugmsg_comment_nodeveloper'] = 'If set to DEVELOPER, performance may be affected slightly.'; $string['check_debugmsg_comment_developer'] = 'If set to a value other than DEVELOPER, performance may be improved slightly.'; $string['check_debugmsg_details'] = 'There is rarely any advantage in going to Developer level, unless requested by a developer.

    Once you have obtained the error message, and copied and pasted it somewhere, it is HIGHLY RECOMMENDED to turn Debug back to NONE. Debug messages can give clues to a hacker as to the setup of your site and may affect performance.

    '; $string['check_enablestats_comment_disable'] = 'Performance may be affected by statistics processing. If enabled, statistics settings should be set with caution.'; $string['check_enablestats_comment_enable'] = 'Performance may be affected by statistics processing. Statistics settings should be set with caution.'; $string['check_enablestats_details'] = 'Enabling this will process the logs in cronjob and gather some statistics. Depending on the amount of traffic on your site, this can take awhile.

    During this process, it will consume more server resources and may affect performance.

    '; $string['check_themedesignermode_comment_enable'] = 'If disabled, images and style sheets are cached, resulting in significant performance improvements.'; $string['check_themedesignermode_comment_disable'] = 'If enabled, images and style sheets will not be cached, resulting in significant performance degradation.'; $string['check_themedesignermode_details'] = 'This is often the cause of slow Moodle sites.

    On average it might take at least twice the amount of CPU to run a Moodle site with theme designer mode enabled.

    '; $string['comments'] = 'Comments'; $string['edit'] = 'Edit'; $string['enabled'] = 'Enabled'; $string['disabled'] = 'Disabled'; $string['issue'] = 'Issue'; $string['morehelp'] = 'more help'; $string['performance:view'] = 'View performance report'; $string['performancereportdesc'] = 'This report lists issues which may affect performance of the site {$a}'; $string['pluginname'] = 'Performance overview'; $string['value'] = 'Value'; $string['privacy:metadata'] = 'The Performance overview plugin does not store any personal data.'; PK{\D3 performance/version.phpnu[. /** * Version info for report_performance. * * @package report_performance * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_performance'; // Full name of the plugin (used for diagnostics). PK{\U(performance/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_performance. * * @package report_performance * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_performance\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_performance implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\K..performance/index.phpnu[. /** * Performance overview report * * @package report_performance * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('NO_OUTPUT_BUFFERING', true); require('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); admin_externalpage_setup('reportperformance', '', null, '', array('pagelayout' => 'report')); $detail = optional_param('detail', '', PARAM_TEXT); // Show detailed info about one check only. $url = '/report/performance/index.php'; $table = new core\check\table('performance', $url, $detail); if (!empty($table->detail)) { $PAGE->set_docs_path(new moodle_url($url, ['detail' => $detail])); $PAGE->navbar->add($table->detail->get_name()); } echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('pluginname', 'report_performance')); echo $table->render($OUTPUT); echo $OUTPUT->footer(); PK{\jrrperformance/settings.phpnu[. /** * Settings and links * * @package report_performance * @copyright 2013 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $ADMIN->add('reports', new admin_externalpage('reportperformance', get_string('pluginname', 'report_performance'), $CFG->wwwroot."/report/performance/index.php", 'report/performance:view')); // No report settings. $settings = null; PK{\*Jff UPGRADING.mdnu[# report (plugin type) Upgrade notes ## 4.5 ### Added - Report has been added to subsystem components list. For more information see [MDL-81771](https://tracker.moodle.org/browse/MDL-81771) - A new general output class, `\core_report\output\coursestructure`, has been created. For more information see [MDL-81771](https://tracker.moodle.org/browse/MDL-81771) ### Changed - The `\core\report_helper::print_report_selector()` method accepts a new `$additional`` argument for adding content to the tertiary navigation to align with the report selector. For more information see [MDL-78773](https://tracker.moodle.org/browse/MDL-78773) ### Removed - The previously deprecated `\core\report_helper::save_selected_report()` method has been removed and can no longer be used. For more information see [MDL-72353](https://tracker.moodle.org/browse/MDL-72353) PK{\+loglive/styles.cssnu[#page-report-loglive-index .info { margin: 10px; } table.flexible > tbody > tr:nth-child(n).newrow > td { background: #d4d4d4; }PK{\x  loglive/db/access.phpnu[. /** * Capabilities * * This files lists capabilites related to report_logline * * @package report_loglive * @copyright 2011 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'report/loglive:view' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), ), ); PK{\o"loglive/lang/en/report_loglive.phpnu[. /** * Lang strings. * * Language strings used by report_loglive * * @package report_loglive * @copyright 2011 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['eventreportviewed'] = 'Live log report viewed'; $string['eventcomponent'] = 'Component'; $string['eventcontext'] = 'Event context'; $string['eventloggedas'] = '{$a->realusername} as {$a->asusername}'; $string['eventorigin'] = 'Origin'; $string['eventrelatedfullnameuser'] = 'Affected user'; $string['livelogs'] = 'Live logs from the past hour'; $string['livelogswithupdate'] = 'Live logs from the past hour ({$a})'; $string['loglive:view'] = 'View live logs'; $string['nologreaderenabled'] = 'No log reader enabled'; $string['pause'] = 'Pause live updates'; $string['pluginname'] = 'Live logs'; $string['resume'] = 'Resume live updates'; $string['selectlogreader'] = 'Select log reader'; $string['privacy:metadata'] = 'The Live logs plugin does not store any personal data.'; PK{\l=loglive/lib.phpnu[. /** * Libs, public API. * * NOTE: page type not included because there can not be any blocks in popups * * @package report_loglive * @copyright 2011 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\report_helper; defined('MOODLE_INTERNAL') || die; /** * This function extends the navigation with the report items * * @global stdClass $CFG * @global core_renderer $OUTPUT * @param navigation_node $navigation The navigation node to extend * @param stdClass $course The course to object for the report * @param context $context The context of the course */ function report_loglive_extend_navigation_course($navigation, $course, $context) { if ( has_capability('report/log:view', $context) && report_helper::has_valid_group($context) ) { $url = new moodle_url('/report/loglive/index.php', array('id' => $course->id)); $navigation->add(get_string('pluginname', 'report_loglive'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/report', '')); } } /** * Callback to verify if the given instance of store is supported by this report or not. * * @param string $instance store instance. * * @return bool returns true if the store is supported by the report, false otherwise. */ function report_loglive_supports_logstore($instance) { if ($instance instanceof \core\log\sql_reader) { return true; } return false; } PK{\Èloglive/version.phpnu[. /** * Version info * * This file contains version information about report_loglive * * @package report_loglive * @copyright 2011 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_loglive'; // Full name of the plugin (used for diagnostics). PK{\Џloglive/classes/renderer.phpnu[. /** * Loglive report renderer. * * @package report_loglive * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * Report log renderer's for printing reports. * * @since Moodle 2.7 * @package report_loglive * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_loglive_renderer extends plugin_renderer_base { /** * This method should never be manually called, it should only be called by process. * Please call the render method instead. * * @deprecated since 2.8, to be removed in 2.9 * @param report_loglive_renderable $reportloglive * @return string */ public function render_report_loglive_renderable(report_loglive_renderable $reportloglive) { debugging('Do not call this method. Please call $renderer->render($reportloglive) instead.', DEBUG_DEVELOPER); return $this->render($reportloglive); } /** * Return html to render the loglive page.. * * @param report_loglive_renderable $reportloglive object of report_log. * * @return string html used to render the page; */ protected function render_report_loglive(report_loglive_renderable $reportloglive) { if (empty($reportloglive->selectedlogreader)) { return $this->output->notification(get_string('nologreaderenabled', 'report_loglive'), 'notifyproblem'); } $table = $reportloglive->get_table(); return $this->render_table($table, $reportloglive->perpage); } /** * Prints/return reader selector * * @param report_loglive_renderable $reportloglive log report. * * @return string Returns rendered widget */ public function reader_selector(report_loglive_renderable $reportloglive) { $readers = $reportloglive->get_readers(true); if (count($readers) <= 1) { // One or no readers found, no need of this drop down. return ''; } $select = new single_select($reportloglive->url, 'logreader', $readers, $reportloglive->selectedlogreader, null); $select->set_label(get_string('selectlogreader', 'report_loglive')); return $this->output->render($select); } /** * Prints a button to update/resume live updates. * * @param report_loglive_renderable $reportloglive log report. * * @return string Returns rendered widget */ public function toggle_liveupdate_button(report_loglive_renderable $reportloglive) { // Add live log controls. if ($reportloglive->page == 0 && $reportloglive->selectedlogreader) { echo html_writer::tag('button' , get_string('pause', 'report_loglive'), array('id' => 'livelogs-pause-button', 'class' => 'btn btn-secondary')); $icon = new pix_icon('i/loading_small', 'loading', 'moodle', array('class' => 'spinner')); return html_writer::tag('span', $this->output->render($icon), array('class' => 'spinner')); } return ''; } /** * Get the html for the table. * * @param report_loglive_table_log $table table object. * @param int $perpage entries to display perpage. * * @return string table html */ protected function render_table(report_loglive_table_log $table, $perpage) { $o = ''; ob_start(); $table->out($perpage, true); $o = ob_get_contents(); ob_end_clean(); return $o; } } PK{\`Xloglive/classes/renderable.phpnu[. /** * Loglive report renderable class. * * @package report_loglive * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * Report loglive renderable class. * * @since Moodle 2.7 * @package report_loglive * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_loglive_renderable implements renderable { /** @var int number of seconds to show logs from, by default. */ const CUTOFF = 3600; /** @var \core\log\manager log manager */ protected $logmanager; /** @var string selected log reader pluginname */ public $selectedlogreader = null; /** @var int page number */ public $page; /** @var int perpage records to show */ public $perpage; /** @var stdClass course record */ public $course; /** @var moodle_url url of report page */ public $url; /** @var int selected date from which records should be displayed */ public $date; /** @var string order to sort */ public $order; /** @var int group id */ public $groupid; /** @var int forces the use of a course filter in site context */ public $sitecoursefilter; /** @var report_loglive_table_log table log which will be used for rendering logs */ public $tablelog; /** @var int refresh rate in seconds */ protected $refresh = 60; /** * Constructor. * * @param string $logreader (optional)reader pluginname from which logs will be fetched. * @param stdClass|int $course (optional) course record or id * @param moodle_url|string $url (optional) page url. * @param int $date date (optional) from which records will be fetched. * @param int $page (optional) page number. * @param int $perpage (optional) number of records to show per page. * @param string $order (optional) sortorder of fetched records * @param int $sitecoursefilter (optional) use a course filter in site context. */ public function __construct($logreader = "", $course = 0, $url = "", $date = 0, $page = 0, $perpage = 100, $order = "timecreated DESC", $sitecoursefilter = 0) { global $PAGE, $SITE; // Use first reader as selected reader, if not passed. if (empty($logreader)) { $readers = $this->get_readers(); if (!empty($readers)) { reset($readers); $logreader = key($readers); } else { $logreader = null; } } $this->selectedlogreader = $logreader; // Use page url if empty. if (empty($url)) { $url = new moodle_url($PAGE->url); } else { $url = new moodle_url($url); } $this->url = $url; // Use site course id, if course is empty. if (!empty($course) && is_int($course)) { $courseid = $course; try { $course = get_course($courseid); } catch (dml_missing_record_exception) { // Missing courses may have be deleted, so display them in site context. $course = 0; $sitecoursefilter = $courseid; } } $this->course = $course; if ($date == 0 ) { $date = time() - self::CUTOFF; } $this->date = $date; $this->page = $page; $this->perpage = $perpage; $this->order = $order; $this->sitecoursefilter = $sitecoursefilter; $this->set_refresh_rate(); } /** * Get a list of enabled sql_reader objects/name * * @param bool $nameonly if true only reader names will be returned. * * @return array core\log\sql_reader object or name. */ public function get_readers($nameonly = false) { if (!isset($this->logmanager)) { $this->logmanager = get_log_manager(); } $readers = $this->logmanager->get_readers('core\log\sql_reader'); if ($nameonly) { foreach ($readers as $pluginname => $reader) { $readers[$pluginname] = $reader->get_name(); } } return $readers; } /** * Setup table log. */ protected function setup_table() { $filter = $this->setup_filters(); $this->tablelog = new report_loglive_table_log('report_loglive', $filter); $this->tablelog->define_baseurl($this->url); } /** * Setup table log for ajax output. */ protected function setup_table_ajax() { $filter = $this->setup_filters(); $this->tablelog = new report_loglive_table_log_ajax('report_loglive', $filter); $this->tablelog->define_baseurl($this->url); } /** * Setup filters * * @return stdClass filters */ protected function setup_filters() { $readers = $this->get_readers(); // Set up filters. $filter = new \stdClass(); if (!empty($this->course)) { $filter->courseid = $this->course->id; $context = context_course::instance($filter->courseid); if (!has_capability('moodle/site:viewanonymousevents', $context)) { $filter->anonymous = 0; } } else { $filter->courseid = 0; } $filter->logreader = $readers[$this->selectedlogreader]; $filter->date = $this->date; $filter->orderby = $this->order; $filter->sitecoursefilter = $this->sitecoursefilter; return $filter; } /** * Set refresh rate of the live updates. */ protected function set_refresh_rate() { if (defined('BEHAT_SITE_RUNNING')) { // Hack for behat tests. $this->refresh = 5; } else { if (defined('REPORT_LOGLIVE_REFRESH')) { // Backward compatibility. $this->refresh = REPORT_LOGLIVE_REFRESH; } else { // Default. $this->refresh = 60; } } } /** * Get refresh rate of the live updates. */ public function get_refresh_rate() { return $this->refresh; } /** * Setup table and return it. * * @param bool $ajax If set to true report_loglive_table_log_ajax is set instead of report_loglive_table_log. * * @return report_loglive_table_log|report_loglive_table_log_ajax table object */ public function get_table($ajax = false) { if (empty($this->tablelog)) { if ($ajax) { $this->setup_table_ajax(); } else { $this->setup_table(); } } return $this->tablelog; } } PK{\$loglive/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_loglive. * * @package report_loglive * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_loglive\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_loglive implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\`c'loglive/classes/event/report_viewed.phpnu[. /** * The report_loglive report viewed event. * * @package report_loglive * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_loglive\event; defined('MOODLE_INTERNAL') || die(); /** * The report_loglive report viewed event class. * * @package report_loglive * @since Moodle 2.7 * @copyright 2013 Ankit Agarwal * @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() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventreportviewed', 'report_loglive'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the live log report for the course with id '$this->courseid'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/loglive/index.php', array('id' => $this->courseid)); } } PK{\ڜAAloglive/classes/table_log.phpnu[. /** * Table log for displaying logs. * * @package report_loglive * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; require_once($CFG->libdir . '/tablelib.php'); /** * Table log class for displaying logs. * * @since Moodle 2.7 * @package report_loglive * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_loglive_table_log extends table_sql { /** @var array list of user fullnames shown in report */ protected $userfullnames = array(); /** @var array list of course short names shown in report */ protected $courseshortnames = array(); /** @var array list of context name shown in report */ protected $contextname = array(); /** @var stdClass filters parameters */ protected $filterparams; /** @var int[] A list of users to filter by */ private ?array $lateuseridfilter = null; /** * Sets up the table_log parameters. * * @param string $uniqueid unique id of form. * @param stdClass $filterparams (optional) filter params. * - int courseid: id of course * - int userid: user id * - int|string modid: Module id or "site_errors" to view site errors * - int groupid: Group id * - \core\log\sql_reader logreader: reader from which data will be fetched. * - int edulevel: educational level. * - string action: view action * - int date: Date from which logs to be viewed. */ public function __construct($uniqueid, $filterparams = null) { parent::__construct($uniqueid); $this->set_attribute('class', 'reportloglive generaltable table-sm'); $this->set_attribute('aria-live', 'polite'); $this->filterparams = $filterparams; // Add course column if logs are displayed for site. $cols = array(); $headers = array(); if (empty($filterparams->courseid)) { $cols = array('course'); $headers = array(get_string('course')); } $this->define_columns(array_merge($cols, array('time', 'fullnameuser', 'relatedfullnameuser', 'context', 'component', 'eventname', 'description', 'origin', 'ip'))); $this->define_headers(array_merge($headers, array( get_string('time'), get_string('fullnameuser'), get_string('eventrelatedfullnameuser', 'report_loglive'), get_string('eventcontext', 'report_loglive'), get_string('eventcomponent', 'report_loglive'), get_string('eventname'), get_string('description'), get_string('eventorigin', 'report_loglive'), get_string('ip_address') ) )); $this->collapsible(false); $this->sortable(false); $this->pageable(true); $this->is_downloadable(false); } /** * Generate the course column. * * @param stdClass $event event data. * @return string HTML for the course column. */ public function col_course($event) { if (empty($event->courseid) || empty($this->courseshortnames[$event->courseid])) { return '-'; } else { return $this->courseshortnames[$event->courseid]; } } /** * Generate the time column. * * @param stdClass $event event data. * @return string HTML for the time column */ public function col_time($event) { $recenttimestr = get_string('strftimedatetimeaccurate', 'core_langconfig'); return userdate($event->timecreated, $recenttimestr); } /** * Generate the username column. * * @param stdClass $event event data. * @return string HTML for the username column */ public function col_fullnameuser($event) { // Get extra event data for origin and realuserid. $logextra = $event->get_logextra(); // Add username who did the action. if (!empty($logextra['realuserid'])) { $a = new stdClass(); $params = array('id' => $logextra['realuserid']); if ($event->courseid) { $params['course'] = $event->courseid; } $a->realusername = html_writer::link(new moodle_url("/user/view.php", $params), $this->userfullnames[$logextra['realuserid']]); $params['id'] = $event->userid; $a->asusername = html_writer::link(new moodle_url("/user/view.php", $params), $this->userfullnames[$event->userid]); $username = get_string('eventloggedas', 'report_loglive', $a); } else if (!empty($event->userid) && !empty($this->userfullnames[$event->userid])) { $params = array('id' => $event->userid); if ($event->courseid) { $params['course'] = $event->courseid; } $username = html_writer::link(new moodle_url("/user/view.php", $params), $this->userfullnames[$event->userid]); } else { $username = '-'; } return $username; } /** * Generate the related username column. * * @param stdClass $event event data. * @return string HTML for the related username column */ public function col_relatedfullnameuser($event) { // Add affected user. if (!empty($event->relateduserid) && isset($this->userfullnames[$event->relateduserid])) { $params = array('id' => $event->relateduserid); if ($event->courseid) { $params['course'] = $event->courseid; } return html_writer::link(new moodle_url("/user/view.php", $params), $this->userfullnames[$event->relateduserid]); } else { return '-'; } } /** * Generate the context column. * * @param stdClass $event event data. * @return string HTML for the context column */ public function col_context($event) { // Add context name. if ($event->contextid) { // If context name was fetched before then return, else get one. if (isset($this->contextname[$event->contextid])) { return $this->contextname[$event->contextid]; } else { $context = context::instance_by_id($event->contextid, IGNORE_MISSING); if ($context) { $contextname = $context->get_context_name(true); if ($url = $context->get_url()) { $contextname = html_writer::link($url, $contextname); } } else if (!$contextname = \report_log\helper::get_context_fallback($event)) { $contextname = get_string('other'); } } } else { $contextname = get_string('other'); } $this->contextname[$event->contextid] = $contextname; return $contextname; } /** * Generate the component column. * * @param stdClass $event event data. * @return string HTML for the component column */ public function col_component($event) { // Component. $componentname = $event->component; if (($event->component === 'core') || ($event->component === 'legacy')) { return get_string('coresystem'); } else if (get_string_manager()->string_exists('pluginname', $event->component)) { return get_string('pluginname', $event->component); } else { return $componentname; } } /** * Generate the event name column. * * @param stdClass $event event data. * @return string HTML for the event name column */ public function col_eventname($event) { $eventname = $event->get_name(); if ($url = $event->get_url()) { $eventname = $this->action_link($url, $eventname, 'action'); } return $eventname; } /** * Generate the description column. * * @param stdClass $event event data. * @return string HTML for the description column */ public function col_description($event) { return format_text($event->get_description(), FORMAT_PLAIN); } /** * Generate the origin column. * * @param stdClass $event event data. * @return string HTML for the origin column */ public function col_origin($event) { // Get extra event data for origin and realuserid. $logextra = $event->get_logextra(); // Add event origin, normally IP/cron. return $logextra['origin']; } /** * Generate the ip column. * * @param stdClass $event event data. * @return string HTML for the ip column */ public function col_ip($event) { // Get extra event data for origin and realuserid. $logextra = $event->get_logextra(); $url = new moodle_url("/iplookup/index.php?popup=1&ip={$logextra['ip']}&user=$event->userid"); return $this->action_link($url, $logextra['ip'], 'ip'); } /** * Method to create a link with popup action. * * @param moodle_url $url The url to open. * @param string $text Anchor text for the link. * @param string $name Name of the popup window. * * @return string html to use. */ protected function action_link(moodle_url $url, $text, $name = 'popup') { global $OUTPUT; $link = new action_link($url, $text, new popup_action('click', $url, $name, array('height' => 550, 'width' => 700))); return $OUTPUT->render($link); } /** * Query the reader. Store results in the object for use by build_table. * * @param int $pagesize size of page for paginated displayed table. * @param bool $useinitialsbar do you want to use the initials bar. */ public function query_db($pagesize, $useinitialsbar = true) { $joins = []; $params = []; if (!empty($this->filterparams->courseid)) { $joins[] = "courseid = :courseid"; $params['courseid'] = $this->filterparams->courseid; } // Add filters for missing/deleted courses in site context. if (!empty($this->filterparams->sitecoursefilter)) { $joins[] = "courseid = :courseid"; $params['courseid'] = $this->filterparams->sitecoursefilter; } // Getting all members of a group. [ 'joins' => $groupjoins, 'params' => $groupparams, 'useridfilter' => $this->lateuseridfilter, ] = \core\report_helper::get_group_filter($this->filterparams); $joins = array_merge($joins, $groupjoins); $params = array_merge($params, $groupparams); if (!empty($this->filterparams->date)) { $joins[] = "timecreated > :date"; $params['date'] = $this->filterparams->date; } if (isset($this->filterparams->anonymous)) { $joins[] = "anonymous = :anon"; $params['anon'] = $this->filterparams->anonymous; } $selector = implode(' AND ', $joins); $total = $this->filterparams->logreader->get_events_select_count($selector, $params); $this->pagesize($pagesize, $total); $this->rawdata = array_filter( $this->filterparams->logreader->get_events_select( $selector, $params, $this->filterparams->orderby, $this->get_page_start(), $this->get_page_size(), ), function($event) { if ($this->lateuseridfilter === null) { return true; } return isset($this->lateuseridfilter[$event->userid]); }, ); // Set initial bars. if ($useinitialsbar) { $this->initialbars($total > $pagesize); } // Update list of users and courses list which will be displayed on log page. $this->update_users_and_courses_used(); } /** * Helper function to create list of course shortname and user fullname shown in log report. * This will update $this->userfullnames and $this->courseshortnames array with userfullname and courseshortname (with link), * which will be used to render logs in table. */ public function update_users_and_courses_used() { global $SITE, $DB; $this->userfullnames = array(); $this->courseshortnames = array($SITE->id => $SITE->shortname); $userids = array(); $courseids = array(); // For each event cache full username and course. // Get list of userids and courseids which will be shown in log report. foreach ($this->rawdata as $event) { $logextra = $event->get_logextra(); if (!empty($event->userid) && !in_array($event->userid, $userids)) { $userids[] = $event->userid; } if (!empty($logextra['realuserid']) && !in_array($logextra['realuserid'], $userids)) { $userids[] = $logextra['realuserid']; } if (!empty($event->relateduserid) && !in_array($event->relateduserid, $userids)) { $userids[] = $event->relateduserid; } if (!empty($event->courseid) && ($event->courseid != $SITE->id) && !in_array($event->courseid, $courseids)) { $courseids[] = $event->courseid; } } // Get user fullname and put that in return list. if (!empty($userids)) { list($usql, $uparams) = $DB->get_in_or_equal($userids); $userfieldsapi = \core_user\fields::for_name(); $users = $DB->get_records_sql("SELECT id," . $userfieldsapi->get_sql('', false, '', '', false)->selects . " FROM {user} WHERE id " . $usql, $uparams); foreach ($users as $userid => $user) { $this->userfullnames[$userid] = fullname($user, has_capability('moodle/site:viewfullnames', $this->get_context())); } } // Get course shortname and put that in return list. if (!empty($courseids)) { // If all logs don't belog to site level then get course info. list($coursesql, $courseparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; $courseparams['contextlevel'] = CONTEXT_COURSE; $sql = "SELECT c.id,c.shortname $ccselect FROM {course} c $ccjoin WHERE c.id " . $coursesql; $courses = $DB->get_records_sql($sql, $courseparams); foreach ($courses as $courseid => $course) { $url = new moodle_url("/course/view.php", array('id' => $courseid)); context_helper::preload_from_record($course); $context = context_course::instance($courseid, IGNORE_MISSING); // Method format_string() takes care of missing contexts. $this->courseshortnames[$courseid] = html_writer::link($url, format_string($course->shortname, true, array('context' => $context))); } } } /** * Returns the latest timestamp of the records in the table. * * @return int */ public function get_until(): int { $until = $this->filterparams->date; if (!empty($this->rawdata)) { foreach ($this->rawdata as $row) { $until = max($row->timecreated, $until); } } return $until; } } PK{\MT}v v "loglive/classes/table_log_ajax.phpnu[. /** * Table log for generating data in ajax mode. * * @package report_loglive * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * Table log class for generating data in ajax mode. * * @since Moodle 2.7 * @package report_loglive * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_loglive_table_log_ajax extends report_loglive_table_log { /** * Convenience method to call a number of methods for you to display the * table. * * @param int $pagesize pagesize * @param bool $useinitialsbar Not used, present only for compatibility with parent. * @param string $downloadhelpbutton Not used, present only for compatibility with parent. * * @return string json encoded data containing html of new rows. */ public function out($pagesize, $useinitialsbar, $downloadhelpbutton = '') { $this->query_db($pagesize, false); $html = ''; if ($this->rawdata && $this->columns) { foreach ($this->rawdata as $row) { $formatedrow = $this->format_row($row); $formatedrow = $this->get_row_from_keyed($formatedrow); $html .= $this->get_row_html($formatedrow, 'newrow'); } } $result = array('logs' => $html, 'until' => $this->get_until()); return json_encode($result); } /** * Popup actions do not function when they are rendered in response to an AJAX request, encode within the link itself * * @param moodle_url $url * @param string $text * @param string $name * @return string */ protected function action_link(moodle_url $url, $text, $name = 'popup') { global $OUTPUT; $link = new action_link($url, $text, null, [ 'data-action' => 'action-popup', 'data-popup-action' => json_encode( new popup_action('click', $url, $name, ['height' => 440, 'width' => 700]) ), ]); return $OUTPUT->render($link); } } PK{\oHN!loglive/classes/renderer_ajax.phpnu[. /** * Log live report ajax renderer. * * @package report_loglive * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Log live report ajax renderer. * * @since Moodle 2.7 * @package report_loglive * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_loglive_renderer_ajax extends plugin_renderer_base { /** * This method should never be manually called, it should only be called by process. * Please call the render method instead. * * @deprecated since 2.8, to be removed in 2.9 * @param report_loglive_renderable $reportloglive * @return string */ public function render_report_loglive_renderable(report_loglive_renderable $reportloglive) { debugging('Do not call this method. Please call $renderer->render($reportloglive) instead.', DEBUG_DEVELOPER); return $this->render($reportloglive); } /** * Render logs for ajax. * * @param report_loglive_renderable $reportloglive object of report_loglive_renderable. * * @return string html to be displayed to user. */ protected function render_report_loglive(report_loglive_renderable $reportloglive) { if (empty($reportloglive->selectedlogreader)) { return null; } $table = $reportloglive->get_table(true); return $table->out($reportloglive->perpage, false); } } PK{\$loglive/yui/src/fetchlogs/build.jsonnu[{ "name": "moodle-report_loglive-fetchlogs", "builds": { "moodle-report_loglive-fetchlogs": { "jsfiles": [ "fetchlogs.js" ] } } } PK{\9u-loglive/yui/src/fetchlogs/meta/fetchlogs.jsonnu[{ "moodle-report_loglive-fetchlogs": { "requires": [ "base", "event", "node", "io", "node-event-delegate" ] } } PK{\4aa)loglive/yui/src/fetchlogs/js/fetchlogs.jsnu[/** * A module to manage ajax requests. * * @module moodle-report_loglive-fetchlogs */ /** * A module to manage ajax requests. * * @class moodle-report_loglive.fetchlogs * @extends Base * @constructor */ function FetchLogs() { FetchLogs.superclass.constructor.apply(this, arguments); } var CSS = { NEWROW: 'newrow', SPINNER: 'spinner', ICONSMALL: 'iconsmall' }, SELECTORS = { NEWROW: '.' + CSS.NEWROW, TBODY: '.flexible tbody', ACTIONLINK: '[data-action="action-popup"]', PAUSEBUTTON: '#livelogs-pause-button', SPINNER: '.' + CSS.SPINNER }; Y.extend(FetchLogs, Y.Base, { /** * Reference to the callBack object generated by Y.later * * @property callBack * @type Object * @default {} * @protected */ callBack: {}, /** * Reference to the spinner node. * * @property callBack * @type Object * @default {} * @protected */ spinner: {}, /** * Reference to the toggleButton node. * * @property pauseButton * @type Object * @default {} * @protected */ pauseButton: {}, /** * Initializer. * Basic setup and event listeners. * * @method initializer */ initializer: function() { // We don't want the pages to be updated when viewing a specific page in the chain. if (this.get('page') === 0) { this.callBack = Y.later(this.get('interval') * 1000, this, this.fetchRecentLogs, null, true); } this.spinner = Y.one(SELECTORS.SPINNER); this.pauseButton = Y.one(SELECTORS.PAUSEBUTTON); this.spinner.hide(); Y.one(SELECTORS.TBODY).delegate('click', this.openActionLink, SELECTORS.ACTIONLINK, this); Y.one(SELECTORS.PAUSEBUTTON).on('click', this.toggleUpdate, this); }, /** * Method to fetch recent logs. * * @method fetchRecentLogs */ fetchRecentLogs: function() { this.spinner.show(); // Show a loading icon. var data = { logreader: this.get('logreader'), since: this.get('since'), page: this.get('page'), id: this.get('courseid') }; var cfg = { method: 'get', context: this, on: { complete: this.updateLogTable }, data: data }; var url = M.cfg.wwwroot + '/report/loglive/loglive_ajax.php'; Y.io(url, cfg); }, /** * Method to update the log table, populate the table with new entries and remove old entries if needed. * * @method updateLogTable */ updateLogTable: function(tid, response) { // Hide loading icon, give sometime to people to actually see it. We should do it, event in case of an error. Y.later(600, this, 'hideLoadingIcon'); var responseobject; // Attempt to parse the response into an object. try { responseobject = Y.JSON.parse(response.responseText); if (responseobject.error) { Y.use('moodle-core-notification-ajaxexception', function() { return new M.core.ajaxException(responseobject); }); return this; } } catch (error) { Y.use('moodle-core-notification-exception', function() { return new M.core.exception(error); }); return this; } this.set('since', responseobject.until); var logs = responseobject.logs; var tbody = Y.one(SELECTORS.TBODY); var firstTr = null; if (tbody && logs) { firstTr = tbody.get('firstChild'); if (firstTr) { tbody.insertBefore(logs, firstTr); } // @Todo, when no data is present our library should generate an empty table. so data can be added // dynamically (MDL-44525). // Let us chop off some data from end of table to prevent really long pages. var oldChildren = tbody.get('children').slice(this.get('perpage')); oldChildren.remove(); Y.later(5000, this, 'removeHighlight'); // Remove highlighting from new rows. } }, /** * Remove background highlight from the newly added rows. * * @method removeHighlight */ removeHighlight: function() { Y.all(SELECTORS.NEWROW).removeClass(CSS.NEWROW); }, /** * Hide the spinning icon. * * @method hideLoadingIcon */ hideLoadingIcon: function() { this.spinner.hide(); }, /** * Open a report action link * * @param {Event} event * @method openActionLink */ openActionLink: function(event) { var popupAction = JSON.parse(event.target.get('dataset').popupAction); window.openpopup(event, popupAction.jsfunctionargs); }, /** * Toggle live update. * * @method toggleUpdate */ toggleUpdate: function() { if (this.callBack) { this.callBack.cancel(); this.callBack = ''; this.pauseButton.setContent(M.util.get_string('resume', 'report_loglive')); } else { this.callBack = Y.later(this.get('interval') * 1000, this, this.fetchRecentLogs, null, true); this.pauseButton.setContent(M.util.get_string('pause', 'report_loglive')); } } }, { NAME: 'fetchLogs', ATTRS: { /** * time stamp from where the new logs needs to be fetched. * * @attribute since * @default null * @type String */ since: null, /** * courseid for which the logs are shown. * * @attribute courseid * @default 0 * @type int */ courseid: 0, /** * Page number shown to user. * * @attribute page * @default 0 * @type int */ page: 0, /** * Items to show per page. * * @attribute perpage * @default 100 * @type int */ perpage: 100, /** * Refresh interval. * * @attribute interval * @default 60 * @type int */ interval: 60, /** * Which logstore is being used. * * @attribute logreader * @default logstore_standard * @type String */ logreader: 'logstore_standard' } }); Y.namespace('M.report_loglive.FetchLogs').init = function(config) { return new FetchLogs(config); }; PK{\/Zloglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs-debug.jsnu[YUI.add('moodle-report_loglive-fetchlogs', function (Y, NAME) { /** * A module to manage ajax requests. * * @module moodle-report_loglive-fetchlogs */ /** * A module to manage ajax requests. * * @class moodle-report_loglive.fetchlogs * @extends Base * @constructor */ function FetchLogs() { FetchLogs.superclass.constructor.apply(this, arguments); } var CSS = { NEWROW: 'newrow', SPINNER: 'spinner', ICONSMALL: 'iconsmall' }, SELECTORS = { NEWROW: '.' + CSS.NEWROW, TBODY: '.flexible tbody', ACTIONLINK: '[data-action="action-popup"]', PAUSEBUTTON: '#livelogs-pause-button', SPINNER: '.' + CSS.SPINNER }; Y.extend(FetchLogs, Y.Base, { /** * Reference to the callBack object generated by Y.later * * @property callBack * @type Object * @default {} * @protected */ callBack: {}, /** * Reference to the spinner node. * * @property callBack * @type Object * @default {} * @protected */ spinner: {}, /** * Reference to the toggleButton node. * * @property pauseButton * @type Object * @default {} * @protected */ pauseButton: {}, /** * Initializer. * Basic setup and event listeners. * * @method initializer */ initializer: function() { // We don't want the pages to be updated when viewing a specific page in the chain. if (this.get('page') === 0) { this.callBack = Y.later(this.get('interval') * 1000, this, this.fetchRecentLogs, null, true); } this.spinner = Y.one(SELECTORS.SPINNER); this.pauseButton = Y.one(SELECTORS.PAUSEBUTTON); this.spinner.hide(); Y.one(SELECTORS.TBODY).delegate('click', this.openActionLink, SELECTORS.ACTIONLINK, this); Y.one(SELECTORS.PAUSEBUTTON).on('click', this.toggleUpdate, this); }, /** * Method to fetch recent logs. * * @method fetchRecentLogs */ fetchRecentLogs: function() { this.spinner.show(); // Show a loading icon. var data = { logreader: this.get('logreader'), since: this.get('since'), page: this.get('page'), id: this.get('courseid') }; var cfg = { method: 'get', context: this, on: { complete: this.updateLogTable }, data: data }; var url = M.cfg.wwwroot + '/report/loglive/loglive_ajax.php'; Y.io(url, cfg); }, /** * Method to update the log table, populate the table with new entries and remove old entries if needed. * * @method updateLogTable */ updateLogTable: function(tid, response) { // Hide loading icon, give sometime to people to actually see it. We should do it, event in case of an error. Y.later(600, this, 'hideLoadingIcon'); var responseobject; // Attempt to parse the response into an object. try { responseobject = Y.JSON.parse(response.responseText); if (responseobject.error) { Y.use('moodle-core-notification-ajaxexception', function() { return new M.core.ajaxException(responseobject); }); return this; } } catch (error) { Y.use('moodle-core-notification-exception', function() { return new M.core.exception(error); }); return this; } this.set('since', responseobject.until); var logs = responseobject.logs; var tbody = Y.one(SELECTORS.TBODY); var firstTr = null; if (tbody && logs) { firstTr = tbody.get('firstChild'); if (firstTr) { tbody.insertBefore(logs, firstTr); } // @Todo, when no data is present our library should generate an empty table. so data can be added // dynamically (MDL-44525). // Let us chop off some data from end of table to prevent really long pages. var oldChildren = tbody.get('children').slice(this.get('perpage')); oldChildren.remove(); Y.later(5000, this, 'removeHighlight'); // Remove highlighting from new rows. } }, /** * Remove background highlight from the newly added rows. * * @method removeHighlight */ removeHighlight: function() { Y.all(SELECTORS.NEWROW).removeClass(CSS.NEWROW); }, /** * Hide the spinning icon. * * @method hideLoadingIcon */ hideLoadingIcon: function() { this.spinner.hide(); }, /** * Open a report action link * * @param {Event} event * @method openActionLink */ openActionLink: function(event) { var popupAction = JSON.parse(event.target.get('dataset').popupAction); window.openpopup(event, popupAction.jsfunctionargs); }, /** * Toggle live update. * * @method toggleUpdate */ toggleUpdate: function() { if (this.callBack) { this.callBack.cancel(); this.callBack = ''; this.pauseButton.setContent(M.util.get_string('resume', 'report_loglive')); } else { this.callBack = Y.later(this.get('interval') * 1000, this, this.fetchRecentLogs, null, true); this.pauseButton.setContent(M.util.get_string('pause', 'report_loglive')); } } }, { NAME: 'fetchLogs', ATTRS: { /** * time stamp from where the new logs needs to be fetched. * * @attribute since * @default null * @type String */ since: null, /** * courseid for which the logs are shown. * * @attribute courseid * @default 0 * @type int */ courseid: 0, /** * Page number shown to user. * * @attribute page * @default 0 * @type int */ page: 0, /** * Items to show per page. * * @attribute perpage * @default 100 * @type int */ perpage: 100, /** * Refresh interval. * * @attribute interval * @default 60 * @type int */ interval: 60, /** * Which logstore is being used. * * @attribute logreader * @default logstore_standard * @type String */ logreader: 'logstore_standard' } }); Y.namespace('M.report_loglive.FetchLogs').init = function(config) { return new FetchLogs(config); }; }, '@VERSION@', {"requires": ["base", "event", "node", "io", "node-event-delegate"]}); PK{\/Tloglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs.jsnu[YUI.add('moodle-report_loglive-fetchlogs', function (Y, NAME) { /** * A module to manage ajax requests. * * @module moodle-report_loglive-fetchlogs */ /** * A module to manage ajax requests. * * @class moodle-report_loglive.fetchlogs * @extends Base * @constructor */ function FetchLogs() { FetchLogs.superclass.constructor.apply(this, arguments); } var CSS = { NEWROW: 'newrow', SPINNER: 'spinner', ICONSMALL: 'iconsmall' }, SELECTORS = { NEWROW: '.' + CSS.NEWROW, TBODY: '.flexible tbody', ACTIONLINK: '[data-action="action-popup"]', PAUSEBUTTON: '#livelogs-pause-button', SPINNER: '.' + CSS.SPINNER }; Y.extend(FetchLogs, Y.Base, { /** * Reference to the callBack object generated by Y.later * * @property callBack * @type Object * @default {} * @protected */ callBack: {}, /** * Reference to the spinner node. * * @property callBack * @type Object * @default {} * @protected */ spinner: {}, /** * Reference to the toggleButton node. * * @property pauseButton * @type Object * @default {} * @protected */ pauseButton: {}, /** * Initializer. * Basic setup and event listeners. * * @method initializer */ initializer: function() { // We don't want the pages to be updated when viewing a specific page in the chain. if (this.get('page') === 0) { this.callBack = Y.later(this.get('interval') * 1000, this, this.fetchRecentLogs, null, true); } this.spinner = Y.one(SELECTORS.SPINNER); this.pauseButton = Y.one(SELECTORS.PAUSEBUTTON); this.spinner.hide(); Y.one(SELECTORS.TBODY).delegate('click', this.openActionLink, SELECTORS.ACTIONLINK, this); Y.one(SELECTORS.PAUSEBUTTON).on('click', this.toggleUpdate, this); }, /** * Method to fetch recent logs. * * @method fetchRecentLogs */ fetchRecentLogs: function() { this.spinner.show(); // Show a loading icon. var data = { logreader: this.get('logreader'), since: this.get('since'), page: this.get('page'), id: this.get('courseid') }; var cfg = { method: 'get', context: this, on: { complete: this.updateLogTable }, data: data }; var url = M.cfg.wwwroot + '/report/loglive/loglive_ajax.php'; Y.io(url, cfg); }, /** * Method to update the log table, populate the table with new entries and remove old entries if needed. * * @method updateLogTable */ updateLogTable: function(tid, response) { // Hide loading icon, give sometime to people to actually see it. We should do it, event in case of an error. Y.later(600, this, 'hideLoadingIcon'); var responseobject; // Attempt to parse the response into an object. try { responseobject = Y.JSON.parse(response.responseText); if (responseobject.error) { Y.use('moodle-core-notification-ajaxexception', function() { return new M.core.ajaxException(responseobject); }); return this; } } catch (error) { Y.use('moodle-core-notification-exception', function() { return new M.core.exception(error); }); return this; } this.set('since', responseobject.until); var logs = responseobject.logs; var tbody = Y.one(SELECTORS.TBODY); var firstTr = null; if (tbody && logs) { firstTr = tbody.get('firstChild'); if (firstTr) { tbody.insertBefore(logs, firstTr); } // @Todo, when no data is present our library should generate an empty table. so data can be added // dynamically (MDL-44525). // Let us chop off some data from end of table to prevent really long pages. var oldChildren = tbody.get('children').slice(this.get('perpage')); oldChildren.remove(); Y.later(5000, this, 'removeHighlight'); // Remove highlighting from new rows. } }, /** * Remove background highlight from the newly added rows. * * @method removeHighlight */ removeHighlight: function() { Y.all(SELECTORS.NEWROW).removeClass(CSS.NEWROW); }, /** * Hide the spinning icon. * * @method hideLoadingIcon */ hideLoadingIcon: function() { this.spinner.hide(); }, /** * Open a report action link * * @param {Event} event * @method openActionLink */ openActionLink: function(event) { var popupAction = JSON.parse(event.target.get('dataset').popupAction); window.openpopup(event, popupAction.jsfunctionargs); }, /** * Toggle live update. * * @method toggleUpdate */ toggleUpdate: function() { if (this.callBack) { this.callBack.cancel(); this.callBack = ''; this.pauseButton.setContent(M.util.get_string('resume', 'report_loglive')); } else { this.callBack = Y.later(this.get('interval') * 1000, this, this.fetchRecentLogs, null, true); this.pauseButton.setContent(M.util.get_string('pause', 'report_loglive')); } } }, { NAME: 'fetchLogs', ATTRS: { /** * time stamp from where the new logs needs to be fetched. * * @attribute since * @default null * @type String */ since: null, /** * courseid for which the logs are shown. * * @attribute courseid * @default 0 * @type int */ courseid: 0, /** * Page number shown to user. * * @attribute page * @default 0 * @type int */ page: 0, /** * Items to show per page. * * @attribute perpage * @default 100 * @type int */ perpage: 100, /** * Refresh interval. * * @attribute interval * @default 60 * @type int */ interval: 60, /** * Which logstore is being used. * * @attribute logreader * @default logstore_standard * @type String */ logreader: 'logstore_standard' } }); Y.namespace('M.report_loglive.FetchLogs').init = function(config) { return new FetchLogs(config); }; }, '@VERSION@', {"requires": ["base", "event", "node", "io", "node-event-delegate"]}); PK{\LXloglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs-min.jsnu[YUI.add("moodle-report_loglive-fetchlogs",function(a,e){function t(){t.superclass.constructor.apply(this,arguments)}var i="newrow",r={NEWROW:"."+i,TBODY:".flexible tbody",ACTIONLINK:'[data-action="action-popup"]',PAUSEBUTTON:"#livelogs-pause-button",SPINNER:"."+"spinner"};a.extend(t,a.Base,{callBack:{},spinner:{},pauseButton:{},initializer:function(){0===this.get("page")&&(this.callBack=a.later(1e3*this.get("interval"),this,this.fetchRecentLogs,null,!0)),this.spinner=a.one(r.SPINNER),this.pauseButton=a.one(r.PAUSEBUTTON),this.spinner.hide(),a.one(r.TBODY).delegate("click",this.openActionLink,r.ACTIONLINK,this),a.one(r.PAUSEBUTTON).on("click",this.toggleUpdate,this)},fetchRecentLogs:function(){var e,t;this.spinner.show(),e={logreader:this.get("logreader"),since:this.get("since"),page:this.get("page"),id:this.get("courseid")},e={method:"get",context:this,on:{complete:this.updateLogTable},data:e},t=M.cfg.wwwroot+"/report/loglive/loglive_ajax.php",a.io(t,e)},updateLogTable:function(e,t){var i,n,o;a.later(600,this,"hideLoadingIcon");try{if((i=a.JSON.parse(t.responseText)).error)return a.use("moodle-core-notification-ajaxexception",function(){return new M.core.ajaxException(i)}),this}catch(s){return a.use("moodle-core-notification-exception",function(){return new M.core.exception(s)}),this}this.set("since",i.until),t=i.logs,(n=a.one(r.TBODY))&&t&&((o=n.get("firstChild"))&&n.insertBefore(t,o),n.get("children").slice(this.get("perpage")).remove(),a.later(5e3,this,"removeHighlight"))},removeHighlight:function(){a.all(r.NEWROW).removeClass(i)},hideLoadingIcon:function(){this.spinner.hide()},openActionLink:function(e){var t=JSON.parse(e.target.get("dataset").popupAction);window.openpopup(e,t.jsfunctionargs)},toggleUpdate:function(){this.callBack?(this.callBack.cancel(),this.callBack="",this.pauseButton.setContent(M.util.get_string("resume","report_loglive"))):(this.callBack=a.later(1e3*this.get("interval"),this,this.fetchRecentLogs,null,!0),this.pauseButton.setContent(M.util.get_string("pause","report_loglive")))}},{NAME:"fetchLogs",ATTRS:{since:null,courseid:0,page:0,perpage:100,interval:60,logreader:"logstore_standard"}}),a.namespace("M.report_loglive.FetchLogs").init=function(e){return new t(e)}},"@VERSION@",{requires:["base","event","node","io","node-event-delegate"]});PK{\Em,llloglive/index.phpnu[. /** * Displays live view of recent logs * * This file generates live view of recent logs. * * @package report_loglive * @copyright 2011 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\report_helper; require('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/course/lib.php'); global $SITE, $PAGE; $id = optional_param('id', 0, PARAM_INT); $page = optional_param('page', 0, PARAM_INT); $logreader = optional_param('logreader', '', PARAM_COMPONENT); // Reader which will be used for displaying logs. // Get course details. if (!empty($id)) { $course = $DB->get_record('course', ['id' => $id], '*'); if ($course) { require_login($course); $context = context_course::instance($course->id); $coursename = format_string($course->fullname, true, ['context' => $context]); } } if (empty($course)) { admin_externalpage_setup('reportloglive', '', null, '', ['pagelayout' => 'report']); $context = context_system::instance(); $coursename = format_string($SITE->fullname, true, ['context' => $context]); } require_capability('report/loglive:view', $context); $params = array(); if ($id != 0) { $params['id'] = $id; } if ($page != 0) { $params['page'] = $page; } if ($logreader !== '') { $params['logreader'] = $logreader; } $url = new moodle_url("/report/loglive/index.php", $params); $PAGE->set_url($url); $PAGE->set_pagelayout('report'); $PAGE->set_context($context); $strlivelogs = get_string('livelogs', 'report_loglive'); $PAGE->set_title("$coursename: $strlivelogs"); $output = $PAGE->get_renderer('report_loglive'); echo $output->header(); if (!report_helper::has_valid_group($context)) { echo $output->notification(get_string('notingroup')); echo $output->footer(); exit(); } $renderable = new report_loglive_renderable($logreader, $id, $url, 0, $page); $refresh = $renderable->get_refresh_rate(); $logreader = $renderable->selectedlogreader; $strupdatesevery = get_string('updatesevery', 'moodle', $refresh); $PAGE->set_heading($coursename); // Print selector dropdown. $pluginname = get_string('pluginname', 'report_loglive'); report_helper::print_report_selector($pluginname); echo html_writer::div(get_string('livelogswithupdate', 'report_loglive', $strupdatesevery), 'mb-3'); echo $output->reader_selector($renderable); echo $output->toggle_liveupdate_button($renderable); echo $output->render($renderable); // Include and trigger ajax requests. if ($page == 0 && !empty($logreader)) { // Tell Js to fetch new logs only, by passing the latest timestamp of records in the table. $until = $renderable->get_table()->get_until(); $jsparams = array('since' => $until , 'courseid' => $id, 'page' => $page, 'logreader' => $logreader, 'interval' => $refresh, 'perpage' => $renderable->perpage); $PAGE->requires->strings_for_js(array('pause', 'resume'), 'report_loglive'); $PAGE->requires->yui_module('moodle-report_loglive-fetchlogs', 'Y.M.report_loglive.FetchLogs.init', array($jsparams)); } // Trigger a logs viewed event. $event = \report_loglive\event\report_viewed::create(array('context' => $context)); $event->trigger(); echo $output->footer(); PK{\xxloglive/loglive_ajax.phpnu[. /** * Ajax responder page. * * @package report_loglive * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('AJAX_SCRIPT', true); require_once('../../config.php'); $id = optional_param('id', 0, PARAM_INT); $page = optional_param('page', 0, PARAM_INT); $since = optional_param('since', 0, PARAM_INT); $logreader = optional_param('logreader', '', PARAM_COMPONENT); // Reader which will be used for displaying logs. $PAGE->set_url('/report/loglive/loglive_ajax.php'); // Capability checks. if (!empty($id)) { $course = $DB->get_record('course', ['id' => $id], '*'); if ($course) { require_login($course); $context = context_course::instance($course->id); } } if (empty($course)) { require_login(); $context = context_system::instance(); $PAGE->set_context($context); } require_capability('report/loglive:view', $context); if (!$since) { echo $since = $since - report_loglive_renderable::CUTOFF; } $renderable = new report_loglive_renderable($logreader, $id, '', $since, $page); $output = $PAGE->get_renderer('report_loglive'); echo $output->render($renderable); PK{\wi55 loglive/tests/table_log_test.phpnu[. namespace report_loglive; use advanced_testcase; use context_course; use core_user; /** * Tests for table log and groups. * * @package report_loglive * @copyright 2024 onwards Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class table_log_test extends advanced_testcase { /** * @var int The course with separate groups. */ const COURSE_SEPARATE_GROUP = 0; /** * @var int The course with separate groups. */ const COURSE_VISIBLE_GROUP = 1; /** * @var int The course with separate groups. */ const COURSE_NO_GROUP = 2; /** * @var array The setup of users. */ const SETUP_USER_DEFS = [ // Make student2 also member of group1. 'student' => [ 'student0' => ['group0'], 'student1' => ['group1'], 'student2' => ['group0', 'group1'], 'student3' => [], ], // Make teacher2 also member of group1. 'teacher' => [ 'teacher0' => ['group0'], 'teacher1' => ['group1'], 'teacher2' => ['group0', 'group1'], 'teacher3' => [], ], // Make editingteacher also member of group1. 'editingteacher' => [ 'editingteacher0' => ['group0'], 'editingteacher1' => ['group1'], 'editingteacher2' => ['group0', 'group1'], ], ]; /** * @var array|\stdClass all users indexed by username. */ private $users = []; /** * @var array The groups by courses (array of array). */ private $groupsbycourse = []; /** * @var array The courses. */ private $courses; /** * Data provider for test_get_table_logs. * * @return array */ public static function get_report_logs_provider(): array { return [ 'separategroups: student 0' => [ self::COURSE_SEPARATE_GROUP, 'student0', // All users in group 0. [ 'student0', 'student2', 'teacher0', 'teacher2', 'editingteacher0', 'editingteacher2', ], ], 'separategroups: student 1' => [ self::COURSE_SEPARATE_GROUP, 'student1', // All users in group1. [ 'student1', 'student2', 'teacher1', 'teacher2', 'editingteacher1', 'editingteacher2', ], ], 'separategroups: student 2' => [ self::COURSE_SEPARATE_GROUP, 'student2', // All users in group0 and group1. [ 'student0', 'student1', 'student2', 'teacher0', 'teacher1', 'teacher2', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'separategroups: student3' => [ self::COURSE_SEPARATE_GROUP, 'student3', // Student 3 is not in any group so should only see user without a group. [ 'student3', 'teacher3', ], ], 'separategroups: editing teacher 0' => [ self::COURSE_SEPARATE_GROUP, 'editingteacher0', // All users including student 3 as we can see all users (event the one not in a group). [ 'student0', 'student1', 'student2', 'student3', 'teacher0', 'teacher1', 'teacher2', 'teacher3', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'separategroups: teacher 0' => [ self::COURSE_SEPARATE_GROUP, 'teacher0', // All users in group 0. [ 'student0', 'student2', 'teacher0', 'teacher2', 'editingteacher0', 'editingteacher2', ], ], 'separategroups: teacher 2' => [ self::COURSE_SEPARATE_GROUP, 'teacher2', // All users in group0 and group1. [ 'student0', 'student1', 'student2', 'teacher0', 'teacher1', 'teacher2', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'separategroups: teacher 3' => [ self::COURSE_SEPARATE_GROUP, 'teacher3', // Teacher 3 is not in any group so should only see user without a group. [ 'student3', 'teacher3', ], ], 'visiblegroup: editing teacher' => [ self::COURSE_VISIBLE_GROUP, 'editingteacher0', // All users. [ 'student0', 'student1', 'student2', 'student3', 'teacher0', 'teacher1', 'teacher2', 'teacher3', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'visiblegroup: student' => [ self::COURSE_VISIBLE_GROUP, 'student0', // All users. [ 'student0', 'student1', 'student2', 'student3', 'teacher0', 'teacher1', 'teacher2', 'teacher3', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'nogroup: teacher 0' => [ self::COURSE_VISIBLE_GROUP, 'teacher2', // All users. [ 'student0', 'student1', 'student2', 'student3', 'teacher0', 'teacher1', 'teacher2', 'teacher3', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'nogroup: editing teacher 0' => [ self::COURSE_VISIBLE_GROUP, 'editingteacher0', // All users. [ 'student0', 'student1', 'student2', 'student3', 'teacher0', 'teacher1', 'teacher2', 'teacher3', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], 'nogroup: student' => [ self::COURSE_VISIBLE_GROUP, 'student0', // All users. [ 'student0', 'student1', 'student2', 'student3', 'teacher0', 'teacher1', 'teacher2', 'teacher3', 'editingteacher0', 'editingteacher1', 'editingteacher2', ], ], ]; } /** * Set up a course with two groups, three students being each in one of the groups, * two teachers each in either group while the second teacher is also member of the other group. * * @return void * @throws \coding_exception */ public function setUp(): void { global $DB; parent::setUp(); $this->resetAfterTest(); $this->preventResetByRollback(); // This is to ensure that we can actually trigger event and record them in the log store. $this->courses[self::COURSE_SEPARATE_GROUP] = $this->getDataGenerator()->create_course(['groupmode' => SEPARATEGROUPS]); $this->courses[self::COURSE_VISIBLE_GROUP] = $this->getDataGenerator()->create_course(['groupmode' => VISIBLEGROUPS]); $this->courses[self::COURSE_NO_GROUP] = $this->getDataGenerator()->create_course(); foreach ($this->courses as $coursetype => $course) { if ($coursetype == self::COURSE_NO_GROUP) { continue; } $this->groupsbycourse[$coursetype] = []; $this->groupsbycourse[$coursetype]['group0'] = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'group0']); $this->groupsbycourse[$coursetype]['group1'] = $this->getDataGenerator()->create_group(['courseid' => $course->id, 'name' => 'group1']); } foreach (self::SETUP_USER_DEFS as $role => $userdefs) { foreach ($userdefs as $username => $groups) { $user = $this->getDataGenerator()->create_user( [ 'username' => $username, 'firstname' => "FN{$role}{$username}", 'lastname' => "LN{$role}{$username}", ]); foreach ($this->courses as $coursetype => $course) { $this->getDataGenerator()->enrol_user($user->id, $course->id, $role); foreach ($groups as $groupname) { if ($coursetype == self::COURSE_NO_GROUP) { continue; } $this->getDataGenerator()->create_group_member([ 'groupid' => $this->groupsbycourse[$coursetype][$groupname]->id, 'userid' => $user->id, ]); } } $this->users[$username] = $user; } } // Configure log store. set_config('enabled_stores', 'logstore_standard', 'tool_log'); get_log_manager(true); $DB->delete_records('logstore_standard_log'); foreach ($this->courses as $course) { foreach ($this->users as $user) { $eventdata = [ 'context' => context_course::instance($course->id), 'userid' => $user->id, ]; $event = \core\event\course_viewed::create($eventdata); $event->trigger(); } } } /** * Test table_log * * @param int $courseindex * @param string $username * @param array $expectedusers * @covers \report_log_renderable::get_user_list * @dataProvider get_report_logs_provider * @return void */ public function test_get_table_logs(int $courseindex, string $username, array $expectedusers): void { $manager = get_log_manager(); $stores = $manager->get_readers(); $store = $stores['logstore_standard']; // Build the report. $url = new \moodle_url("/report/loglive/index.php"); $renderable = new \report_loglive_renderable('logstore_standard', $this->courses[$courseindex], $url); $table = $renderable->get_table(); $currentuser = $this->users[$username]; $this->setUser($currentuser->id); $store->flush(); $table->query_db(100); $filteredevents = array_filter( $table->rawdata, fn($event) => get_class($event) === \core\event\course_viewed::class ); $usernames = array_map( function($event) { $user = core_user::get_user($event->userid, '*', MUST_EXIST); return $user->username; }, $filteredevents); sort($expectedusers); sort($usernames); $this->assertEquals($expectedusers, $usernames); } /** * Test getting logs for deleted courses. * * @covers \report_loglive_renderable::get_table * @return void */ public function test_get_deleted_course_logs(): void { global $DB, $PAGE; $this->preventResetByRollback(); // Ensure events can be recorded in log store. // Configure log store and user. $manager = get_log_manager(); $stores = $manager->get_readers(); $store = $stores['logstore_standard']; $this->setUser(get_admin()); // Set and delete course. $course = reset($this->courses); $deletedcourseid = $course->id; delete_course($course, false); $this->assertFalse($DB->record_exists('course', ['id' => $deletedcourseid])); // Test rendering. $PAGE->set_url('/report/loglive/index.php?id=' . $deletedcourseid); $renderable = new \report_loglive_renderable('', (int) $deletedcourseid); $table = $renderable->get_table(); $store->flush(); $table->query_db(100); // Confirm we have logs for the course deletion and that the filtering is correct. $this->assertNotEmpty($table->totalrows); $expectedrows = count($DB->get_records('logstore_standard_log', ['courseid' => $deletedcourseid])); $this->assertEquals($expectedrows, $table->totalrows); } } PK{\'[ [ loglive/tests/lib_test.phpnu[. /** * Tests for report library functions. * * @package report_loglive * @copyright 2014 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace report_loglive; defined('MOODLE_INTERNAL') || die(); /** * Class report_loglive_lib_testcase * * @package report_loglive * @copyright 2014 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class lib_test extends \advanced_testcase { /** * Test report_log_supports_logstore. */ public function test_report_participation_supports_logstore(): void { $logmanager = get_log_manager(); $allstores = \core_component::get_plugin_list_with_class('logstore', 'log\store'); $supportedstores = array( 'logstore_standard' => '\logstore_standard\log\store' ); // Make sure all supported stores are installed. $expectedstores = array_keys(array_intersect($allstores, $supportedstores)); $stores = $logmanager->get_supported_logstores('report_loglive'); $stores = array_keys($stores); foreach ($expectedstores as $expectedstore) { $this->assertContains($expectedstore, $stores); } } /** * Test the latest record timestamp of the report data set. * * @covers ::get_until() */ public function test_report_get_until(): void { global $DB; $this->resetAfterTest(); $this->preventResetByRollback(); $now = time(); // Configure log store. set_config('enabled_stores', 'logstore_standard', 'tool_log'); $manager = get_log_manager(); $stores = $manager->get_readers(); $store = $stores['logstore_standard']; $DB->delete_records('logstore_standard_log'); // Build the report. $url = new \moodle_url("/report/loglive/index.php"); $renderable = new \report_loglive_renderable('logstore_standard', 0, $url); $table = $renderable->get_table(); $table->query_db(100); $until = $table->get_until(); // There is no record in the log table at this stage so until date is supposed to be equal to CUTOFF date. $this->assertLessThanOrEqual(time() - \report_loglive_renderable::CUTOFF, $until); // Create a user, store the event and re-build the report. $this->getDataGenerator()->create_user(); $store->flush(); $table->query_db(100); $until = $table->get_until(); // Assert that until date reflects user creation event date (now). $this->assertGreaterThanOrEqual($now, $until); } } PK{\Ѥ e #loglive/tests/event/events_test.phpnu[. /** * Tests for report log live events. * * @package report_loglive * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace report_loglive\event; /** * Class report_loglive_events_testcase * * Class for tests related to log live events. * * @package report_loglive * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class events_test extends \advanced_testcase { /** * Setup testcase. */ public function setUp(): void { parent::setUp(); $this->setAdminUser(); $this->resetAfterTest(); } /** * Test the report viewed event. * * It's not possible to use the moodle API to simulate the viewing of log report, so here we * simply create the event and trigger it. */ public function test_report_viewed(): void { $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Trigger event for loglive report viewed. $event = \report_loglive\event\report_viewed::create(array('context' => $context)); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $event = reset($events); $this->assertInstanceOf('\report_loglive\event\report_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); $url = new \moodle_url('/report/loglive/index.php', array('id' => $course->id)); $this->assertEquals($url, $event->get_url()); } } PK{\F *loglive/tests/behat/loglive_report.featurenu[@report @report_loglive Feature: In a report, admin can see loglive data In order see loglive data As an admin I need to view loglive report and see if the live update feature works Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | idnumber | middlename | alternatename | firstnamephonetic | lastnamephonetic | | student1 | Grainne | Beauchamp | student1@example.com | s1 | Ann | Jill | Gronya | Beecham | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | And the following config values are set as admin: | fullnamedisplay | firstname | | alternativefullnameformat | middlename, alternatename, firstname, lastname | And I log in as "admin" And I add a data activity to course "Course 1" section "3" and I fill the form with: | Name | Test name | | Description | Test database description | @javascript Scenario: Check loglive report entries and make sure the report works for standard reader Given I navigate to "Reports > Live logs" in site administration When I should see "Course module created" Then I should see "Test name" @javascript @_switch_window Scenario: Check loglive report entries and make sure the pause/resume button works for standard reader along with ajax calls Given I am on site homepage When I navigate to "Reports > Live logs" in site administration And I should not see "Test name2" And I press "Pause live updates" And I follow "Course module created" And I switch to "action" window And I am on "Course 1" course homepage And I change window size to "large" And I reload the page And I add a data activity to course "Course 1" section "3" and I fill the form with: | Name | Test name2 | | Description | Test database description | And I switch to the main window And I wait "8" seconds Then I should not see "Test name2" And I press "Resume live updates" And I wait "8" seconds And I should see "Test name2" @javascript Scenario: Check course loglive report entries for a user Given I log out And I am on the "Test name" "data activity" page logged in as student1 And I log out And I am on the "Course 1" Course page logged in as admin When I navigate to "Reports > Live logs" in site administration Then I should see "Course module viewed" And I should see "Test name" And I should see "Ann, Jill, Grainne, Beauchamp" PK{\J!wKK,loglive/tests/behat/behat_report_loglive.phpnu[. require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); /** * Step definition for report_loglive behat tests. * * @package report_loglive * @category test * @copyright 2025 Laurent David * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_report_loglive extends behat_base { /** * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. * * Recognised page names are: * | pagetype | name meaning | description | * | Logs | Course name | The course report loglive page | * * @param string $page identifies which type of page this is, e.g. 'Logs'. * @param string $identifier identifies the particular page, e.g. 'C1'. * @return moodle_url the corresponding URL. * @throws Exception with a meaningful error message if the specified page cannot be found. */ protected function resolve_page_instance_url(string $page, string $identifier): moodle_url { switch (strtolower($page)) { case 'logs': $courseid = $this->get_course_id($identifier); return new moodle_url('/report/loglive/index.php', [ 'id' => $courseid, ]); default: throw new Exception("Unrecognised page type '{$page}'"); } } } PK{\331loglive/tests/behat/course_report_loglive.featurenu[@report @report_loglive Feature: In a course administration page, navigate through report page, test for report live log page In order to navigate through report page As an admin Go to the course administration -> reports -> live logs Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | admin | C1 | editingteacher | | student1 | C1 | student | @javascript Scenario: Selector should be available in live logs page Given I log in as "admin" And I am on "Course 1" course homepage When I navigate to "Reports" in current page administration And I click on "Live logs" "link" Then "Report" "field" should exist in the "tertiary-navigation" "region" And I should see "Live logs" in the "tertiary-navigation" "region" PK{\n``0loglive/tests/behat/group_report_loglive.featurenu[@report @report_loglive Feature: In a course with group mode, I can view the group report loglive page or not depending on the group I am in. Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course separate group | C1 | 0 | 1 | | Course visible group | C2 | 0 | 2 | | Course no group | C3 | 0 | 0 | And 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 "course enrolments" exist: | user | course | role | | teacher1 | C1 | teacher | | teacher2 | C1 | teacher | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C2 | teacher | | teacher2 | C2 | teacher | | student1 | C2 | student | | student2 | C2 | student | | teacher1 | C3 | teacher | | teacher2 | C3 | teacher | | student1 | C3 | student | | student2 | C3 | student | And the following "groups" exist: | course | name | idnumber | | C1 | Group C1.1 | group11 | | C2 | Group C2.1 | group21 | | C3 | Group C3.1 | group31 | And the following "group members" exist: | group | user | | group11 | student1 | | group11 | teacher1 | | group21 | student1 | | group21 | teacher1 | | group31 | student1 | | group31 | teacher1 | And the following "activities" exist: | activity | name | intro | course | idnumber | | page | Page11 | Page11 | C1 | page1 | | page | Page21 | Page21 | C2 | page1 | | page | Page31 | Page31 | C3 | page1 | # Generate logs for the pages. And I am on the "Page11" "page activity" page logged in as student1 And I am on "Course separate group" course homepage And I log out And I am on the "Page11" "page activity" page logged in as student2 And I am on "Course separate group" course homepage And I log out And I am on the "Page21" "page activity" page logged in as student1 And I am on "Course visible group" course homepage And I log out And I am on the "Page21" "page activity" page logged in as student2 And I am on "Course visible group" course homepage And I log out And I am on the "Page31" "page activity" page logged in as student1 And I am on "Course no group" course homepage And I log out And I am on the "Page31" "page activity" page logged in as student2 And I am on "Course no group" course homepage And I log out Scenario Outline: As a user in a course, I can view a link to the report live logs if I am in the right group. Given I log in as "" And I am on "" course homepage When I navigate to "Reports" in current page administration And "Live logs" "link" And I log out Examples: | course | user | shouldexist | | Course separate group | teacher1 | should exist | | Course separate group | teacher2 | should not exist | | Course visible group | teacher1 | should exist | | Course visible group | teacher2 | should exist | | Course no group | teacher1 | should exist | | Course no group | teacher2 | should exist | Scenario Outline: As a non editing teacher not in a group, I can not view the live report logs. Given I log in as "" When I am on the "" "report_loglive > Logs" page Then I "you need to be part of a group to see this page." Examples: | course | user | shouldsee | | Course separate group | teacher1 | should not see | | Course separate group | teacher2 | should see | | Course visible group | teacher1 | should not see | | Course visible group | teacher2 | should not see | | Course no group | teacher1 | should not see | | Course no group | teacher2 | should not see | PK{\loglive/settings.phpnu[. /** * Links and settings * * This file contains links and settings used by report_loglive * * @package report_loglive * @copyright 2011 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; // Just a link to course report. $ADMIN->add('reports', new admin_externalpage('reportloglive', get_string('pluginname', 'report_loglive'), "$CFG->wwwroot/report/loglive/index.php", 'report/loglive:view')); // No report settings. $settings = null; PK{\wllcompletion/styles.cssnu[#page-report-completion-index table#completion-progress { margin-top: 20px; margin-bottom: 30px; } #page-report-completion-index .export-actions { text-align: center; list-style: none; } #page-report-completion-index .criterianame, #page-report-completion-index .criteriaicon, #page-report-completion-index .completion-progresscell { text-align: center; } /* Custom CSS for rotated header.. */ #page-report-completion-index .rotated-text-container { display: inline-block; width: 16px; } /*rtl:begin:ignore*/ #page-report-completion-index .rotated-text { display: inline-block; white-space: nowrap; transform: translate(0, 100%) rotate(-90deg); transform-origin: 0 0; vertical-align: middle; } #page-report-completion-index .rotated-text:after { content: ""; float: left; margin-top: 100%; } /*rtl:end:ignore*/PK{\x:completion/db/install.phpnu[. /** * Post installation and migration code. * * @package report * @subpackage completion * @copyright 2011 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; function xmldb_report_completion_install() { global $DB; } PK{\s#QLLcompletion/db/access.phpnu[. /** * Capabilities * * @package report_completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'report/completion:view' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'coursereport/completion:view', ) ); PK{\x!(completion/lang/en/report_completion.phpnu[. /** * Lang strings * * @package report * @subpackage completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['clicktomarkusercomplete'] = 'Click to mark user complete'; $string['completion:view'] = 'View course completion report'; $string['completiondate'] = 'Completion date'; $string['id'] = 'ID'; $string['name'] = 'Name'; $string['nocapability'] = 'Can not access user completion report'; $string['page-report-completion-x'] = 'Any completion report'; $string['page-report-completion-index'] = 'Course completion report'; $string['page-report-completion-user'] = 'User course completion report'; $string['pluginname'] = 'Course completion'; $string['privacy:metadata'] = 'The Course completion report only shows data stored in other locations.'; $string['eventreportviewed'] = 'Completion report viewed'; $string['eventuserreportviewed'] = 'Completion user report viewed'; PK{\'ҁcompletion/lib.phpnu[. /** * Version details. * * @package report * @subpackage completion * @copyright 2009 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * This function extends the navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $course The course to object for the report * @param stdClass $context The context of the course */ function report_completion_extend_navigation_course($navigation, $course, $context) { global $CFG; require_once($CFG->libdir.'/completionlib.php'); if (has_capability('report/completion:view', $context)) { $completion = new completion_info($course); if ($completion->is_enabled() && $completion->has_criteria()) { $url = new moodle_url('/report/completion/index.php', array('course'=>$course->id)); $navigation->add(get_string('pluginname','report_completion'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/report', '')); } } } /** * This function extends the course navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $user * @param stdClass $course The course to object for the report */ function report_completion_extend_navigation_user($navigation, $user, $course) { return; //TODO: this plugin was not linked from navigation in 2.0, let's keep it that way for now --skodak if (report_completion_can_access_user_report($user, $course)) { $url = new moodle_url('/report/completion/user.php', array('id'=>$user->id, 'course'=>$course->id)); $navigation->add(get_string('coursecompletion'), $url); } } /** * Is current user allowed to access this report * * @private defined in lib.php for performance reasons * * @param stdClass $user * @param stdClass $course * @return bool */ function report_completion_can_access_user_report($user, $course) { global $USER, $CFG; if (empty($CFG->enablecompletion)) { return false; } if ($course->id != SITEID and !$course->enablecompletion) { return false; } $coursecontext = context_course::instance($course->id); $personalcontext = context_user::instance($user->id); if ($user->id == $USER->id) { if ($course->showreports and (is_viewing($coursecontext, $USER) or is_enrolled($coursecontext, $USER))) { return true; } } else if (has_capability('moodle/user:viewuseractivitiesreport', $personalcontext)) { if ($course->showreports and (is_viewing($coursecontext, $user) or is_enrolled($coursecontext, $user))) { return true; } } // Check if $USER shares group with $user (in case separated groups are enabled and 'moodle/site:accessallgroups' is disabled). if (!groups_user_groups_visible($course, $user->id)) { return false; } if (has_capability('report/completion:view', $coursecontext)) { return true; } return false; } /** * Return a list of page types * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block * @return array */ function report_completion_page_type_list($pagetype, $parentcontext, $currentcontext) { $array = array( '*' => get_string('page-x', 'pagetype'), 'report-*' => get_string('page-report-x', 'pagetype'), 'report-completion-*' => get_string('page-report-completion-x', 'report_completion'), 'report-completion-index' => get_string('page-report-completion-index', 'report_completion'), 'report-completion-user' => get_string('page-report-completion-user', 'report_completion') ); return $array; } PK{\Ycompletion/version.phpnu[. /** * Version details * * @package report * @subpackage completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_completion'; // Full name of the plugin (used for diagnostics) PK{\q'completion/classes/privacy/provider.phpnu[. /** * Privacy provider implementation for report_completion. * * @package report_completion * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_completion\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy provider implementation for report_completion. * * @copyright 2018 Mihail Geshoski * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK{\3ʩ *completion/classes/event/report_viewed.phpnu[. /** * The report_completion report viewed event. * * @package report_completion * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_completion\event; defined('MOODLE_INTERNAL') || die(); /** * The report_completion report viewed event class. * * @package report_completion * @since Moodle 2.7 * @copyright 2014 onwards Ankit Agarwal * @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() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventreportviewed', 'report_completion'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the completion report for the course with id '$this->courseid'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/completion/index.php', array('course' => $this->courseid)); } /** * custom validations. * * @throws \coding_exception when validation fails. * @return void */ protected function validate_data() { parent::validate_data(); if ($this->contextlevel != CONTEXT_COURSE) { throw new \coding_exception('Context level must be CONTEXT_COURSE.'); } } } PK{\[& /completion/classes/event/user_report_viewed.phpnu[. /** * The report_completion user report viewed event. * * @package report_completion * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_completion\event; defined('MOODLE_INTERNAL') || die(); /** * The report_completion user report viewed event class. * * @package report_completion * @since Moodle 2.7 * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_report_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventuserreportviewed', 'report_completion'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the user completion report for the user with id '$this->relateduserid'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/completion/user.php', array('course' => $this->courseid, 'id' => $this->relateduserid)); } /** * custom validations. * * @throws \coding_exception when validation fails. * @return void */ protected function validate_data() { parent::validate_data(); if ($this->contextlevel != CONTEXT_COURSE) { throw new \coding_exception('Context level must be CONTEXT_COURSE.'); } if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } } } PK{\:]]completion/index.phpnu[. /** * Course completion progress report * * @package report * @subpackage completion * @copyright 2009 Catalyst IT Ltd * @author Aaron Barnes * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\report_helper; require_once(__DIR__.'/../../config.php'); require_once("{$CFG->libdir}/completionlib.php"); /** * Configuration */ define('COMPLETION_REPORT_PAGE', 25); define('COMPLETION_REPORT_COL_TITLES', true); /* * Setup page, check permissions */ // Get course $courseid = required_param('course', PARAM_INT); $format = optional_param('format','',PARAM_ALPHA); $sort = optional_param('sort','',PARAM_ALPHA); $edituser = optional_param('edituser', 0, PARAM_INT); $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); $context = context_course::instance($course->id); $url = new moodle_url('/report/completion/index.php', array('course'=>$course->id)); $PAGE->set_url($url); $PAGE->set_pagelayout('report'); $firstnamesort = ($sort == 'firstname'); $excel = ($format == 'excelcsv'); $csv = ($format == 'csv' || $excel); if ($csv) { $dateformat = "%F %T"; } else { $dateformat = get_string('strftimedatetimeshort', 'langconfig'); } // Load CSV library if ($csv) { require_once("{$CFG->libdir}/csvlib.class.php"); } // Paging $start = optional_param('start', 0, PARAM_INT); $sifirst = optional_param('sifirst', 'all', PARAM_NOTAGS); $silast = optional_param('silast', 'all', PARAM_NOTAGS); // Whether to show extra user identity information. $extrafields = \core_user\fields::get_identity_fields($context, true); $leftcols = 1 + count($extrafields); // Check permissions require_login($course); require_capability('report/completion:view', $context); // Get group mode $group = groups_get_course_group($course, true); // Supposed to verify group if ($group === 0 && $course->groupmode == SEPARATEGROUPS) { require_capability('moodle/site:accessallgroups',$context); } /** * Load data */ // Retrieve course_module data for all modules in the course $modinfo = get_fast_modinfo($course); // Get criteria for course $completion = new completion_info($course); if (!$completion->has_criteria()) { throw new \moodle_exception('nocriteriaset', 'completion', $CFG->wwwroot.'/course/report.php?id='.$course->id); } // Get criteria and put in correct order $criteria = array(); foreach ($completion->get_criteria(COMPLETION_CRITERIA_TYPE_COURSE) as $criterion) { $criteria[] = $criterion; } foreach ($completion->get_criteria(COMPLETION_CRITERIA_TYPE_ACTIVITY) as $criterion) { $criteria[] = $criterion; } foreach ($completion->get_criteria() as $criterion) { if (!in_array($criterion->criteriatype, array( COMPLETION_CRITERIA_TYPE_COURSE, COMPLETION_CRITERIA_TYPE_ACTIVITY))) { $criteria[] = $criterion; } } // Can logged in user mark users as complete? // (if the logged in user has a role defined in the role criteria) $allow_marking = false; $allow_marking_criteria = null; if (!$csv) { // Get role criteria $rcriteria = $completion->get_criteria(COMPLETION_CRITERIA_TYPE_ROLE); if (!empty($rcriteria)) { foreach ($rcriteria as $rcriterion) { $users = get_role_users($rcriterion->role, $context, true); // If logged in user has this role, allow marking complete if ($users && in_array($USER->id, array_keys($users))) { $allow_marking = true; $allow_marking_criteria = $rcriterion->id; break; } } } } /* * Setup page header */ if ($csv) { $shortname = format_string($course->shortname, true, array('context' => $context)); $shortname = preg_replace('/[^a-z0-9-]/', '_',core_text::strtolower(strip_tags($shortname))); $export = new csv_export_writer('comma', '"', 'application/download', $excel); $export->set_filename('completion-'.$shortname); } else { // Navigation and header $strcompletion = get_string('coursecompletion'); $PAGE->set_title($strcompletion); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); // Print the selected dropdown. $pluginname = get_string('pluginname', 'report_completion'); report_helper::print_report_selector($pluginname); // Handle groups (if enabled) groups_print_course_menu($course, $CFG->wwwroot.'/report/completion/index.php?course='.$course->id); } if ($sifirst !== 'all') { set_user_preference('ifirst', $sifirst); } if ($silast !== 'all') { set_user_preference('ilast', $silast); } if (!empty($USER->preference['ifirst'])) { $sifirst = $USER->preference['ifirst']; } else { $sifirst = 'all'; } if (!empty($USER->preference['ilast'])) { $silast = $USER->preference['ilast']; } else { $silast = 'all'; } // Generate where clause $where = array(); $where_params = array(); if ($sifirst !== 'all') { $where[] = $DB->sql_like('u.firstname', ':sifirst', false, false); $where_params['sifirst'] = $sifirst.'%'; } if ($silast !== 'all') { $where[] = $DB->sql_like('u.lastname', ':silast', false, false); $where_params['silast'] = $silast.'%'; } // Get user match count $total = $completion->get_num_tracked_users(implode(' AND ', $where), $where_params, $group); // Total user count $grandtotal = $completion->get_num_tracked_users('', array(), $group); // If no users in this course what-so-ever if (!$grandtotal) { echo $OUTPUT->container(get_string('err_nousers', 'completion'), 'errorbox errorboxcontent'); echo $OUTPUT->footer(); exit; } // Get user data $progress = array(); if ($total) { $progress = $completion->get_progress_all( implode(' AND ', $where), $where_params, $group, $firstnamesort ? 'u.firstname ASC' : 'u.lastname ASC', $csv ? 0 : COMPLETION_REPORT_PAGE, $csv ? 0 : $start, $context ); } // Build link for paging $link = $CFG->wwwroot.'/report/completion/index.php?course='.$course->id; if (strlen($sort)) { $link .= '&sort='.$sort; } $link .= '&start='; $pagingbar = ''; // Initials bar. $prefixfirst = 'sifirst'; $prefixlast = 'silast'; $pagingbar .= $OUTPUT->initials_bar($sifirst, 'firstinitial', get_string('firstname'), $prefixfirst, $url); $pagingbar .= $OUTPUT->initials_bar($silast, 'lastinitial', get_string('lastname'), $prefixlast, $url); // Do we need a paging bar? if ($total > COMPLETION_REPORT_PAGE) { // Paging bar $pagingbar .= '
    '; $pagingbar .= get_string('page').': '; $sistrings = array(); if ($sifirst != 'all') { $sistrings[] = "sifirst={$sifirst}"; } if ($silast != 'all') { $sistrings[] = "silast={$silast}"; } $sistring = !empty($sistrings) ? '&'.implode('&', $sistrings) : ''; // Display previous link if ($start > 0) { $pstart = max($start - COMPLETION_REPORT_PAGE, 0); $pagingbar .= "(".get_string('previous').') '; } // Create page links $curstart = 0; $curpage = 0; while ($curstart < $total) { $curpage++; if ($curstart == $start) { $pagingbar .= ' '.$curpage.' '; } else { $pagingbar .= " $curpage "; } $curstart += COMPLETION_REPORT_PAGE; } // Display next link $nstart = $start + COMPLETION_REPORT_PAGE; if ($nstart < $total) { $pagingbar .= " (".get_string('next').')'; } $pagingbar .= '
    '; } /* * Draw table header */ // Start of table if (!$csv) { print '
    '; // ugh $total_header = ($total == $grandtotal) ? $total : "{$total}/{$grandtotal}"; echo $OUTPUT->heading(get_string('allparticipants').": {$total_header}", 3); print $pagingbar; if (!$total) { echo $OUTPUT->notification(get_string('nothingtodisplay'), 'info', false); echo $OUTPUT->footer(); exit; } print ''; // Print criteria group names print PHP_EOL.''; echo ''; $current_group = false; $col_count = 0; for ($i = 0; $i <= count($criteria); $i++) { if (isset($criteria[$i])) { $criterion = $criteria[$i]; if ($current_group && $criterion->criteriatype === $current_group->criteriatype) { ++$col_count; continue; } } // Print header cell if ($col_count) { print ''; } if (isset($criteria[$i])) { // Move to next criteria type $current_group = $criterion; $col_count = 1; } } // Overall course completion status print ''; print ''; // Print aggregation methods print PHP_EOL.''; echo ''; $current_group = false; $col_count = 0; for ($i = 0; $i <= count($criteria); $i++) { if (isset($criteria[$i])) { $criterion = $criteria[$i]; if ($current_group && $criterion->criteriatype === $current_group->criteriatype) { ++$col_count; continue; } } // Print header cell if ($col_count) { $has_agg = array( COMPLETION_CRITERIA_TYPE_COURSE, COMPLETION_CRITERIA_TYPE_ACTIVITY, COMPLETION_CRITERIA_TYPE_ROLE, ); if (in_array($current_group->criteriatype, $has_agg)) { // Try load a aggregation method $method = $completion->get_aggregation_method($current_group->criteriatype); $method = $method == 1 ? get_string('all') : get_string('any'); } else { $method = '-'; } print ''; } if (isset($criteria[$i])) { // Move to next criteria type $current_group = $criterion; $col_count = 1; } } // Overall course aggregation method print ''; print ''; // Print criteria titles if (COMPLETION_REPORT_COL_TITLES) { print PHP_EOL.''; echo ''; foreach ($criteria as $criterion) { // Get criteria details $details = $criterion->get_title_detailed(); print ''; } // Overall course completion status print ''; } // Print user heading and icons print ''; // User heading / sort option print ''; // Print user identity columns foreach ($extrafields as $field) { echo ''; } /// /// Print criteria icons /// foreach ($criteria as $criterion) { // Generate icon details $iconlink = ''; $iconalt = ''; // Required $iconattributes = array('class' => 'icon'); switch ($criterion->criteriatype) { case COMPLETION_CRITERIA_TYPE_ACTIVITY: // Display icon $iconlink = $CFG->wwwroot.'/mod/'.$criterion->module.'/view.php?id='.$criterion->moduleinstance; $iconattributes['title'] = $modinfo->cms[$criterion->moduleinstance]->get_formatted_name(); $iconalt = get_string('modulename', $criterion->module); break; case COMPLETION_CRITERIA_TYPE_COURSE: // Load course $crs = $DB->get_record('course', array('id' => $criterion->courseinstance)); // Display icon $iconlink = $CFG->wwwroot.'/course/view.php?id='.$criterion->courseinstance; $iconattributes['title'] = format_string($crs->fullname, true, array('context' => context_course::instance($crs->id, MUST_EXIST))); $iconalt = format_string($crs->shortname, true, array('context' => context_course::instance($crs->id))); break; case COMPLETION_CRITERIA_TYPE_ROLE: // Load role $role = $DB->get_record('role', array('id' => $criterion->role)); // Display icon $iconalt = $role->name; break; } // Create icon alt if not supplied if (!$iconalt) { $iconalt = $criterion->get_title(); } // Print icon and cell print ''; } // Overall course completion status print ''; print ''; echo ''; } else { // The CSV headers $row = array(); $row[] = get_string('id', 'report_completion'); $row[] = get_string('name', 'report_completion'); foreach ($extrafields as $field) { $row[] = \core_user\fields::get_display_name($field); } // Add activity headers foreach ($criteria as $criterion) { // Handle activity completion differently if ($criterion->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) { // Load activity $mod = $criterion->get_mod_instance(); $row[] = $formattedname = format_string($mod->name, true, array('context' => context_module::instance($criterion->moduleinstance))); $row[] = $formattedname . ' - ' . get_string('completiondate', 'report_completion'); } else { // Handle all other criteria $row[] = strip_tags($criterion->get_title_detailed()); } } $row[] = get_string('coursecomplete', 'completion'); $export->add_data($row); } /// /// Display a row for each user /// foreach ($progress as $user) { // User name if ($csv) { $row = array(); $row[] = $user->id; $row[] = fullname($user, has_capability('moodle/site:viewfullnames', $context)); foreach ($extrafields as $field) { $row[] = $user->{$field}; } } else { print PHP_EOL.''; if (completion_can_view_data($user->id, $course)) { $userurl = new moodle_url('/blocks/completionstatus/details.php', array('course' => $course->id, 'user' => $user->id)); } else { $userurl = new moodle_url('/user/view.php', array('id' => $user->id, 'course' => $course->id)); } print ''; foreach ($extrafields as $field) { echo ''; } } // Progress for each course completion criteria foreach ($criteria as $criterion) { $criteria_completion = $completion->get_user_completion($user->id, $criterion); $is_complete = $criteria_completion->is_complete(); // Handle activity completion differently if ($criterion->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) { // Load activity $activity = $modinfo->cms[$criterion->moduleinstance]; // Get progress information and state if (array_key_exists($activity->id, $user->progress)) { $state = $user->progress[$activity->id]->completionstate; } else if ($is_complete) { $state = COMPLETION_COMPLETE; } else { $state = COMPLETION_INCOMPLETE; } if ($is_complete) { $date = userdate($criteria_completion->timecompleted, $dateformat); } else { $date = ''; } // Work out how it corresponds to an icon switch($state) { case COMPLETION_INCOMPLETE : $completiontype = 'n'; break; case COMPLETION_COMPLETE : $completiontype = 'y'; break; case COMPLETION_COMPLETE_PASS : $completiontype = 'pass'; break; case COMPLETION_COMPLETE_FAIL : $completiontype = 'fail'; break; } $auto = $activity->completion == COMPLETION_TRACKING_AUTOMATIC; $completionicon = 'completion-'.($auto ? 'auto' : 'manual').'-'.$completiontype; $describe = get_string('completion-'.$completiontype, 'completion'); $a = new StdClass(); $a->state = $describe; $a->date = $date; $a->user = fullname($user); $a->activity = $activity->get_formatted_name(); $fulldescribe = get_string('progress-title', 'completion', $a); if ($csv) { $row[] = $describe; $row[] = $date; } else { print ''; } continue; } // Handle all other criteria $completiontype = $is_complete ? 'y' : 'n'; $completionicon = 'completion-auto-'.$completiontype; $describe = get_string('completion-'.$completiontype, 'completion'); $a = new stdClass(); $a->state = $describe; if ($is_complete) { $a->date = userdate($criteria_completion->timecompleted, $dateformat); } else { $a->date = ''; } $a->user = fullname($user); $a->activity = strip_tags($criterion->get_title()); $fulldescribe = get_string('progress-title', 'completion', $a); if ($csv) { $row[] = $a->date; } else { print ''; } else { print $OUTPUT->pix_icon('i/' . $completionicon, $fulldescribe) . ''; } print ''; } } // Handle overall course completion // Load course completion $params = array( 'userid' => $user->id, 'course' => $course->id ); $ccompletion = new completion_completion($params); $completiontype = $ccompletion->is_complete() ? 'y' : 'n'; $describe = get_string('completion-'.$completiontype, 'completion'); $a = new StdClass; if ($ccompletion->is_complete()) { $a->date = userdate($ccompletion->timecompleted, $dateformat); } else { $a->date = ''; } $a->state = $describe; $a->user = fullname($user); $a->activity = strip_tags(get_string('coursecomplete', 'completion')); $fulldescribe = get_string('progress-title', 'completion', $a); if ($csv) { $row[] = $a->date; } else { print ''; } if ($csv) { $export->add_data($row); } else { print ''; } } if ($csv) { $export->download_file(); } else { echo ''; } print '
    ' . get_string('criteriagroup', 'completion') . ''.$current_group->get_type_title().''.get_string('course').'
    ' . get_string('aggregationmethod', 'completion').''.$method.''; // Get course aggregation $method = $completion->get_aggregation_method(); print $method == 1 ? get_string('all') : get_string('any'); print '
    ' . get_string('criteria', 'completion') . ''; print '
    '.$details.'
    '; print '
    '; print '
    '.get_string('coursecomplete', 'completion').'
    '; print '
    '; $sistring = "&silast={$silast}&sifirst={$sifirst}"; if ($firstnamesort) { print get_string('firstname')." / id}{$sistring}\">". get_string('lastname').''; } else { print "id}&sort=firstname{$sistring}\">". get_string('firstname').' / '. get_string('lastname'); } print '' . \core_user\fields::get_display_name($field) . ''; print ($iconlink ? '' : ''); print $OUTPUT->render($criterion->get_icon($iconalt, $iconattributes)); print ($iconlink ? '' : ''); print ''; print $OUTPUT->pix_icon('i/course', get_string('coursecomplete', 'completion')); print '
    ' . fullname($user, has_capability('moodle/site:viewfullnames', $context)) . ''.s($user->{$field}).''; print $OUTPUT->pix_icon('i/' . $completionicon, $fulldescribe); print ''; if ($allow_marking_criteria === $criterion->id) { $describe = get_string('completion-'.$completiontype, 'completion'); $toggleurl = new moodle_url( '/course/togglecompletion.php', array( 'user' => $user->id, 'course' => $course->id, 'rolec' => $allow_marking_criteria, 'sesskey' => sesskey() ) ); print '' . $OUTPUT->pix_icon('i/completion-manual-' . ($is_complete ? 'y' : 'n'), $describe) . ''; // Display course completion status icon print $OUTPUT->pix_icon('i/completion-auto-' . $completiontype, $fulldescribe); print '
    '; $csvurl = new moodle_url('/report/completion/index.php', array('course' => $course->id, 'format' => 'csv')); $excelurl = new moodle_url('/report/completion/index.php', array('course' => $course->id, 'format' => 'excelcsv')); print ''; echo $OUTPUT->footer($course); // Trigger a report viewed event. $event = \report_completion\event\report_viewed::create(array('context' => $context)); $event->trigger(); PK{\)^^&completion/tests/event/events_test.phpnu[. /** * Tests for report completion events. * * @package report_completion * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace report_completion\event; /** * Class report_completion_events_testcase * * Class for tests related to completion report events. * * @package report_completion * @copyright 2014 onwards Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class events_test extends \advanced_testcase { /** * Setup testcase. */ public function setUp(): void { parent::setUp(); $this->setAdminUser(); $this->resetAfterTest(); } /** * Test the report viewed event. * * It's not possible to use the moodle API to simulate the viewing of log report, so here we * simply create the event and trigger it. */ public function test_report_viewed(): void { $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Trigger event for completion report viewed. $event = \report_completion\event\report_viewed::create(array('context' => $context)); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $event = reset($events); $this->assertInstanceOf('\report_completion\event\report_viewed', $event); $this->assertEquals($context, $event->get_context()); $url = new \moodle_url('/report/completion/index.php', array('course' => $course->id)); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); } /** * Test the user report viewed event. * * It's not possible to use the moodle API to simulate the viewing of log report, so here we * simply create the event and trigger it. */ public function test_user_report_viewed(): void { $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Trigger event for completion report viewed. $event = \report_completion\event\user_report_viewed::create(array('context' => $context, 'relateduserid' => 3)); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $event = reset($events); $this->assertInstanceOf('\report_completion\event\user_report_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEquals(3, $event->relateduserid); $this->assertEquals(new \moodle_url('/report/completion/user.php', array('id' => 3, 'course' => $course->id)), $event->get_url()); $this->assertEventContextNotUsed($event); } } PK{\2|  0completion/tests/behat/completion_report.featurenu[@report @report_completion Feature: See the completion for items in a course In order see completion data As a teacher I need to view completion report Background: Given the following "custom profile fields" exist: | datatype | shortname | name | | text | fruit | Fruit | And the following "users" exist: | username | firstname | lastname | email | middlename | alternatename | firstnamephonetic | lastnamephonetic | profile_field_fruit | | teacher1 | Teacher | 1 | teacher1@example.com | | fred | | | | | student1 | Grainne | Beauchamp | student1@example.com | Ann | Jill | Gronya | Beecham | Kumquat | And the following "courses" exist: | fullname | shortname | category | enablecompletion | | Course 1 | C1 | 0 | 1 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | completion | completionview | | page | PageName1 | PageDesc1 | C1 | PAGE1 | 1 | 1 | @javascript Scenario: The completion report respects user fullname setting Given the following config values are set as admin: | fullnamedisplay | firstname | | alternativefullnameformat | middlename, alternatename, firstname, lastname | And I am on the "C1" "Course" page logged in as "teacher1" And I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | Page - PageName1 | 1 | And I press "Save changes" And I am on "Course 1" course homepage When I navigate to "Reports" in current page administration And I click on "Course completion" "link" in the "region-main" "region" Then I should see "Ann, Jill, Grainne, Beauchamp" @javascript Scenario: The completion report displays custom user profile fields Given the following config values are set as admin: | showuseridentity | email,profile_field_fruit | And I am on the "C1" "Course" page logged in as "teacher1" And I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | Page - PageName1 | 1 | And I press "Save changes" And I am on "Course 1" course homepage When I navigate to "Reports" in current page administration And I click on "Course completion" "link" in the "region-main" "region" # We can't refer to table headings by name because they aren't on the first row. Then the following should exist in the "completionreport" table: | -1- | -2- | -3- | | Grainne Beauchamp | student1@example.com | Kumquat | PK{\)̼7completion/tests/behat/course_report_completion.featurenu[@report @report_completion Feature: In a course administration page, navigate through report page, test for course completion page In order to navigate through report page As an admin Go to course administration -> reports -> course completion Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | enablecompletion | | Course 1 | C1 | 0 | 1 | 1 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | admin | C1 | editingteacher | | student1 | C1 | student | @javascript Scenario: Selector should be available in the course completion page Given I log in as "admin" And I am on "Course 1" course homepage with editing mode on And I navigate to "Course completion" in current page administration And I expand all fieldsets And I set the following fields to these values: | id_criteria_self | 1 | And I press "Save changes" And I am on "Course 1" course homepage When I navigate to "Reports" in current page administration And I click on "Course completion" "link" in the "region-main" "region" Then "Report" "field" should exist in the "tertiary-navigation" "region" And I should see "Course completion" in the "tertiary-navigation" "region" PK{\ ( (completion/user.phpnu[. /** * Display user completion report * * @package report * @subpackage completion * @copyright 2009 Catalyst IT Ltd * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../../config.php'); require_once($CFG->dirroot.'/report/completion/lib.php'); require_once($CFG->libdir.'/completionlib.php'); $userid = required_param('id', PARAM_INT); $courseid = required_param('course', PARAM_INT); $user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0), '*', MUST_EXIST); $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST); $coursecontext = context_course::instance($course->id); $personalcontext = context_user::instance($user->id); if ($USER->id != $user->id and has_capability('moodle/user:viewuseractivitiesreport', $personalcontext) and !is_enrolled($coursecontext, $USER) and is_enrolled($coursecontext, $user)) { //TODO: do not require parents to be enrolled in courses - this is a hack! require_login(); $PAGE->set_course($course); } else { require_login($course); } if (!report_completion_can_access_user_report($user, $course)) { // this should never happen throw new \moodle_exception('nocapability', 'report_completion'); } $stractivityreport = get_string('activityreport'); $PAGE->set_pagelayout('admin'); $PAGE->set_url('/report/completion/user.php', array('id'=>$user->id, 'course'=>$course->id)); $PAGE->navigation->extend_for_user($user); $PAGE->navigation->set_userid_for_parent_checks($user->id); // see MDL-25805 for reasons and for full commit reference for reversal when fixed. $PAGE->set_title("$course->shortname: $stractivityreport"); $PAGE->set_heading($course->fullname); echo $OUTPUT->header(); // Display course completion user report // Grab all courses the user is enrolled in and their completion status $sql = " SELECT DISTINCT c.id AS id FROM {course} c INNER JOIN {context} con ON con.instanceid = c.id INNER JOIN {role_assignments} ra ON ra.contextid = con.id INNER JOIN {enrol} e ON c.id = e.courseid INNER JOIN {user_enrolments} ue ON e.id = ue.enrolid AND ra.userid = ue.userid AND ra.userid = {$user->id} "; // Get roles that are tracked by course completion if ($roles = $CFG->gradebookroles) { $sql .= ' AND ra.roleid IN ('.$roles.') '; } $sql .= ' WHERE con.contextlevel = '.CONTEXT_COURSE.' AND c.enablecompletion = 1 '; // If we are looking at a specific course if ($course->id != 1) { $sql .= ' AND c.id = '.(int)$course->id.' '; } // Check if result is empty $rs = $DB->get_recordset_sql($sql); if (!$rs->valid()) { if ($course->id != 1) { $error = get_string('nocompletions', 'report_completion'); // TODO: missing string } else { $error = get_string('nocompletioncoursesenroled', 'report_completion'); // TODO: missing string } echo $OUTPUT->notification($error); $rs->close(); // not going to loop (but break), close rs echo $OUTPUT->footer(); die(); } // Categorize courses by their status $courses = array( 'inprogress' => array(), 'complete' => array(), 'unstarted' => array() ); // Sort courses by the user's status in each foreach ($rs as $course_completion) { $c_info = new completion_info((object)$course_completion); // Is course complete? $coursecomplete = $c_info->is_course_complete($user->id); // Has this user completed any criteria? $criteriacomplete = $c_info->count_course_user_data($user->id); if ($coursecomplete) { $courses['complete'][] = $c_info; } else if ($criteriacomplete) { $courses['inprogress'][] = $c_info; } else { $courses['unstarted'][] = $c_info; } } $rs->close(); // after loop, close rs // Loop through course status groups foreach ($courses as $type => $infos) { // If there are courses with this status if (!empty($infos)) { echo '

    '.get_string($type, 'report_completion').'

    '; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; if ($type === 'complete') { echo ''; } echo ''; // For each course foreach ($infos as $c_info) { // Get course info $c_course = $DB->get_record('course', array('id' => $c_info->course_id)); $course_context = context_course::instance($c_course->id, MUST_EXIST); $course_name = format_string($c_course->fullname, true, array('context' => $course_context)); // Get completions $completions = $c_info->get_completions($user->id); // Save row data $rows = array(); // For aggregating activity completion $activities = array(); $activities_complete = 0; // For aggregating prerequisites $prerequisites = array(); $prerequisites_complete = 0; // Loop through course criteria foreach ($completions as $completion) { $criteria = $completion->get_criteria(); $complete = $completion->is_complete(); // Activities are a special case, so cache them and leave them till last if ($criteria->criteriatype == COMPLETION_CRITERIA_TYPE_ACTIVITY) { $activities[$criteria->moduleinstance] = $complete; if ($complete) { $activities_complete++; } continue; } // Prerequisites are also a special case, so cache them and leave them till last if ($criteria->criteriatype == COMPLETION_CRITERIA_TYPE_COURSE) { $prerequisites[$criteria->courseinstance] = $complete; if ($complete) { $prerequisites_complete++; } continue; } $row = array(); $row['title'] = $criteria->get_title(); $row['status'] = $completion->get_status(); $rows[] = $row; } // Aggregate activities if (!empty($activities)) { $row = array(); $row['title'] = get_string('activitiescomplete', 'report_completion'); $row['status'] = $activities_complete.' of '.count($activities); $rows[] = $row; } // Aggregate prerequisites if (!empty($prerequisites)) { $row = array(); $row['title'] = get_string('prerequisitescompleted', 'completion'); $row['status'] = $prerequisites_complete.' of '.count($prerequisites); array_splice($rows, 0, 0, array($row)); } $first_row = true; // Print table foreach ($rows as $row) { // Display course name on first row if ($first_row) { echo ''; } else { echo ''; } echo ''; // Display completion date for completed courses on first row if ($type === 'complete' && $first_row) { $params = array( 'userid' => $user->id, 'course' => $c_course->id ); $ccompletion = new completion_completion($params); echo ''; } $first_row = false; echo ''; } } echo '
    '.get_string('course').''.get_string('requiredcriteria', 'completion').''.get_string('status').''.get_string('info').''.get_string('completiondate', 'report_completion').'
    '.$course_name.'
    '; echo $row['title']; echo ''; switch ($row['status']) { case 'Yes': echo get_string('complete'); break; case 'No': echo get_string('incomplete', 'report_completion'); break; default: echo $row['status']; } // Display link on first row echo ''; if ($first_row) { echo ''.get_string('detailedview', 'report_completion').''; } echo ''.userdate($ccompletion->timecompleted, '%e %B %G').'
    '; } } echo $OUTPUT->footer(); // Trigger a user report viewed event. $event = \report_completion\event\user_report_viewed::create(array('context' => $coursecontext, 'relateduserid' => $userid)); $event->trigger(); PK{\y6questioninstances/db/access.phpnu[. /** * Capabilities * * @package report_questioninstances * @copyright 2008 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'report/questioninstances:view' => array( 'captype' => 'read', 'contextlevel' => CONTEXT_SYSTEM, 'archetypes' => array( 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/site:config', ) ); PK{\ 6questioninstances/lang/en/report_questioninstances.phpnu[. /** * lang strings * * @package report * @subpackage questioninstances * @copyright 2008 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['editquestionshere'] = 'Edit questions in this context'; $string['eventreportviewed'] = 'Report viewed'; $string['getreport'] = 'Get the report'; $string['hiddenquestions'] = 'Hidden'; $string['intro'] = 'This report lists all the contexts in the system where there are questions of a particular type.'; $string['pluginname'] = 'Question instances'; $string['questioninstances:view'] = 'View question instances report'; $string['reportforallqtypes'] = 'Report for all question types'; $string['reportformissingqtypes'] = 'Report for question of unknown types'; $string['reportforqtype'] = 'Report for question type \'{$a}\''; $string['reportsettings'] = 'Report settings'; $string['totalquestions'] = 'Total'; $string['visiblequestions'] = 'Visible'; $string['privacy:metadata'] = 'The Question instances plugin does not store any personal data.'; PK{\rynquestioninstances/version.phpnu[. /** * Version info * * @package report * @subpackage questioninstances * @copyright 2008 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_questioninstances'; // Full name of the plugin (used for diagnostics) PK{\]'.questioninstances/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_questioninstances. * * @package report_questioninstances * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_questioninstances\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_questioninstances implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\}. /** * The report_questioninstances report viewed event. * * @package report_questioninstances * @copyright 2014 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_questioninstances\event; defined('MOODLE_INTERNAL') || die(); /** * The report_questioninstances report viewed event class. * * @property-read array $other { * Extra information about the event. * * - string requestedqtype: Requested question type. * } * * @package report_questioninstances * @since Moodle 2.7 * @copyright 2014 Petr Skoda * @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() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; $this->context = \context_system::instance(); } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventreportviewed', 'report_questioninstances'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the question instances report."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/questioninstances/index.php', array('qtype' => $this->other['requestedqtype'])); } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['requestedqtype'])) { throw new \coding_exception('The \'requestedqtype\' value must be set in other.'); } } public static function get_other_mapping() { // Nothing to map. return false; } } PK{\_ttquestioninstances/index.phpnu[. /** * For a given question type, list the number of * * @package report * @subpackage questioninstances * @copyright 2008 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require(__DIR__.'/../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->libdir.'/questionlib.php'); // Get URL parameters. $requestedqtype = optional_param('qtype', '', PARAM_SAFEDIR); // Print the header & check permissions. admin_externalpage_setup('reportquestioninstances', '', null, '', array('pagelayout'=>'report')); $PAGE->set_primary_active_tab('siteadminnode'); echo $OUTPUT->header(); // Log. \report_questioninstances\event\report_viewed::create(array('other' => array('requestedqtype' => $requestedqtype)))->trigger(); // Prepare the list of capabilities to choose from $qtypes = question_bank::get_all_qtypes(); $qtypechoices = array(); foreach ($qtypes as $qtype) { $qtypechoices[$qtype->name()] = $qtype->local_name(); } // Sort by label. core_collator::asort($qtypechoices); // Print the settings form. echo $OUTPUT->box_start('generalbox boxwidthwide boxaligncenter centerpara'); echo '
    '; echo $OUTPUT->heading(get_string('reportsettings', 'report_questioninstances')); echo '

    ', get_string('intro', 'report_questioninstances') , '

    '; echo '

    '; echo html_writer::select($qtypechoices, 'qtype', $requestedqtype, array('_all_'=>get_string('all'))); echo '

    '; echo '

    '; echo '
    '; echo $OUTPUT->box_end(); $params[] = \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN; // If we have a qtype to report on, generate the report. if ($requestedqtype) { // Work out the bits needed for the SQL WHERE clauses. if ($requestedqtype == 'missingtype') { $title = get_string('reportformissingqtypes', 'report_questioninstances'); $othertypes = array_keys($qtypes); $key = array_search('missingtype', $othertypes); unset($othertypes[$key]); [$sqlqtypetest, $sqlqtypetestparams] = $DB->get_in_or_equal($othertypes, SQL_PARAMS_QM, '', false); $sqlqtypetest = 'WHERE qtype ' . $sqlqtypetest; $params = array_merge($params, $sqlqtypetestparams); } else if ($requestedqtype == '_all_') { $title = get_string('reportforallqtypes', 'report_questioninstances'); $sqlqtypetest = ''; } else { $title = get_string('reportforqtype', 'report_questioninstances', question_bank::get_qtype($requestedqtype)->local_name()); $sqlqtypetest = 'WHERE qtype = ?'; $params [] = $requestedqtype; } // Get the question counts, and all the context information, for each // context. That is, rows of these results can be used as $context objects. $ctxpreload = context_helper::get_preload_record_columns_sql('con'); $ctxgroupby = implode(',', array_keys(context_helper::get_preload_record_columns('con'))); $counts = $DB->get_records_sql(" SELECT result.contextid, SUM(numquestions) AS numquestions, SUM(numhidden) AS numhidden, $ctxpreload FROM (SELECT data.contextid, data.versionid, COUNT(data.numquestions) AS numquestions, (SELECT COUNT(qv.id) FROM {question_versions} qv WHERE qv.id = data.versionid AND qv.status = ?) AS numhidden FROM (SELECT qv.id as versionid, qc.contextid, 1 AS numquestions FROM {question} q JOIN {question_versions} qv ON qv.questionid = q.id JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid JOIN {context} con ON con.id = qc.contextid $sqlqtypetest AND qv.version = (SELECT MAX(v.version) FROM {question_versions} v JOIN {question_bank_entries} be ON be.id = v.questionbankentryid WHERE be.id = qbe.id) AND (q.parent = 0 OR q.parent = q.id)) data GROUP BY data.contextid, data.versionid) result JOIN {context} con ON con.id = result.contextid GROUP BY result.contextid, $ctxgroupby ORDER BY numquestions DESC, numhidden ASC, con.contextlevel ASC, con.id ASC", $params); // Print the report heading. echo $OUTPUT->heading($title); // Initialise the table. $table = new html_table(); $table->head = array( get_string('context', 'role'), get_string('totalquestions', 'report_questioninstances'), get_string('visiblequestions', 'report_questioninstances'), get_string('hiddenquestions', 'report_questioninstances')); $table->data = array(); $table->class = ''; $table->id = ''; // Add the data for each row. $totalquestions = 0; $totalvisible = 0; $totalhidden = 0; foreach ($counts as $count) { // Work out a link for editing questions in this context. context_helper::preload_from_record($count); $context = context::instance_by_id($count->contextid); $contextname = $context->get_context_name(); $url = question_edit_url($context); if ($url) { $contextname = '' . $contextname . ''; } // Put the scores in the table. $numvisible = $count->numquestions - $count->numhidden; $table->data[] = array( $contextname, $count->numquestions, $numvisible, $count->numhidden); // Update the totals. $totalquestions += $count->numquestions; $totalvisible += $numvisible; $totalhidden += $count->numhidden; } // Add a totals row. $table->data[] = array( '' . get_string('total') . '', $totalquestions, $totalvisible, $totalhidden); // Print it. echo html_writer::table($table); } // Footer. echo $OUTPUT->footer(); PK{\-questioninstances/tests/event/events_test.phpnu[. /** * Tests for question instances events. * * @package report_questioninstances * @copyright 2014 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace report_questioninstances\event; /** * Class for question instances events. * * @package report_questioninstances * @copyright 2014 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class events_test extends \advanced_testcase { /** * Setup testcase. */ public function setUp(): void { parent::setUp(); $this->setAdminUser(); $this->resetAfterTest(); } /** * Test the report viewed event. */ public function test_report_viewed(): void { $requestedqtype = 'all'; $event = \report_questioninstances\event\report_viewed::create(array('other' => array('requestedqtype' => $requestedqtype))); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $event = reset($events); $this->assertInstanceOf('\report_questioninstances\event\report_viewed', $event); $this->assertEquals(\context_system::instance(), $event->get_context()); $this->assertEventContextNotUsed($event); $url = new \moodle_url('/report/questioninstances/index.php', array('qtype' => $requestedqtype)); $this->assertEquals($url, $event->get_url()); $event->get_name(); } } PK{\ 6questioninstances/tests/behat/question_reports.featurenu[@core @core_question @report @report_questioninstance Feature: A Teacher can generate question instance reports In order to see question instance reports As a Teacher I need to generate them Background: Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | weeks | And the following "activities" exist: | activity | course | name | | quiz | C1 | Test quiz Q001 | And the following "questions" exist: | questioncategory | qtype | name | | Default for Test quiz Q001 | truefalse | TF | | Default for Test quiz Q001 | shortanswer | SA | And quiz "Test quiz Q001" contains the following questions: | question | page | maxmark | | TF | 1 | 5.0 | | SA | 1 | 5.0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | And the following "question categories" exist: | contextlevel | reference | name | | Activity module | Test quiz Q001 | Quiz category | Scenario: Generate general and specific report Given I am on the "C1" "Course" page logged in as "admin" And I navigate to "Reports > Question instances" in site administration # Checks the dropdown list is in alphabetical order And "Matching" "option" should appear before "Select missing words" "option" in the "Question type" "select" When I press "Get the report" Then "Quiz: Test quiz Q001" row "Total" column of "generaltable" table should contain "2" And "Quiz: Test quiz Q001" row "Visible" column of "generaltable" table should contain "2" And "Quiz: Test quiz Q001" row "Hidden" column of "generaltable" table should contain "0" Scenario: Generate report displaying hidden questions Given I am on the "Test quiz Q001" "mod_quiz > question bank" page logged in as "admin" And I choose "Delete" action for "TF" in the question bank And I press "Delete" And I navigate to "Reports > Question instances" in site administration When I press "Get the report" Then "Quiz: Test quiz Q001" row "Total" column of "generaltable" table should contain "2" And "Quiz: Test quiz Q001" row "Visible" column of "generaltable" table should contain "1" And "Quiz: Test quiz Q001" row "Hidden" column of "generaltable" table should contain "1" And I set the field "menuqtype" to "True/False" And I press "Get the report" And "Quiz: Test quiz Q001" row "Total" column of "generaltable" table should contain "1" And "Quiz: Test quiz Q001" row "Visible" column of "generaltable" table should contain "0" And "Quiz: Test quiz Q001" row "Hidden" column of "generaltable" table should contain "1" PK{\ZQquestioninstances/settings.phpnu[. /** * Settings and links * * @package report * @subpackage questioninstances * @copyright 2008 Tim Hunt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $ADMIN->add('reports', new admin_externalpage('reportquestioninstances', get_string('pluginname', 'report_questioninstances'), "$CFG->wwwroot/report/questioninstances/index.php", 'report/questioninstances:view')); // no report settings $settings = null; PK{\5l$EEcourseoverview/db/access.phpnu[. /** * Report capabilities * * @package report_courseoverview * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'report/courseoverview:view' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_SYSTEM, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'moodle/site:viewreports', ) ); PK{\}}0courseoverview/lang/en/report_courseoverview.phpnu[. /** * Strings for component 'report_courseoverview'. * * @package report * @subpackage courseoverview * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['courseoverview:view'] = 'View course overview report'; $string['pluginname'] = 'Course overview'; $string['privacy:metadata'] = 'The Course overview plugin does not store any personal data.'; PK{\KZkLoocourseoverview/reportsgraph.phpnu[. /** * Graph * * @package report * @subpackage courseoverview * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->libdir . '/filelib.php'); debugging('This way of generating the chart is deprecated, refer to report_courseoverview_print_chart().', DEBUG_DEVELOPER); send_file_not_found(); PK{\E̔bcourseoverview/version.phpnu[. /** * Version info * * @package report * @subpackage courseoverview * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_courseoverview'; // Full name of the plugin (used for diagnostics) PK{\@eU+courseoverview/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_courseoverview. * * @package report_courseoverview * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_courseoverview\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_courseoverview implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\t{ courseoverview/locallib.phpnu[. /** * This file contains functions used by the course overview report. * * @package report_courseoverview * @copyright 2016 Simey Lameze * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; require_once('../../config.php'); require_once($CFG->dirroot . '/lib/statslib.php'); /** * Gather course overview data and print the chart. * * @param int $report represents the report type field on the course overview report filter. * @param int $time represents the time period field on the course overview report filter. * @param int $numcourses represents the number of courses field on the course overview report filter. * @return void */ function report_courseoverview_print_chart($report, $time, $numcourses) { global $DB, $OUTPUT, $PAGE; $param = stats_get_parameters($time, $report, SITEID, STATS_MODE_RANKED); if (!empty($param->sql)) { $sql = $param->sql; } else { $sql = "SELECT courseid, $param->fields FROM {" . 'stats_' . $param->table . "} WHERE timeend >= $param->timeafter AND stattype = 'activity' AND roleid = 0 GROUP BY courseid $param->extras ORDER BY $param->orderby"; } $courses = $DB->get_records_sql($sql, $param->params, 0, $numcourses); if (empty($courses)) { $PAGE->set_url('/report/courseoverview/index.php'); throw new \moodle_exception('statsnodata', 'error', $PAGE->url->out()); } $data = []; $i = 0; foreach ($courses as $c) { $data['labels'][$i] = $DB->get_field('course', 'shortname', array('id' => $c->courseid)); // Line3 represents the third column of the report table except for the most active users report. // It is a float number and can be participation radio or activity per user. if (isset($c->line3)) { $data['series'][$i] = round($c->line3, 2); } else { $data['series'][$i] = $c->{$param->graphline}; } $i++; } $chart = new \core\chart_bar(); $series = new \core\chart_series($param->{$param->graphline}, $data['series']); $chart->add_series($series); $chart->set_labels($data['labels']); echo $OUTPUT->render($chart); } PK{\H-courseoverview/index.phpnu[. /** * Course overview report * * @package report * @subpackage courseoverview * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once('../../config.php'); require_once($CFG->dirroot.'/lib/statslib.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/report/courseoverview/locallib.php'); $report = optional_param('report', STATS_REPORT_ACTIVE_COURSES, PARAM_INT); $time = optional_param('time', 0, PARAM_INT); $numcourses = optional_param('numcourses', 20, PARAM_INT); $systemcontext = context_system::instance(); if (empty($CFG->enablestats)) { if (has_capability('moodle/site:config', $systemcontext)) { redirect("$CFG->wwwroot/$CFG->admin/search.php?query=enablestats", get_string('mustenablestats', 'admin'), 3); } else { throw new \moodle_exception('statsdisable'); } } admin_externalpage_setup('reportcourseoverview', '', null, '', array('pagelayout'=>'report')); echo $OUTPUT->header(); $course = get_site(); stats_check_uptodate($course->id); $strreports = get_string('reports'); $strcourseoverview = get_string('courseoverview'); $reportoptions = stats_get_report_options($course->id,STATS_MODE_RANKED); $earliestday = $DB->get_field_sql('SELECT MIN(timeend) FROM {stats_daily}'); $earliestweek = $DB->get_field_sql('SELECT MIN(timeend) FROM {stats_weekly}'); $earliestmonth = $DB->get_field_sql('SELECT MIN(timeend) FROM {stats_monthly}'); if (empty($earliestday)) $earliestday = time(); if (empty($earliestweek)) $earliestweek = time(); if (empty($earliestmonth)) $earliestmonth = time(); $now = stats_get_base_daily(); $lastweekend = stats_get_base_weekly(); $lastmonthend = stats_get_base_monthly(); $timeoptions = stats_get_time_options($now,$lastweekend,$lastmonthend,$earliestday,$earliestweek,$earliestmonth); if (empty($timeoptions)) { throw new \moodle_exception('nostatstodisplay', 'error', $CFG->wwwroot.'/course/view.php?id='.$course->id); } echo html_writer::start_tag('form', ['action' => 'index.php', 'method' => 'post', 'class' => 'd-flex flex-wrap align-items-center']); echo html_writer::start_tag('div'); $table = new html_table(); $table->width = '*'; $table->align = array('left','left','left','left','left','left'); $reporttypemenu = html_writer::label(get_string('statsreporttype'), 'menureport', false, array('class' => 'accesshide')); $reporttypemenu .= html_writer::select($reportoptions,'report',$report, false); $timeoptionsmenu = html_writer::label(get_string('time'), 'menutime', false, array('class' => 'accesshide')); $timeoptionsmenu .= html_writer::select($timeoptions,'time',$time, false); $table->data[] = array(get_string('statsreporttype'),$reporttypemenu, get_string('statstimeperiod'),$timeoptionsmenu, html_writer::label(get_string('numberofcourses'), 'numcourses', false, array('class' => 'accesshide')) . html_writer::empty_tag('input', array('type' => 'text', 'class' => 'form-control', 'id' => 'numcourses', 'name' => 'numcourses', 'size' => '3', 'maxlength' => '2', 'value' => $numcourses)), html_writer::empty_tag('input', array('type' => 'submit', 'class' => 'btn btn-secondary', 'value' => get_string('view')))); echo html_writer::table($table); echo html_writer::end_tag('div'); echo html_writer::end_tag('form'); echo $OUTPUT->heading($reportoptions[$report]); if (!empty($report) && !empty($time)) { $param = stats_get_parameters($time,$report,SITEID,STATS_MODE_RANKED); if (!empty($param->sql)) { $sql = $param->sql; } else { $sql = "SELECT courseid,".$param->fields." FROM {".'stats_'.$param->table."} WHERE timeend >= $param->timeafter AND stattype = 'activity' AND roleid = 0 GROUP BY courseid $param->extras ORDER BY $param->orderby"; } $courses = $DB->get_records_sql($sql, $param->params, 0, $numcourses); if (empty($courses)) { echo $OUTPUT->notification(get_string('statsnodata')); echo ''; } else { require_capability('report/courseoverview:view', $systemcontext); echo html_writer::start_div(); report_courseoverview_print_chart($report, $time, $numcourses); echo html_writer::end_div(); $table = new html_table(); $table->align = array('left','center','center','center'); $table->head = array(get_string('course'),$param->line1); if (!empty($param->line2)) { $table->head[] = $param->line2; } if (!empty($param->line3)) { $table->head[] = $param->line3; } foreach ($courses as $c) { $a = array(); $a[] = ''.$DB->get_field('course', 'shortname', array('id'=>$c->courseid)).''; $a[] = $c->line1; if (isset($c->line2)) { $a[] = $c->line2; } if (isset($c->line3)) { $a[] = round($c->line3,2); } $table->data[] = $a; } echo html_writer::table($table); } } echo $OUTPUT->footer(); PK{\zÚcourseoverview/settings.phpnu[. /** * Report settings * * @package report * @subpackage courseoverview * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $ADMIN->add('reports', new admin_externalpage('reportcourseoverview', get_string('pluginname', 'report_courseoverview'), "$CFG->wwwroot/report/courseoverview/index.php",'report/courseoverview:view', empty($CFG->enablestats))); // no report settings $settings = null; PK{\<<stats/styles.cssnu[#page-report-stats-index .graph { margin-bottom: 1em; } PK{\C stats/db/install.phpnu[. /** * Post installation and migration code. * * @package report * @subpackage stats * @copyright 2011 Petr Skoda {@link http://skodak.org} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; function xmldb_report_stats_install() { global $DB; } PK{\j..stats/db/access.phpnu[. /** * Plugin capabilities * * @package report_stats * @copyright 1999 onwards Martin Dougiamas http://moodle.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'report/stats:view' => array( 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_COURSE, 'archetypes' => array( 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ), 'clonepermissionsfrom' => 'coursereport/stats:view', ) ); PK{\՘stats/lang/en/report_stats.phpnu[. /** * Strings * * @package report * @subpackage stats * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['eventreportviewed'] = 'Statistics report viewed'; $string['eventuserreportviewed'] = 'User statistics report viewed'; $string['nocapability'] = 'Can not access user statistics report'; $string['pluginname'] = 'Statistics'; $string['page-report-stats-x'] = 'Any statistics report'; $string['page-report-stats-index'] = 'Course statistics report'; $string['page-report-stats-user'] = 'User course statistics report'; $string['stats:view'] = 'View course statistics report'; $string['privacy:metadata'] = 'The Statistics plugin does not store any personal data.'; $string['privacy:metadata:courseid'] = 'An identifier for a course'; $string['privacy:metadata:userid'] = 'The user ID linked to this table.'; $string['privacy:metadata:roleid'] = 'The role ID of the user.'; $string['privacy:metadata:timeend'] = 'End time of logs view'; $string['privacy:metadata:statsreads'] = 'Views of content'; $string['privacy:metadata:statswrites'] = 'Content made in the course.'; $string['privacy:metadata:stattype'] = 'The type of stat being recorded'; $string['privacy:metadata:statssummary'] = 'Records basic statistics about user interaction in courses.'; $string['privacy:weeklypath'] = 'Stats weekly'; $string['privacy:dailypath'] = 'Stats daily'; $string['privacy:monthlypath'] = 'Stats monthly'; PK{\o+K stats/lib.phpnu[. /** * This file contains functions used by the log reports * * This file is also required by /admin/reports/stats/index.php. * * @package report * @subpackage stats * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * This function extends the navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $course The course to object for the report * @param stdClass $context The context of the course */ function report_stats_extend_navigation_course($navigation, $course, $context) { global $CFG; if (empty($CFG->enablestats)) { return; } if (has_capability('report/stats:view', $context)) { $url = new moodle_url('/report/stats/index.php', array('course'=>$course->id)); $navigation->add(get_string('pluginname', 'report_stats'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/report', '')); } } /** * This function extends the course navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $user * @param stdClass $course The course to object for the report */ function report_stats_extend_navigation_user($navigation, $user, $course) { global $CFG; if (empty($CFG->enablestats)) { return; } if (report_stats_can_access_user_report($user, $course)) { $url = new moodle_url('/report/stats/user.php', array('id'=>$user->id, 'course'=>$course->id)); $navigation->add(get_string('stats'), $url); } } /** * Is current user allowed to access this report * * @private defined in lib.php for performance reasons * * @param stdClass $user * @param stdClass $course * @return bool */ function report_stats_can_access_user_report($user, $course) { global $USER; $coursecontext = context_course::instance($course->id); $personalcontext = context_user::instance($user->id); if ($user->id == $USER->id) { if ($course->showreports and (is_viewing($coursecontext, $USER) or is_enrolled($coursecontext, $USER))) { return true; } } else if (has_capability('moodle/user:viewuseractivitiesreport', $personalcontext)) { if ($course->showreports and (is_viewing($coursecontext, $user) or is_enrolled($coursecontext, $user))) { return true; } } // Check if $USER shares group with $user (in case separated groups are enabled and 'moodle/site:accessallgroups' is disabled). if (!groups_user_groups_visible($course, $user->id)) { return false; } if (has_capability('report/stats:view', $coursecontext)) { return true; } return false; } /** * Return a list of page types * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block * @return array */ function report_stats_page_type_list($pagetype, $parentcontext, $currentcontext) { $array = array( '*' => get_string('page-x', 'pagetype'), 'report-*' => get_string('page-report-x', 'pagetype'), 'report-stats-*' => get_string('page-report-stats-x', 'report_stats'), 'report-stats-index' => get_string('page-report-stats-index', 'report_stats'), 'report-stats-user' => get_string('page-report-stats-user', 'report_stats') ); return $array; } /** * Callback to verify if the given instance of store is supported by this report or not. * * @param string $instance store instance. * * @return bool returns true if the store is supported by the report, false otherwise. */ function report_stats_supports_logstore($instance) { if ($instance instanceof \core\log\sql_internal_table_reader) { return true; } return false; } /** * Add nodes to myprofile page. * * @param \core_user\output\myprofile\tree $tree Tree object * @param stdClass $user user object * @param bool $iscurrentuser * @param stdClass $course Course object * @return bool */ function report_stats_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) { global $CFG; if (empty($CFG->enablestats)) { return false; } if (empty($course)) { // We want to display these reports under the site context. $course = get_fast_modinfo(SITEID)->get_course(); } if (report_stats_can_access_user_report($user, $course)) { $url = new moodle_url('/report/stats/user.php', array('id' => $user->id, 'course' => $course->id)); $node = new core_user\output\myprofile\node('reports', 'stats', get_string('stats'), null, $url); $tree->add_node($node); } } PK{\P stats/version.phpnu[. /** * Version info * * @package report * @subpackage stats * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_stats'; // Full name of the plugin (used for diagnostics) PK{\$==f,f,"stats/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_stats. * * @package report_stats * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_stats\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\contextlist; use \core_privacy\local\request\approved_contextlist; use \core_privacy\local\request\userlist; use \core_privacy\local\request\approved_userlist; /** * Privacy Subsystem for report_stats implementing provider. * * @copyright 2018 Zig Tan * @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\subsystem\provider{ /** * Returns information about the user data stored in this component. * * @param collection $collection A list of information about this component * @return collection The collection object filled out with information about this component. */ public static function get_metadata(collection $collection): collection { $statsuserdaily = [ 'courseid' => 'privacy:metadata:courseid', 'userid' => 'privacy:metadata:userid', 'roleid' => 'privacy:metadata:roleid', 'timeend' => 'privacy:metadata:timeend', 'statsreads' => 'privacy:metadata:statsreads', 'statswrites' => 'privacy:metadata:statswrites', 'stattype' => 'privacy:metadata:stattype' ]; $statsuserweekly = [ 'courseid' => 'privacy:metadata:courseid', 'userid' => 'privacy:metadata:userid', 'roleid' => 'privacy:metadata:roleid', 'timeend' => 'privacy:metadata:timeend', 'statsreads' => 'privacy:metadata:statsreads', 'statswrites' => 'privacy:metadata:statswrites', 'stattype' => 'privacy:metadata:stattype' ]; $statsusermonthly = [ 'courseid' => 'privacy:metadata:courseid', 'userid' => 'privacy:metadata:userid', 'roleid' => 'privacy:metadata:roleid', 'timeend' => 'privacy:metadata:timeend', 'statsreads' => 'privacy:metadata:statsreads', 'statswrites' => 'privacy:metadata:statswrites', 'stattype' => 'privacy:metadata:stattype' ]; $collection->add_database_table('stats_user_daily', $statsuserdaily, 'privacy:metadata:statssummary'); $collection->add_database_table('stats_user_weekly', $statsuserweekly, 'privacy:metadata:statssummary'); $collection->add_database_table('stats_user_monthly', $statsusermonthly, 'privacy:metadata:statssummary'); 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 { $params = ['userid' => $userid, 'contextcourse' => CONTEXT_COURSE]; $sql = "SELECT ctx.id FROM {context} ctx JOIN {stats_user_daily} sud ON sud.courseid = ctx.instanceid AND sud.userid = :userid WHERE ctx.contextlevel = :contextcourse"; $contextlist = new contextlist(); $contextlist->add_from_sql($sql, $params); $sql = "SELECT ctx.id FROM {context} ctx JOIN {stats_user_weekly} suw ON suw.courseid = ctx.instanceid AND suw.userid = :userid WHERE ctx.contextlevel = :contextcourse"; $contextlist->add_from_sql($sql, $params); $sql = "SELECT ctx.id FROM {context} ctx JOIN {stats_user_monthly} sum ON sum.courseid = ctx.instanceid AND sum.userid = :userid WHERE ctx.contextlevel = :contextcourse"; $contextlist->add_from_sql($sql, $params); return $contextlist; } /** * Get the list of users within a specific 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 (!$context instanceof \context_course) { return; } $params = ['courseid' => $context->instanceid]; $sql = "SELECT userid FROM {stats_user_daily} WHERE courseid = :courseid"; $userlist->add_from_sql('userid', $sql, $params); $sql = "SELECT userid FROM {stats_user_weekly} WHERE courseid = :courseid"; $userlist->add_from_sql('userid', $sql, $params); $sql = "SELECT userid FROM {stats_user_monthly} WHERE courseid = :courseid"; $userlist->add_from_sql('userid', $sql, $params); } /** * 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; // Some sneeky person might have sent us the wrong context list. We should check. if ($contextlist->get_component() != 'report_stats') { return; } // Got to check that someone hasn't foolishly added a context between creating the context list and then filtering down // to an approved context. $contexts = array_filter($contextlist->get_contexts(), function($context) { if ($context->contextlevel == CONTEXT_COURSE) { return $context; } }); $tables = [ 'stats_user_daily' => get_string('privacy:dailypath', 'report_stats'), 'stats_user_weekly' => get_string('privacy:weeklypath', 'report_stats'), 'stats_user_monthly' => get_string('privacy:monthlypath', 'report_stats') ]; $courseids = array_map(function($context) { return $context->instanceid; }, $contexts); foreach ($tables as $table => $path) { list($insql, $params) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED); $sql = "SELECT s.id, c.fullname, s.roleid, s.timeend, s.statsreads, s.statswrites, s.stattype, c.id as courseid FROM {" . $table . "} s JOIN {course} c ON s.courseid = c.id WHERE s.userid = :userid AND c.id $insql ORDER BY c.id ASC"; $params['userid'] = $contextlist->get_user()->id; $records = $DB->get_records_sql($sql, $params); $statsrecords = []; foreach ($records as $record) { $context = \context_course::instance($record->courseid); if (!isset($statsrecords[$record->courseid])) { $statsrecords[$record->courseid] = new \stdClass(); $statsrecords[$record->courseid]->context = $context; } $statsrecords[$record->courseid]->entries[] = [ 'course' => format_string($record->fullname, true, ['context' => $context]), 'roleid' => $record->roleid, 'timeend' => \core_privacy\local\request\transform::datetime($record->timeend), 'statsreads' => $record->statsreads, 'statswrites' => $record->statswrites, 'stattype' => $record->stattype ]; } foreach ($statsrecords as $coursestats) { \core_privacy\local\request\writer::with_context($coursestats->context)->export_data([$path], (object) $coursestats->entries); } } } /** * Delete all data for all users in the specified context. * * @param context $context The specific context to delete data for. */ public static function delete_data_for_all_users_in_context(\context $context) { // Check that this context is a course context. if ($context->contextlevel == CONTEXT_COURSE) { static::delete_stats($context->instanceid); } } /** * 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) { if ($contextlist->get_component() != 'report_stats') { return; } foreach ($contextlist->get_contexts() as $context) { if ($context->contextlevel == CONTEXT_COURSE) { static::delete_stats($context->instanceid, $contextlist->get_user()->id); } } } /** * 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) { global $DB; $context = $userlist->get_context(); if ($context instanceof \context_course) { list($usersql, $userparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED); $select = "courseid = :courseid AND userid {$usersql}"; $params = ['courseid' => $context->instanceid] + $userparams; $DB->delete_records_select('stats_user_daily', $select, $params); $DB->delete_records_select('stats_user_weekly', $select, $params); $DB->delete_records_select('stats_user_monthly', $select, $params); } } /** * Deletes stats for a given course. * * @param int $courseid The course ID to delete the stats for. * @param int $userid Optionally a user id to delete records with. */ protected static function delete_stats(int $courseid, ?int $userid = null) { global $DB; $params = (isset($userid)) ? ['courseid' => $courseid, 'userid' => $userid] : ['courseid' => $courseid]; $DB->delete_records('stats_user_daily', $params); $DB->delete_records('stats_user_weekly', $params); $DB->delete_records('stats_user_monthly', $params); } } PK{\tw %stats/classes/event/report_viewed.phpnu[. /** * The report_stats report viewed event. * * @package report_stats * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_stats\event; defined('MOODLE_INTERNAL') || die(); /** * The report_stats report viewed event class. * * @property-read array $other { * Extra information about the event. * * - int report: (optional) Report type. * - int time: (optional) Time from which report is viewed. * - int mode: (optional) Report mode. * } * * @package report_stats * @since Moodle 2.7 * @copyright 2013 Ankit Agarwal * @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() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventreportviewed', 'report_stats'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the statistics report for the course with id '$this->courseid'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/stats/index.php', array('id' => $this->courseid, 'mode' => $this->other['mode'], 'report' => $this->other['report'])); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['report'])) { throw new \coding_exception('The \'report\' value must be set in other.'); } if (!isset($this->other['time'])) { throw new \coding_exception('The \'time\' value must be set in other.'); } if (!isset($this->other['mode'])) { throw new \coding_exception('The \'mode\' value must be set in other.'); } if (!isset($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } } public static function get_other_mapping() { // Nothing to map. return array(); } } PK{\=ω *stats/classes/event/user_report_viewed.phpnu[. /** * The report_stats user report viewed event. * * @package report_stats * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_stats\event; defined('MOODLE_INTERNAL') || die(); /** * The report_stats user report viewed event class. * * @package report_stats * @since Moodle 2.7 * @copyright 2013 Ankit Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_report_viewed extends \core\event\base { /** * Init method. * * @return void */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventuserreportviewed', 'report_stats'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the user statistics report for the user with id '$this->relateduserid'."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/stats/user.php', array('id' => $this->relateduserid, 'course' => $this->courseid)); } /** * Custom validation. * * @throws \coding_exception * @return void */ protected function validate_data() { parent::validate_data(); if (empty($this->relateduserid)) { throw new \coding_exception('The \'relateduserid\' must be set.'); } } } PK{\4%0Q0Qstats/locallib.phpnu[. /** * Reports implementation * * @package report * @subpackage stats * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; require_once(__DIR__.'/lib.php'); require_once($CFG->dirroot.'/lib/statslib.php'); function report_stats_mode_menu($course, $mode, $time, $url) { global $CFG, $OUTPUT; /* $reportoptions = stats_get_report_options($course->id, $mode); $timeoptions = report_stats_timeoptions($mode); if (empty($timeoptions)) { throw new \moodle_exception('nostatstodisplay', '', $CFG->wwwroot.'/course/view.php?id='.$course->id); } */ $options = array(); $options[STATS_MODE_GENERAL] = get_string('statsmodegeneral'); $options[STATS_MODE_DETAILED] = get_string('statsmodedetailed'); if (has_capability('report/stats:view', context_system::instance())) { $options[STATS_MODE_RANKED] = get_string('reports'); } $popupurl = $url."?course=$course->id&time=$time"; $select = new single_select(new moodle_url($popupurl), 'mode', $options, $mode, null); $select->set_label(get_string('reports'), array('class' => 'accesshide')); $select->formid = 'switchmode'; return $OUTPUT->render($select); } function report_stats_timeoptions($mode) { global $CFG, $DB; if ($mode == STATS_MODE_DETAILED) { $earliestday = $DB->get_field_sql('SELECT MIN(timeend) FROM {stats_user_daily}'); $earliestweek = $DB->get_field_sql('SELECT MIN(timeend) FROM {stats_user_weekly}'); $earliestmonth = $DB->get_field_sql('SELECT MIN(timeend) FROM {stats_user_monthly}'); } else { $earliestday = $DB->get_field_sql('SELECT MIN(timeend) FROM {stats_daily}'); $earliestweek = $DB->get_field_sql('SELECT MIN(timeend) FROM {stats_weekly}'); $earliestmonth = $DB->get_field_sql('SELECT MIN(timeend) FROM {stats_monthly}'); } if (empty($earliestday)) $earliestday = time(); if (empty($earliestweek)) $earliestweek = time(); if (empty($earliestmonth)) $earliestmonth = time(); $now = stats_get_base_daily(); $lastweekend = stats_get_base_weekly(); $lastmonthend = stats_get_base_monthly(); return stats_get_time_options($now,$lastweekend,$lastmonthend,$earliestday,$earliestweek,$earliestmonth); } function report_stats_report($course, $report, $mode, $user, $roleid, $time) { global $CFG, $DB, $OUTPUT; if ($user) { $userid = $user->id; } else { $userid = 0; } $fields = 'c.id,c.shortname,c.visible'; $ccselect = ', ' . context_helper::get_preload_record_columns_sql('ctx'); $ccjoin = "LEFT JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :contextlevel)"; $sortstatement = 'ORDER BY c.shortname'; $sql = "SELECT $fields $ccselect FROM {course} c $ccjoin $sortstatement"; $params = array(); $params['contextlevel'] = CONTEXT_COURSE; $courses = $DB->get_recordset_sql($sql, $params); $courseoptions = array(); foreach ($courses as $c) { context_helper::preload_from_record($c); $context = context_course::instance($c->id); if (has_capability('report/stats:view', $context)) { if (isset($c->visible) && $c->visible <= 0) { // For hidden courses, require visibility check. if (!has_capability('moodle/course:viewhiddencourses', $context)) { continue; } } $courseoptions[$c->id] = format_string($c->shortname, true, array('context' => $context)); } } $courses->close(); $reportoptions = stats_get_report_options($course->id, $mode); $timeoptions = report_stats_timeoptions($mode); if (empty($timeoptions)) { throw new \moodle_exception('nostatstodisplay', '', $CFG->wwwroot.'/course/view.php?id='.$course->id); } $users = array(); $table = new html_table(); $table->width = 'auto'; if ($mode == STATS_MODE_DETAILED) { $param = stats_get_parameters($time, null, $course->id, $mode, $roleid); // We only care about the table and the time string (if we have time). list($sort, $moreparams) = users_order_by_sql('u'); $moreparams['courseid'] = $course->id; $userfieldsapi = \core_user\fields::for_userpic()->including('idnumber'); $fields = $userfieldsapi->get_sql('u', false, '', '', false)->selects; $sql = "SELECT DISTINCT $fields FROM {stats_user_{$param->table}} s JOIN {user} u ON u.id = s.userid WHERE courseid = :courseid"; if (!empty($param->stattype)) { $sql .= " AND stattype = :stattype"; $moreparams['stattype'] = $param->stattype; } if (!empty($time)) { $sql .= " AND timeend >= :timeafter"; $moreparams['timeafter'] = $param->timeafter; } $sql .= " ORDER BY {$sort}"; if (!$us = $DB->get_records_sql($sql, array_merge($param->params, $moreparams))) { throw new \moodle_exception('nousers'); } foreach ($us as $u) { $users[$u->id] = fullname($u, true); } $table->align = array('left','left','left','left','left','left','left','left'); $table->data[] = array(html_writer::label(get_string('course'), 'menucourse'), html_writer::select($courseoptions, 'course', $course->id, false), html_writer::label(get_string('users'), 'menuuserid'), html_writer::select($users, 'userid', $userid, false), html_writer::label(get_string('statsreporttype'), 'menureport'), html_writer::select($reportoptions, 'report', ($report == 5) ? $report.$roleid : $report, false), html_writer::label(get_string('statstimeperiod'), 'menutime'), html_writer::select($timeoptions, 'time', $time, false), ''); } else if ($mode == STATS_MODE_RANKED) { $table->align = array('left','left','left','left','left','left'); $table->data[] = array(html_writer::label(get_string('statsreporttype'), 'menureport'), html_writer::select($reportoptions, 'report', ($report == 5) ? $report.$roleid : $report, false), html_writer::label(get_string('statstimeperiod'), 'menutime'), html_writer::select($timeoptions, 'time', $time, false), ''); } else if ($mode == STATS_MODE_GENERAL) { $table->align = array('left','left','left','left','left','left','left'); $table->data[] = array(html_writer::label(get_string('course'), 'menucourse'), html_writer::select($courseoptions, 'course', $course->id, false), html_writer::label(get_string('statsreporttype'), 'menureport'), html_writer::select($reportoptions, 'report', ($report == 5) ? $report.$roleid : $report, false), html_writer::label(get_string('statstimeperiod'), 'menutime'), html_writer::select($timeoptions, 'time', $time, false), ''); } echo '
    '."\n" .'
    '."\n" .''."\n"; echo html_writer::table($table); echo '
    '; echo '
    '; // Display the report if: // - A report has been selected. // - A time frame has been provided // - If the mode is not detailed OR a valid user has been selected. if (!empty($report) && !empty($time) && ($mode !== STATS_MODE_DETAILED || !empty($userid))) { if ($report == STATS_REPORT_LOGINS && $course->id != SITEID) { throw new \moodle_exception('reportnotavailable'); } $param = stats_get_parameters($time, $report, $course->id, $mode, $roleid); if ($mode == STATS_MODE_DETAILED) { $param->table = 'user_'.$param->table; } if (!empty($param->sql)) { $sql = $param->sql; } else { //TODO: lceanup this ugly mess $sql = 'SELECT '.((empty($param->fieldscomplete)) ? 'id,roleid,timeend,' : '').$param->fields .' FROM {stats_'.$param->table.'} WHERE ' .(($course->id == SITEID) ? '' : ' courseid = '.$course->id.' AND ') .((!empty($userid)) ? ' userid = '.$userid.' AND ' : '') .((!empty($roleid)) ? ' roleid = '.$roleid.' AND ' : '') . ((!empty($param->stattype)) ? ' stattype = \''.$param->stattype.'\' AND ' : '') .' timeend >= '.$param->timeafter .' '.$param->extras .' ORDER BY timeend DESC'; } $stats = $DB->get_records_sql($sql); if (empty($stats)) { echo $OUTPUT->notification(get_string('statsnodata')); } else { $stats = stats_fix_zeros($stats,$param->timeafter,$param->table,(!empty($param->line2))); $rolename = ''; $userdisplayname = ''; $coursecontext = context_course::instance($course->id); if (!empty($roleid) && $role = $DB->get_record('role', ['id' => $roleid])) { $rolename = ' ' . role_get_name($role, $coursecontext); } if (!empty($user)) { $userdisplayname = ' ' . fullname($user, true); } echo $OUTPUT->heading( format_string($course->shortname) . ' - ' . get_string('statsreport' . $report) . $rolename . $userdisplayname, 3 ); if ($mode == STATS_MODE_DETAILED) { report_stats_print_chart($course->id, $report, $time, $mode, $userid); } else { report_stats_print_chart($course->id, $report, $time, $mode, null, $roleid); } $table = new html_table(); $table->align = array('left','center','center','center'); $param->table = str_replace('user_','',$param->table); switch ($param->table) { case 'daily' : $period = get_string('day'); break; case 'weekly' : $period = get_string('week'); break; case 'monthly': $period = get_string('month', 'form'); break; default : $period = ''; } $table->head = array(get_string('periodending','moodle',$period)); if (empty($param->crosstab)) { $table->head[] = $param->line1; if (!empty($param->line2)) { $table->head[] = $param->line2; } } if (!file_exists($CFG->dirroot.'/report/log/index.php')) { // bad luck, we can not link other report } else if (empty($param->crosstab)) { foreach ($stats as $stat) { $a = array(userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone), $stat->line1); if (isset($stat->line2)) { $a[] = $stat->line2; } if (empty($CFG->loglifetime) || ($stat->timeend-(60*60*24)) >= (time()-60*60*24*$CFG->loglifetime)) { if (has_capability('report/log:view', context_course::instance($course->id))) { $a[] = '' .get_string('course').' ' .get_string('logs').' '; } else { $a[] = ''; } } $table->data[] = $a; } } else { $data = array(); $roles = array(); $times = array(); $missedlines = array(); $coursecontext = context_course::instance($course->id); $rolenames = get_viewable_roles($coursecontext); foreach ($stats as $stat) { if (!empty($stat->zerofixed)) { $missedlines[] = $stat->timeend; } $data[$stat->timeend][$stat->roleid] = $stat->line1; if ($stat->roleid != 0) { if (!array_key_exists($stat->roleid, $roles) && array_key_exists($stat->roleid, $rolenames)) { $roles[$stat->roleid] = $rolenames[$stat->roleid]; } } else { if (!array_key_exists($stat->roleid,$roles)) { $roles[$stat->roleid] = get_string('all'); } } if (!array_key_exists($stat->timeend,$times)) { $times[$stat->timeend] = userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone); } } foreach ($data as $time => $rolesdata) { if (in_array($time,$missedlines)) { $rolesdata = array(); foreach ($roles as $roleid => $guff) { $rolesdata[$roleid] = 0; } } else { foreach (array_keys($roles) as $r) { if (!array_key_exists($r, $rolesdata)) { $rolesdata[$r] = 0; } } } krsort($rolesdata); $row = array_merge(array($times[$time]),$rolesdata); if (empty($CFG->loglifetime) || ($stat->timeend-(60*60*24)) >= (time()-60*60*24*$CFG->loglifetime)) { if (has_capability('report/log:view', context_course::instance($course->id))) { $row[] = '' .get_string('course').' ' .get_string('logs').' '; } else { $row[] = ''; } } $table->data[] = $row; } krsort($roles); $table->head = array_merge($table->head,$roles); } $table->head[] = get_string('logs'); //if (!empty($lastrecord)) { //$lastrecord[] = $lastlink; //$table->data[] = $lastrecord; //} echo html_writer::table($table); } } } /** * Fetch statistics data and generate a line chart. * * The statistic chart can be view, posts separated by roles and dates. * * @param int $courseid course id. * @param int $report the report type constant eg. STATS_REPORT_LOGINS as defined on statslib. * @param int $time timestamp of the selected time period. * @param int $mode the report mode, eg. STATS_MODE_DETAILED as defined on statslib. * @param int $userid selected user id. * @param int $roleid selected role id. */ function report_stats_print_chart($courseid, $report, $time, $mode, $userid = 0, $roleid = 0) { global $DB, $CFG, $OUTPUT; $course = $DB->get_record("course", array("id" => $courseid), '*', MUST_EXIST); $coursecontext = context_course::instance($course->id); stats_check_uptodate($course->id); $param = stats_get_parameters($time, $report, $course->id, $mode, $roleid); if (!empty($userid)) { $param->table = 'user_' . $param->table; } // TODO: cleanup this ugly mess. $sql = 'SELECT '.((empty($param->fieldscomplete)) ? 'id,roleid,timeend,' : '').$param->fields .' FROM {stats_'.$param->table.'} WHERE ' .(($course->id == SITEID) ? '' : ' courseid = '.$course->id.' AND ') .((!empty($userid)) ? ' userid = '.$userid.' AND ' : '') .((!empty($roleid)) ? ' roleid = '.$roleid.' AND ' : '') . ((!empty($param->stattype)) ? ' stattype = \''.$param->stattype.'\' AND ' : '') .' timeend >= '.$param->timeafter .' '.$param->extras .' ORDER BY timeend DESC'; $stats = $DB->get_records_sql($sql, $param->params); $stats = stats_fix_zeros($stats, $param->timeafter, $param->table, (!empty($param->line2)), (!empty($param->line3))); $stats = array_reverse($stats); $chart = new \core\chart_line(); if (empty($param->crosstab)) { $data = []; $times = []; foreach ($stats as $stat) { // Build the array of formatted times indexed by timestamp used as labels. if (!array_key_exists($stat->timeend, $times)) { $times[$stat->timeend] = userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone); // Just add the data if the time hasn't been added yet. // The number of lines of data must match the number of labels. $data['line1'][] = $stat->line1; if (isset($stat->line2)) { $data['line2'][] = $stat->line2; } if (isset($stat->line3)) { $data['line3'][] = $stat->line3; } } } foreach ($data as $line => $serie) { $series = new \core\chart_series($param->{$line}, array_values($serie)); $chart->add_series($series); } } else { $data = array(); $times = array(); $roles = array(); $missedlines = array(); $rolenames = get_viewable_roles($coursecontext); foreach ($stats as $stat) { $data[$stat->roleid][$stat->timeend] = $stat->line1; if (!empty($stat->zerofixed)) { $missedlines[] = $stat->timeend; } if ($stat->roleid != 0) { if (!array_key_exists($stat->roleid, $roles) && array_key_exists($stat->roleid, $rolenames)) { $roles[$stat->roleid] = $rolenames[$stat->roleid]; } } else { if (!array_key_exists($stat->roleid, $roles)) { $roles[$stat->roleid] = get_string('all'); } } // Build the array of formatted times indexed by timestamp used as labels. if (!array_key_exists($stat->timeend, $times)) { $times[$stat->timeend] = userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone); } } // Fill empty days with zero to avoid chart errors. foreach (array_keys($times) as $t) { foreach ($data as $roleid => $stuff) { if (!array_key_exists($t, $stuff)) { $data[$roleid][$t] = 0; } } } krsort($roles); foreach ($roles as $roleid => $rolename) { ksort($data[$roleid]); $series = new \core\chart_series($rolename, array_values($data[$roleid])); $chart->add_series($series); } } $chart->set_labels(array_values($times)); echo $OUTPUT->render_chart($chart, false); } PK{\$stats/index.phpnu[. /** * stats report * * @package report * @subpackage stats * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\report_helper; require_once('../../config.php'); require_once($CFG->dirroot.'/report/stats/locallib.php'); require_once($CFG->libdir.'/adminlib.php'); $courseid = optional_param('course', SITEID, PARAM_INT); $report = optional_param('report', 0, PARAM_INT); $time = optional_param('time', 0, PARAM_INT); $mode = optional_param('mode', STATS_MODE_GENERAL, PARAM_INT); $userid = optional_param('userid', 0, PARAM_INT); $roleid = 0; if ($report > 50) { $roleid = substr($report,1); $report = 5; } if ($report == STATS_REPORT_USER_LOGINS) { $courseid = SITEID; //override } if ($mode == STATS_MODE_RANKED) { redirect($CFG->wwwroot.'/report/stats/index.php?time='.$time); } if (!$course = $DB->get_record("course", array("id"=>$courseid))) { throw new \moodle_exception("invalidcourseid"); } if (!empty($userid)) { $user = $DB->get_record('user', array('id'=>$userid), '*', MUST_EXIST); } else { $user = null; } require_login($course); $context = context_course::instance($course->id); require_capability('report/stats:view', $context); $PAGE->set_url(new moodle_url('/report/stats/index.php', array('course' => $course->id, 'report' => $report, 'time' => $time, 'mode' => $mode, 'userid' => $userid))); navigation_node::override_active_url(new moodle_url('/report/stats/index.php', array('course' => $course->id))); // Trigger a content view event. $event = \report_stats\event\report_viewed::create(array('context' => $context, 'relateduserid' => $userid, 'other' => array('report' => $report, 'time' => $time, 'mode' => $mode))); $event->trigger(); stats_check_uptodate($course->id); if ($course->id == SITEID) { admin_externalpage_setup('reportstats', '', null, '', array('pagelayout'=>'report')); echo $OUTPUT->header(); } else { $strreports = get_string("reports"); $strstats = get_string('stats'); $PAGE->set_title("$course->shortname: $strstats"); $PAGE->set_heading($course->fullname); $PAGE->set_pagelayout('report'); echo $OUTPUT->header(); // Print the selected dropdown. report_helper::print_report_selector( get_string('pluginname', 'report_stats'), report_stats_mode_menu($course, $mode, $time, $PAGE->url->out_omit_querystring()) ); } report_stats_report($course, $report, $mode, $user, $roleid, $time); if (empty($CFG->enablestats)) { if (has_capability('moodle/site:config', context_system::instance())) { redirect("$CFG->wwwroot/$CFG->admin/settings.php?section=stats", get_string('mustenablestats', 'admin'), 3); } else { throw new \moodle_exception('statsdisable'); } } echo $OUTPUT->footer(); PK{\V{{stats/graph.phpnu[. /** * This file is part of the User section Moodle * * @package report * @subpackage stats * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../../config.php'); require_once($CFG->libdir . '/filelib.php'); debugging('This way of generating the chart is deprecated, refer to report_stats_print_chart().', DEBUG_DEVELOPER); send_file_not_found(); PK{\,4949%stats/tests/privacy/provider_test.phpnu[. /** * Tests for privacy functions. * * @package report_stats * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace report_stats\privacy; use report_stats\privacy\provider; use core_privacy\local\request\approved_userlist; use core_privacy\tests\provider_testcase; /** * Privacy provider test for report_stats. * * @package report_stats * @copyright 2018 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class provider_test extends provider_testcase { /** * Convenience function to create stats. * * @param int $courseid Course ID for this record. * @param int $userid User ID for this record. * @param string $table Stat table to insert into. */ protected function create_stats($courseid, $userid, $table) { global $DB; $data = (object) [ 'courseid' => $courseid, 'userid' => $userid, 'roleid' => 0, 'timeend' => time(), 'statsreads' => rand(1, 50), 'statswrites' => rand(1, 50), 'stattype' => 'activity' ]; $DB->insert_record($table, $data); } /** * Get all of the contexts related to a user and stat tables. */ public function test_get_contexts_for_userid(): void { $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $user3 = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $course3 = $this->getDataGenerator()->create_course(); $context1 = \context_course::instance($course1->id); $context2 = \context_course::instance($course2->id); $context3 = \context_course::instance($course3->id); $this->create_stats($course1->id, $user1->id, 'stats_user_daily'); $this->create_stats($course2->id, $user1->id, 'stats_user_monthly'); $this->create_stats($course1->id, $user2->id, 'stats_user_weekly'); $contextlist = provider::get_contexts_for_userid($user1->id); $this->assertCount(2, $contextlist->get_contextids()); foreach ($contextlist->get_contexts() as $context) { $this->assertEquals(CONTEXT_COURSE, $context->contextlevel); $this->assertNotEquals($context3, $context); } $contextlist = provider::get_contexts_for_userid($user2->id); $this->assertCount(1, $contextlist->get_contextids()); $this->assertEquals($context1, $contextlist->current()); } /** * Test that stat data is exported as required. */ public function test_export_user_data(): void { $this->resetAfterTest(); $user = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $context1 = \context_course::instance($course1->id); $context2 = \context_course::instance($course2->id); $this->create_stats($course1->id, $user->id, 'stats_user_daily'); $this->create_stats($course1->id, $user->id, 'stats_user_daily'); $this->create_stats($course2->id, $user->id, 'stats_user_weekly'); $this->create_stats($course2->id, $user->id, 'stats_user_monthly'); $this->create_stats($course1->id, $user->id, 'stats_user_monthly'); $approvedlist = new \core_privacy\local\request\approved_contextlist($user, 'report_stats', [$context1->id, $context2->id]); provider::export_user_data($approvedlist); $writer = \core_privacy\local\request\writer::with_context($context1); $dailystats = (array) $writer->get_data([get_string('privacy:dailypath', 'report_stats')]); $this->assertCount(2, $dailystats); $monthlystats = (array) $writer->get_data([get_string('privacy:monthlypath', 'report_stats')]); $this->assertCount(1, $monthlystats); $data = array_shift($monthlystats); $this->assertEquals($course1->fullname, $data['course']); $writer = \core_privacy\local\request\writer::with_context($context2); $monthlystats = (array) $writer->get_data([get_string('privacy:monthlypath', 'report_stats')]); $this->assertCount(1, $monthlystats); $data = array_shift($monthlystats); $this->assertEquals($course2->fullname, $data['course']); $weeklystats = (array) $writer->get_data([get_string('privacy:weeklypath', 'report_stats')]); $this->assertCount(1, $weeklystats); $data = array_shift($weeklystats); $this->assertEquals($course2->fullname, $data['course']); } /** * Test that stat data is deleted for a whole context. */ public function test_delete_data_for_all_users_in_context(): void { global $DB; $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $context1 = \context_course::instance($course1->id); $context2 = \context_course::instance($course2->id); $this->create_stats($course1->id, $user1->id, 'stats_user_daily'); $this->create_stats($course1->id, $user1->id, 'stats_user_daily'); $this->create_stats($course1->id, $user1->id, 'stats_user_monthly'); $this->create_stats($course1->id, $user2->id, 'stats_user_weekly'); $this->create_stats($course2->id, $user2->id, 'stats_user_daily'); $this->create_stats($course2->id, $user2->id, 'stats_user_weekly'); $this->create_stats($course2->id, $user2->id, 'stats_user_monthly'); $dailyrecords = $DB->get_records('stats_user_daily'); $this->assertCount(3, $dailyrecords); $weeklyrecords = $DB->get_records('stats_user_weekly'); $this->assertCount(2, $weeklyrecords); $monthlyrecords = $DB->get_records('stats_user_monthly'); $this->assertCount(2, $monthlyrecords); // Delete all user data for course 1. provider::delete_data_for_all_users_in_context($context1); $dailyrecords = $DB->get_records('stats_user_daily'); $this->assertCount(1, $dailyrecords); $weeklyrecords = $DB->get_records('stats_user_weekly'); $this->assertCount(1, $weeklyrecords); $monthlyrecords = $DB->get_records('stats_user_monthly'); $this->assertCount(1, $monthlyrecords); } /** * Test that stats are deleted for one user. */ public function test_delete_data_for_user(): void { global $DB; $this->resetAfterTest(); $user1 = $this->getDataGenerator()->create_user(); $user2 = $this->getDataGenerator()->create_user(); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $context1 = \context_course::instance($course1->id); $context2 = \context_course::instance($course2->id); $this->create_stats($course1->id, $user1->id, 'stats_user_daily'); $this->create_stats($course1->id, $user1->id, 'stats_user_daily'); $this->create_stats($course1->id, $user1->id, 'stats_user_monthly'); $this->create_stats($course1->id, $user2->id, 'stats_user_weekly'); $this->create_stats($course2->id, $user2->id, 'stats_user_daily'); $this->create_stats($course2->id, $user2->id, 'stats_user_weekly'); $this->create_stats($course2->id, $user2->id, 'stats_user_monthly'); $dailyrecords = $DB->get_records('stats_user_daily'); $this->assertCount(3, $dailyrecords); $weeklyrecords = $DB->get_records('stats_user_weekly'); $this->assertCount(2, $weeklyrecords); $monthlyrecords = $DB->get_records('stats_user_monthly'); $this->assertCount(2, $monthlyrecords); // Delete all user data for course 1. $approvedlist = new \core_privacy\local\request\approved_contextlist($user1, 'report_stats', [$context1->id]); provider::delete_data_for_user($approvedlist); $dailyrecords = $DB->get_records('stats_user_daily'); $this->assertCount(1, $dailyrecords); $weeklyrecords = $DB->get_records('stats_user_weekly'); $this->assertCount(2, $weeklyrecords); $monthlyrecords = $DB->get_records('stats_user_monthly'); $this->assertCount(1, $monthlyrecords); } /** * Test that only users within a course context are fetched. */ public function test_get_users_in_context(): void { $this->resetAfterTest(); $component = 'report_stats'; // Create user1. $user1 = $this->getDataGenerator()->create_user(); // Create user2. $user2 = $this->getDataGenerator()->create_user(); // Create course1. $course1 = $this->getDataGenerator()->create_course(); $coursecontext1 = \context_course::instance($course1->id); // Create course2. $course2 = $this->getDataGenerator()->create_course(); $coursecontext2 = \context_course::instance($course2->id); $userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(0, $userlist1); $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(0, $userlist2); $this->create_stats($course1->id, $user1->id, 'stats_user_daily'); $this->create_stats($course2->id, $user1->id, 'stats_user_monthly'); $this->create_stats($course1->id, $user2->id, 'stats_user_weekly'); // The list of users within the course context should contain users. provider::get_users_in_context($userlist1); $this->assertCount(2, $userlist1); $this->assertTrue(in_array($user1->id, $userlist1->get_userids())); $this->assertTrue(in_array($user2->id, $userlist1->get_userids())); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); $this->assertTrue(in_array($user1->id, $userlist2->get_userids())); // The list of users within other contexts than course should be empty. $systemcontext = \context_system::instance(); $userlist3 = new \core_privacy\local\request\userlist($systemcontext, $component); provider::get_users_in_context($userlist3); $this->assertCount(0, $userlist3); } /** * Test that data for users in approved userlist is deleted. */ public function test_delete_data_for_users(): void { $this->resetAfterTest(); $component = 'report_stats'; // Create user1. $user1 = $this->getDataGenerator()->create_user(); // Create user2. $user2 = $this->getDataGenerator()->create_user(); // Create user3. $user3 = $this->getDataGenerator()->create_user(); // Create course1. $course1 = $this->getDataGenerator()->create_course(); $coursecontext1 = \context_course::instance($course1->id); // Create course2. $course2 = $this->getDataGenerator()->create_course(); $coursecontext2 = \context_course::instance($course2->id); $this->create_stats($course1->id, $user1->id, 'stats_user_daily'); $this->create_stats($course2->id, $user1->id, 'stats_user_monthly'); $this->create_stats($course1->id, $user2->id, 'stats_user_weekly'); $this->create_stats($course1->id, $user3->id, 'stats_user_weekly'); $userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component); provider::get_users_in_context($userlist1); $this->assertCount(3, $userlist1); $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component); provider::get_users_in_context($userlist2); $this->assertCount(1, $userlist2); // Convert $userlist1 into an approved_contextlist. $approvedlist1 = new approved_userlist($coursecontext1, $component, [$user1->id, $user2->id]); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist1); // Re-fetch users in coursecontext1. $userlist1 = new \core_privacy\local\request\userlist($coursecontext1, $component); provider::get_users_in_context($userlist1); // The approved user data in coursecontext1 should be deleted. // The user list should still return user3. $this->assertCount(1, $userlist1); $this->assertTrue(in_array($user3->id, $userlist1->get_userids())); // Re-fetch users in coursecontext2. $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component); provider::get_users_in_context($userlist2); // The user data in coursecontext2 should be still present. $this->assertCount(1, $userlist2); // Convert $userlist2 into an approved_contextlist in the system context. $systemcontext = \context_system::instance(); $approvedlist2 = new approved_userlist($systemcontext, $component, $userlist2->get_userids()); // Delete using delete_data_for_user. provider::delete_data_for_users($approvedlist2); // Re-fetch users in coursecontext2. $userlist2 = new \core_privacy\local\request\userlist($coursecontext2, $component); provider::get_users_in_context($userlist2); // The user data in systemcontext should not be deleted. $this->assertCount(1, $userlist2); } } PK{\_stats/tests/lib_test.phpnu[. /** * Tests for report library functions. * * @package report_stats * @copyright 2014 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace report_stats; defined('MOODLE_INTERNAL') || die(); /** * Class report_stats_lib_testcase * * @package report_stats * @copyright 2014 onwards Ankit agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class lib_test extends \advanced_testcase { /** * @var stdClass The user. */ private $user; /** * @var stdClass The course. */ private $course; /** * @var \core_user\output\myprofile\tree The navigation tree. */ private $tree; public function setUp(): void { parent::setUp(); $this->user = $this->getDataGenerator()->create_user(); $this->course = $this->getDataGenerator()->create_course(); $this->tree = new \core_user\output\myprofile\tree(); $this->resetAfterTest(); } /** * Test report_log_supports_logstore. */ public function test_report_participation_supports_logstore(): void { $logmanager = get_log_manager(); $allstores = \core_component::get_plugin_list_with_class('logstore', 'log\store'); $supportedstores = array( 'logstore_standard' => '\logstore_standard\log\store' ); // Make sure all supported stores are installed. $expectedstores = array_keys(array_intersect($allstores, $supportedstores)); $stores = $logmanager->get_supported_logstores('report_stats'); $stores = array_keys($stores); foreach ($expectedstores as $expectedstore) { $this->assertContains($expectedstore, $stores); } } /** * Tests the report_stats_myprofile_navigation() function. */ public function test_report_stats_myprofile_navigation(): void { $this->setAdminUser(); $iscurrentuser = false; // Enable stats. set_config('enablestats', true); report_stats_myprofile_navigation($this->tree, $this->user, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayHasKey('stats', $nodes->getValue($this->tree)); } /** * Tests the report_stats_myprofile_navigation() function when stats are disabled. */ public function test_report_stats_myprofile_navigation_stats_disabled(): void { $this->setAdminUser(); $iscurrentuser = false; // Disable stats. set_config('enablestats', false); report_stats_myprofile_navigation($this->tree, $this->user, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayNotHasKey('stats', $nodes->getValue($this->tree)); } /** * Tests the report_stats_myprofile_navigation() function without permission. */ public function test_report_stats_myprofile_navigation_without_permission(): void { // Try to see as a user without permission. $this->setUser($this->user); $iscurrentuser = true; // Enable stats. set_config('enablestats', true); report_stats_myprofile_navigation($this->tree, $this->user, $iscurrentuser, $this->course); $reflector = new \ReflectionObject($this->tree); $nodes = $reflector->getProperty('nodes'); $this->assertArrayNotHasKey('stats', $nodes->getValue($this->tree)); } } PK{\P !stats/tests/event/events_test.phpnu[. /** * Tests for stats report events. * * @package report_stats * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ namespace report_stats\event; /** * Class report_stats_events_testcase * * Class for tests related to stats report events. * * @package report_stats * @copyright 2014 Rajesh Taneja * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later. */ final class events_test extends \advanced_testcase { /** * Setup testcase. */ public function setUp(): void { parent::setUp(); $this->setAdminUser(); $this->resetAfterTest(); } /** * Test the stats report viewed event. * * It's not possible to use the moodle API to simulate the viewing of stats report, so here we * simply create the event and trigger it. */ public function test_report_viewed(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Trigger event for stats report viewed. $event = \report_stats\event\report_viewed::create(array('context' => $context, 'relateduserid' => $user->id, 'other' => array('report' => 0, 'time' => 0, 'mode' => 1))); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $event = reset($events); $this->assertInstanceOf('\report_stats\event\report_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); } /** * Test the user stats report viewed event. * * It's not possible to use the moodle API to simulate the viewing of user stats report, so here we * simply create the event and trigger it. */ public function test_user_report_viewed(): void { $user = $this->getDataGenerator()->create_user(); $course = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course->id); // Trigger event for user stats report viewed. $event = \report_stats\event\user_report_viewed::create(array('context' => $context, 'relateduserid' => $user->id)); // Trigger and capture the event. $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $event = reset($events); $this->assertInstanceOf('\report_stats\event\user_report_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEventContextNotUsed($event); } } PK{\ȼstats/user.phpnu[. /** * Display user activity reports for a course (totals) * * @package report * @subpackage stats * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require('../../config.php'); require_once($CFG->dirroot.'/report/stats/locallib.php'); $userid = required_param('id', PARAM_INT); $courseid = required_param('course', PARAM_INT); $user = $DB->get_record('user', array('id'=>$userid, 'deleted'=>0), '*', MUST_EXIST); $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST); $coursecontext = context_course::instance($course->id); $personalcontext = context_user::instance($user->id); $pageheading = $course->fullname; $userfullname = fullname($user); if ($courseid == SITEID) { $PAGE->set_context($personalcontext); $pageheading = $userfullname; } if ($USER->id != $user->id and has_capability('moodle/user:viewuseractivitiesreport', $personalcontext) and !is_enrolled($coursecontext, $USER) and is_enrolled($coursecontext, $user)) { //TODO: do not require parents to be enrolled in courses - this is a hack! require_login(); $PAGE->set_course($course); } else { require_login($course); } if (!report_stats_can_access_user_report($user, $course)) { // this should never happen throw new \moodle_exception('nocapability', 'report_stats'); } $stractivityreport = get_string('activityreport'); $PAGE->set_pagelayout('report'); $PAGE->set_url('/report/stats/user.php', array('id'=>$user->id, 'course'=>$course->id)); $PAGE->navigation->extend_for_user($user); $PAGE->navigation->set_userid_for_parent_checks($user->id); // see MDL-25805 for reasons and for full commit reference for reversal when fixed. // Breadcrumb stuff. $navigationnode = array( 'name' => get_string('stats'), 'url' => new moodle_url('/report/stats/user.php', array('id' => $user->id, 'course' => $course->id)) ); $PAGE->add_report_nodes($user->id, $navigationnode); $PAGE->set_title("$course->shortname: $stractivityreport"); $PAGE->set_heading($pageheading); echo $OUTPUT->header(); if ($courseid != SITEID) { $backurl = new moodle_url('/user/view.php', ['id' => $userid, 'course' => $courseid]); echo $OUTPUT->single_button($backurl, get_string('back'), 'get', ['class' => 'mb-3']); echo $OUTPUT->context_header( array( 'heading' => $userfullname, 'user' => $user, 'usercontext' => $personalcontext ), 2); echo $OUTPUT->heading(get_string('statistics', 'moodle'), 2, 'main mt-4 mb-4'); } // Trigger a user report viewed event. $event = \report_stats\event\user_report_viewed::create(array('context' => $coursecontext, 'relateduserid' => $user->id)); $event->trigger(); if (empty($CFG->enablestats)) { throw new \moodle_exception('statsdisable', 'error'); } $statsstatus = stats_check_uptodate($course->id); if ($statsstatus !== NULL) { echo $OUTPUT->notification($statsstatus); } $earliestday = $DB->get_field_sql('SELECT MIN(timeend) FROM {stats_user_daily}'); $earliestweek = $DB->get_field_sql('SELECT MIN(timeend) FROM {stats_user_weekly}'); $earliestmonth = $DB->get_field_sql('SELECT MIN(timeend) FROM {stats_user_monthly}'); if (empty($earliestday)) { $earliestday = time(); } if (empty($earliestweek)) { $earliestweek = time(); } if (empty($earliestmonth)) { $earliestmonth = time(); } $now = stats_get_base_daily(); $lastweekend = stats_get_base_weekly(); $lastmonthend = stats_get_base_monthly(); $timeoptions = stats_get_time_options($now,$lastweekend,$lastmonthend,$earliestday,$earliestweek,$earliestmonth); if (empty($timeoptions)) { throw new \moodle_exception('nostatstodisplay', '', $CFG->wwwroot.'/course/user.php?id='.$course->id.'&user='.$user->id.'&mode=outline'); } // use the earliest. $timekeys = array_keys($timeoptions); $time = array_pop($timekeys); $param = stats_get_parameters($time,STATS_REPORT_USER_VIEW,$course->id,STATS_MODE_DETAILED); $params = $param->params; $param->table = 'user_'.$param->table; // Build the conditions and parameters. $wheres = [ "userid = :userid", "timeend >= :timeend", "stattype = :stattype", ]; $params['userid'] = $user->id; $params['timeend'] = $param->timeafter; $params['stattype'] = $param->stattype; // Add condition for course ID when specified. if ($course->id != SITEID) { $wheres[] = "courseid = :courseid"; $params['courseid'] = $course->id; } // Combine the conditions. $wheresql = implode(" AND ", $wheres); // Build the query. $sql = " SELECT {$param->fields} FROM {stats_{$param->table}} WHERE {$wheresql} {$param->extras} ORDER BY timeend DESC"; // Fetch the stats data. $stats = $DB->get_records_sql($sql, $params); if (empty($stats)) { throw new \moodle_exception('nostatstodisplay', '', $CFG->wwwroot.'/course/user.php?id='.$course->id.'&user='.$user->id.'&mode=outline'); } report_stats_print_chart($course->id, STATS_REPORT_USER_VIEW, $time, STATS_MODE_DETAILED, $user->id); // What the heck is this about? -- MD $stats = stats_fix_zeros($stats,$param->timeafter,$param->table,(!empty($param->line2)),(!empty($param->line3))); $table = new html_table(); $table->align = array('left','center','center','center'); $param->table = str_replace('user_','',$param->table); switch ($param->table) { case 'daily' : $period = get_string('day'); break; case 'weekly' : $period = get_string('week'); break; case 'monthly': $period = get_string('month', 'form'); break; default : $period = ''; } $table->head = array(get_string('periodending','moodle',$period),$param->line1,$param->line2,$param->line3); foreach ($stats as $stat) { if (!empty($stat->zerofixed)) { // Don't know why this is necessary, see stats_fix_zeros above - MD continue; } $a = array(userdate($stat->timeend - DAYSECS, get_string('strftimedate'), $CFG->timezone), $stat->line1); $a[] = $stat->line2; $a[] = $stat->line3; $table->data[] = $a; } echo html_writer::table($table); echo $OUTPUT->footer(); PK{\postats/settings.phpnu[. /** * Version info * * @package report * @subpackage stats * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com) * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; // just a link to course report $ADMIN->add('reports', new admin_externalpage('reportstats', get_string('pluginname', 'report_stats'), "$CFG->wwwroot/report/stats/index.php", 'report/stats:view', empty($CFG->enablestats))); // no report settings $settings = null; PK{\FMzzsecurity/db/access.phpnu[. /** * Capabilities * * @package report_security * @copyright 2008 Petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = array( 'report/security:view' => array( 'riskbitmask' => RISK_CONFIG, 'captype' => 'read', 'contextlevel' => CONTEXT_SYSTEM, 'archetypes' => array( 'manager' => CAP_ALLOW ), ) ); PK{\199$security/lang/en/report_security.phpnu[. /** * Lang strings * * @package report_security * @copyright 2008 petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['configuration'] = 'Configuration'; $string['description'] = 'Description'; $string['details'] = 'Details'; $string['check_configrw_details'] = '

    It is recommended that the file permissions of config.php are changed after installation so that the file cannot be modified by the web server. Please note that this measure does not improve security of the server significantly, though it may slow down or limit general exploits.

    '; $string['check_configrw_name'] = 'Writable config.php'; $string['check_configrw_ok'] = 'config.php can not be modified by PHP scripts.'; $string['check_configrw_warning'] = 'PHP scripts may modify config.php.'; $string['check_cookiesecure_details'] = '

    If https communication is enabled, it is recommended to enable sending of secure cookies. You should have permanent redirection from http to https and ideally serve HSTS headers as well.

    '; $string['check_cookiesecure_error'] = 'Please enable secure cookies'; $string['check_cookiesecure_http'] = 'You must turn on https in order to use secure cookies'; $string['check_cookiesecure_name'] = 'Secure cookies'; $string['check_cookiesecure_ok'] = 'Secure cookies enabled.'; $string['check_defaultuserrole_details'] = '

    All logged in users are given capabilities of the default user role. Please make sure no risky capabilities are allowed in this role.

    The only supported legacy type for the default user role is Authenticated user. The course view capability must not be enabled.

    Please check if the automatic data deletion request approval(tool_dataprivacy | automaticdatadeletionapproval) option is enabled. Users can request deletions that could delete large amounts of data.

    '; $string['check_defaultuserrole_error'] = 'The default user role "{$a}" is incorrectly defined!'; $string['check_defaultuserrole_name'] = 'Default role for all users'; $string['check_defaultuserrole_notset'] = 'Default role is not set.'; $string['check_defaultuserrole_ok'] = 'Default role for all users definition is OK.'; $string['check_displayerrors_details'] = '

    Enabling the PHP setting display_errors is not recommended on production sites because error messages can reveal sensitive information about your server.

    '; $string['check_displayerrors_error'] = 'The PHP setting to display errors is enabled. It is recommended that this is disabled.'; $string['check_displayerrors_name'] = 'Displaying of PHP errors'; $string['check_displayerrors_ok'] = 'Displaying of PHP errors disabled.'; $string['check_emailchangeconfirmation_details'] = '

    It is recommended that an email confirmation step is required when users change their email address in their profile. If disabled, spammers may try to exploit the server to send spam.

    Email field may be also locked from authentication plugins, this possibility is not considered here.

    '; $string['check_emailchangeconfirmation_error'] = 'Users may enter any email address.'; $string['check_emailchangeconfirmation_info'] = 'Users may enter email addresses from allowed domains only.'; $string['check_emailchangeconfirmation_name'] = 'Email change confirmation'; $string['check_emailchangeconfirmation_ok'] = 'Confirmation of change of email address in user profile.'; $string['check_embed_details'] = '

    Unlimited object embedding is very dangerous - any registered user may launch an XSS attack against other server users. This setting should be disabled on production servers.

    '; $string['check_embed_error'] = 'Unlimited object embedding enabled - this is very dangerous for the majority of servers.'; $string['check_embed_name'] = 'Allow EMBED and OBJECT'; $string['check_embed_ok'] = 'Unlimited object embedding is not allowed.'; $string['check_frontpagerole_details'] = '

    The default site home role is given to all authenticated users for site home activities. Please make sure no risky capabilities are allowed for this role.

    It is recommended that a special role is created for this purpose and a legacy type role is not used.

    '; $string['check_frontpagerole_error'] = 'Incorrectly defined site home role "{$a}" detected!'; $string['check_frontpagerole_name'] = 'Site home role'; $string['check_frontpagerole_notset'] = 'Site home role is not set.'; $string['check_frontpagerole_ok'] = 'Site home role definition is OK.'; $string['check_crawlers_details'] = '

    The "Open to search engines" setting enables search engines to enter courses with guest access. There is no point in enabling this setting if guest login is not allowed.

    '; $string['check_crawlers_error'] = 'Search engine access is allowed but guest access is disabled.'; $string['check_crawlers_info'] = 'Search engines may enter as guests.'; $string['check_crawlers_name'] = 'Open to search engines'; $string['check_crawlers_ok'] = 'Search engine access is not enabled.'; $string['check_antivirus_details'] = 'This status checks whether or not there has been a recent error detected based on the threshold set in the main antivirus settings.'; $string['check_antivirus_error'] = '{$a->errors} errors have been detected within the last {$a->lookback}'; $string['check_antivirus_info'] = 'No antivirus scanners are currently enabled'; $string['check_antivirus_name'] = 'Antivirus'; $string['check_antivirus_ok'] = '{$a->scanners} antivirus scanner(s) enabled, no issues have been detected in the last {$a->lookback}'; $string['check_antivirus_logstore_not_supported'] = 'Unable to verify state of antivirus scanners due to the type of log store chosen'; $string['check_dotfiles_info'] = 'All dotfiles except /.well-known/* should not be public'; $string['check_dirindex_info'] = 'Directory index should not be enabled'; $string['check_guestrole_details'] = '

    The guest role is used for guests, not logged in users and temporary guest course access. Please make sure no risky capabilities are allowed in this role.

    The only supported legacy type for guest role is Guest.

    '; $string['check_guestrole_error'] = 'The guest role "{$a}" is incorrectly defined!'; $string['check_guestrole_name'] = 'Guest role'; $string['check_guestrole_notset'] = 'Guest role is not set.'; $string['check_guestrole_ok'] = 'Guest role definition is OK.'; $string['check_nodemodules_details'] = '

    The directory {$a->path} contains Node.js modules and their dependencies, typically installed by the NPM utility. These modules may be needed for local Moodle development, such as for using the grunt framework. They are not needed to run a Moodle site in production and they can contain potentially dangerous code exposing your site to remote attacks.

    It is strongly recommended to remove the directory if the site is available via a public URL, or at least prohibit web access to it in your webserver configuration.

    '; $string['check_nodemodules_info'] = 'The node_modules directory should not be present on public sites.'; $string['check_nodemodules_name'] = 'Node.js modules directory'; $string['check_openprofiles_details'] = 'Open user profiles can be abused by spammers. It is recommended that either Force users to log in for profiles or Force users to log in are enabled.'; $string['check_openprofiles_error'] = 'Anyone can may view user profiles without logging in.'; $string['check_openprofiles_name'] = 'Open user profiles'; $string['check_openprofiles_ok'] = 'Login is required before viewing user profiles.'; $string['check_passwordpolicy_details'] = '

    It is recommended that a password policy is set, since password guessing is very often the easiest way to gain unauthorised access. Do not make the requirements too strict though, as this can result in users not being able to remember their passwords and either forgetting them or writing them down.

    '; $string['check_passwordpolicy_error'] = 'Password policy not set.'; $string['check_passwordpolicy_name'] = 'Password policy'; $string['check_passwordpolicy_ok'] = 'Password policy enabled.'; $string['check_preventexecpath_name'] = 'Executable paths'; $string['check_preventexecpath_ok'] = 'Executable paths only settable in config.php.'; $string['check_preventexecpath_warning'] = 'Executable paths can be set in the Admin GUI.'; $string['check_preventexecpath_details'] = '

    Allowing executable paths to be set via the Admin GUI is a vector for privilege escalation. This must be forced in config.php:

    $CFG->preventexecpath = true;

    '; $string['check_publicpaths_name'] = 'Check all public / private paths'; $string['check_publicpaths_ok'] = 'All internal paths are not publicly accessible'; $string['check_publicpaths_warning'] = 'Some internal paths are publicly accessible'; $string['check_publicpaths_generic'] = '{$a} files should not be public'; $string['check_publicpaths_403'] = ' (Returned a 403, ideally should be 404)'; $string['check_riskadmin_detailsok'] = '

    Please verify the following list of system administrators:

    {$a}'; $string['check_riskadmin_detailswarning'] = '

    Please verify the following list of system administrators:

    {$a->admins}

    It is recommended to assign administrator role in the system context only. The following users have (unsupported) admin role assignments in other contexts:

    {$a->unsupported}'; $string['check_riskadmin_name'] = 'Administrators'; $string['check_riskadmin_ok'] = 'Found {$a} server administrator(s).'; $string['check_riskadmin_unassign'] = '{$a->fullname} ({$a->email}) review role assignment'; $string['check_riskadmin_warning'] = 'Found {$a->admincount} server administrators and {$a->unsupcount} unsupported admin role assignments.'; $string['check_riskbackup_detailsok'] = 'No roles explicitly allow backup of user data. However, note that admins with the "doanything" capability are still likely to be able to do this.'; $string['check_riskbackup_details_overriddenroles'] = '

    These active overrides give users the ability to include user data in backups. Please make sure this permission is necessary.

    {$a}'; $string['check_riskbackup_details_systemroles'] = '

    The following system roles currently allow users to include user data in backups. Please make sure this permission is necessary.

    {$a}'; $string['check_riskbackup_details_users'] = '

    Because of the above roles or local overrides, the following user accounts currently have permission to make backups containing private data from any users enrolled in their course. Make sure they are (a) trusted and (b) protected by strong passwords:

    {$a}'; $string['check_riskbackup_editoverride'] = '{$a->name} in {$a->contextname}'; $string['check_riskbackup_editrole'] = '{$a->name}'; $string['check_riskbackup_name'] = 'Backup of user data'; $string['check_riskbackup_ok'] = 'No roles explicitly allow backup of user data'; $string['check_riskbackup_unassign'] = '{$a->fullname} ({$a->email}) in {$a->contextname}'; $string['check_riskbackup_warning'] = 'Found {$a->rolecount} roles, {$a->overridecount} overrides and {$a->usercount} users with the ability to backup user data.'; $string['check_riskxss_details'] = '

    RISK_XSS denotes all dangerous capabilities that only trusted users may use.

    Please verify the following list of users and make sure that you trust them completely on this server:

    {$a}

    '; $string['check_riskxss_name'] = 'XSS trusted users'; $string['check_riskxss_warning'] = 'RISK_XSS - found {$a} users that have to be trusted.'; $string['check_unsecuredataroot_details'] = '

    The dataroot directory must not be accessible via web. The best way to make sure the directory is not accessible is to use a directory outside the public web directory.

    If you move the directory, you need to update the $CFG->dataroot setting in config.php accordingly.

    '; $string['check_unsecuredataroot_error'] = 'Your dataroot directory {$a} is in the wrong location and is exposed to the web!'; $string['check_unsecuredataroot_name'] = 'Insecure dataroot'; $string['check_unsecuredataroot_ok'] = 'Dataroot directory must not be accessible via the web.'; $string['check_unsecuredataroot_warning'] = 'Your dataroot directory {$a} is in the wrong location and might be exposed to the web.'; $string['check_upgradefile_info'] = 'Upgrade note files should not be public'; $string['check_vendordir_details'] = '

    The directory {$a->path} contains various third-party libraries and their dependencies, typically installed by the PHP Composer. These libraries may be needed for local Moodle development, such as for installing the PHPUnit framework. They are not needed to run a Moodle site in production and they can contain potentially dangerous code exposing your site to remote attacks.

    It is strongly recommended to remove the directory if the site is available via a public URL, or at least prohibit web access to it in your webserver configuration.

    '; $string['check_vendordir_info'] = 'The vendor directory should not be present on public sites.'; $string['check_vendordir_name'] = 'Vendor directory'; $string['check_webcron_details'] = '

    Running the cron from a web browser can expose privileged information to anonymous users. It is recommended to only run the cron from the command line or set a cron password for remote access.

    '; $string['check_webcron_warning'] = 'Anonymous users can access cron.'; $string['check_webcron_name'] = 'Web cron'; $string['check_webcron_ok'] = 'Anonymous users can not access cron.'; $string['eventreportviewed'] = 'Viewed security check report'; $string['issue'] = 'Issue'; $string['pluginname'] = 'Security checks'; $string['security:view'] = 'View security report'; $string['timewarning'] = 'Data processing may take a long time, please be patient...'; $string['privacy:metadata'] = 'The Security overview plugin does not store any personal data.'; PK{\vvsecurity/version.phpnu[. /** * Version info * * @package report_security * @copyright 2011 petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_security'; // Full name of the plugin (used for diagnostics). PK{\e%security/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_security. * * @package report_security * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_security\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_security implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\k!(security/classes/event/report_viewed.phpnu[. /** * The report_security report viewed event. * * @package report_security * @copyright 2020 Brendan Heywood * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_security\event; defined('MOODLE_INTERNAL') || die(); /** * The report_loglive report viewed event class. * * @package report_security * @copyright 2020 Brendan Heywood * @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() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_OTHER; } /** * Return localised event name. * * @return string */ public static function get_name() { return get_string('eventreportviewed', 'report_security'); } /** * Returns description of what happened. * * @return string */ public function get_description() { return "The user with id '$this->userid' viewed the security check report."; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { return new \moodle_url('/report/security/index.php'); } } PK{\C}}security/index.phpnu[. /** * Security overview report * * @package report_security * @copyright 2008 petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define('NO_OUTPUT_BUFFERING', true); require('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); admin_externalpage_setup('reportsecurity', '', null, '', ['pagelayout' => 'report']); $detail = optional_param('detail', '', PARAM_TEXT); // Show detailed info about one check only. $url = '/report/security/index.php'; $table = new core\check\table('security', $url, $detail); if (!empty($table->detail)) { $PAGE->set_docs_path($url . '?detail=' . $detail); $PAGE->navbar->add($table->detail->get_name()); } echo $OUTPUT->header(); echo $OUTPUT->heading(get_string('pluginname', 'report_security')); echo $table->render($OUTPUT); echo $OUTPUT->footer(); $event = \report_security\event\report_viewed::create(['context' => context_system::instance()]); $event->trigger(); PK{\NG^^security/settings.phpnu[. /** * Settings and links * * @package report_security * @copyright 2008 petr Skoda * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; $ADMIN->add('reports', new admin_externalpage('reportsecurity', get_string('pluginname', 'report_security'), "$CFG->wwwroot/report/security/index.php", 'report/security:view')); // No report settings. $settings = null; PK{\>qqcompetency/db/services.phpnu[. /** * Competency report webservice functions * * * @package report_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $functions = array( // Learning plan related functions. 'report_competency_data_for_report' => array( 'classname' => 'report_competency\external', 'methodname' => 'data_for_report', 'classpath' => '', 'description' => 'Load the data for the competency report in a course.', 'type' => 'read', 'capabilities' => 'moodle/competency:coursecompetencyview', 'ajax' => true, ) ); PK{\GR(competency/lang/en/report_competency.phpnu[. /** * Strings for component 'report_competency', language 'en' * * @package report_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['competency'] = 'Competency'; $string['coursecompetencybreakdownsummary'] = 'A report of all the students in the course, and their progress towards the course competencies'; $string['notrated'] = 'Not rated'; $string['pluginname'] = 'Competency breakdown'; $string['rating'] = 'Rating'; $string['filtermodule'] = 'Competencies linked to "{$a}"'; $string['usercompetencysummary'] = 'User competency summary'; $string['privacy:metadata'] = 'The Competency breakdown plugin does not store any personal data.'; PK{\O0 0 competency/lib.phpnu[. /** * Public API of the competency report. * * Defines the APIs used by competency reports * * @package report_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * This function extends the navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $course The course to object for the report * @param stdClass $context The context of the course */ function report_competency_extend_navigation_course($navigation, $course, $context) { if (!get_config('core_competency', 'enabled')) { return; } if (has_capability('moodle/competency:coursecompetencyview', $context)) { $url = new moodle_url('/report/competency/index.php', array('id' => $course->id)); $name = get_string('pluginname', 'report_competency'); $navigation->add($name, $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/report', '')); } } /** * This function extends the navigation with the report items * * @param navigation_node $navigation The navigation node to extend * @param cminfo $cm The course module. */ function report_competency_extend_navigation_module($navigation, $cm) { if (!get_config('core_competency', 'enabled')) { return; } if (has_any_capability(array('moodle/competency:usercompetencyview', 'moodle/competency:coursecompetencymanage'), context_course::instance($cm->course))) { $url = new moodle_url('/report/competency/index.php', array('id' => $cm->course, 'mod' => $cm->id)); $name = get_string('pluginname', 'report_competency'); $navigation->add($name, $url, navigation_node::TYPE_SETTING, null, 'competencybreakdown', new pix_icon('i/competencies', ''))->set_show_in_secondary_navigation(false); } } PK{\+*competency/version.phpnu[. /** * Plugin version info * * @package report_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_competency'; // Full name of the plugin (used for diagnostics). $plugin->dependencies = [ 'tool_lp' => ANY_VERSION, ]; PK{\26ԥvvcompetency/classes/external.phpnu[. namespace report_competency; use context_course; use core_competency\external\user_competency_course_exporter; use core_course\external\course_summary_exporter; 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_user\external\user_summary_exporter; use tool_lp\external\competency_summary_exporter; /** * This is the external API for this report. * * @package report_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external extends external_api { /** * Returns description of data_for_competency_frameworks_manage_page() parameters. * * @return external_function_parameters */ public static function data_for_report_parameters() { $courseid = new external_value( PARAM_INT, 'The course id', VALUE_REQUIRED ); $userid = new external_value( PARAM_INT, 'The user id', VALUE_REQUIRED ); $moduleid = new external_value( PARAM_INT, 'The module id', VALUE_REQUIRED ); $params = array( 'courseid' => $courseid, 'userid' => $userid, 'moduleid' => $moduleid, ); return new external_function_parameters($params); } /** * Loads the data required to render the report. * * @param int $courseid The course id * @param int $userid The user id * @param int $moduleid The module id * @return \stdClass */ public static function data_for_report($courseid, $userid, $moduleid) { global $PAGE; $params = self::validate_parameters( self::data_for_report_parameters(), array( 'courseid' => $courseid, 'userid' => $userid, 'moduleid' => $moduleid ) ); $context = context_course::instance($params['courseid']); self::validate_context($context); if (!is_enrolled($context, $params['userid'], 'moodle/competency:coursecompetencygradable')) { throw new coding_exception('invaliduser'); } $renderable = new output\report($params['courseid'], $params['userid'], $params['moduleid']); $renderer = $PAGE->get_renderer('report_competency'); $data = $renderable->export_for_template($renderer); return $data; } /** * Returns description of data_for_report() result value. * * @return external_description */ public static function data_for_report_returns() { return new external_single_structure(array ( 'courseid' => new external_value(PARAM_INT, 'Course id'), 'user' => user_summary_exporter::get_read_structure(), 'course' => course_summary_exporter::get_read_structure(), 'usercompetencies' => new external_multiple_structure( new external_single_structure(array( 'usercompetencycourse' => user_competency_course_exporter::get_read_structure(), 'competency' => competency_summary_exporter::get_read_structure() )) ), 'pushratingstouserplans' => new external_value(PARAM_BOOL, 'True if rating is push to user plans') )); } } PK{\s'competency/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_competency. * * @package report_competency * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_competency\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_competency implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\@b! &competency/classes/output/renderer.phpnu[. /** * Renderer class for report_competency * * @package report_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_competency\output; defined('MOODLE_INTERNAL') || die; use plugin_renderer_base; use renderable; /** * Renderer class for competency breakdown report * * @package report_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends plugin_renderer_base { /** * Defer to template. * * @param report $page * @return string html for the page */ public function render_report(report $page) { $data = $page->export_for_template($this); return parent::render_from_template('report_competency/report', $data); } /** * Defer to template. * * @param user_course_navigation $nav * @return string */ public function render_user_course_navigation(user_course_navigation $nav) { $data = $nav->export_for_template($this); return parent::render_from_template('report_competency/user_course_navigation', $data); } /** * Output a nofication. * * @param string $message the message to print out * @return string HTML fragment. * @see \core\output\notification */ public function notify_message($message) { $n = new \core\output\notification($message, \core\output\notification::NOTIFY_INFO); return $this->render($n); } /** * Output an error notification. * * @param string $message the message to print out * @return string HTML fragment. * @see \core\output\notification */ public function notify_problem($message) { $n = new \core\output\notification($message, \core\output\notification::NOTIFY_ERROR); return $this->render($n); } /** * Output a success notification. * * @param string $message the message to print out * @return string HTML fragment. * @see \core\output\notification */ public function notify_success($message) { $n = new \core\output\notification($message, \core\output\notification::NOTIFY_SUCCESS); return $this->render($n); } } PK{\I%$competency/classes/output/report.phpnu[. /** * Class containing data for learning plan template competencies page * * @package report_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_competency\output; use context_course; use renderable; use core_user; use templatable; use renderer_base; use moodle_url; use stdClass; use core_competency\api; use core_competency\external\user_competency_course_exporter; use core_user\external\user_summary_exporter; use core_competency\external\performance_helper; use core_competency\url; use core_competency\user_competency; use tool_lp\external\competency_summary_exporter; use core_course\external\course_summary_exporter; /** * Class containing data for learning plan template competencies page * * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report implements renderable, templatable { /** @var context $context */ protected $context; /** @var int $courseid */ protected $courseid; /** @var int $moduleid */ protected $moduleid; /** @var array $competencies */ protected $competencies; /** @var int The user id */ protected $userid; /** * Construct this renderable. * * @param int $courseid The course id * @param int $userid The user id * @param int $moduleid The module id */ public function __construct($courseid, $userid, $moduleid) { $this->courseid = $courseid; $this->userid = $userid; $this->moduleid = $moduleid; $this->context = context_course::instance($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) { global $DB; $data = new stdClass(); $data->courseid = $this->courseid; $data->moduleid = $this->moduleid; if (empty($data->moduleid)) { $data->moduleid = 0; } $course = $DB->get_record('course', array('id' => $this->courseid)); $coursecontext = context_course::instance($course->id); $exporter = new course_summary_exporter($course, array('context' => $coursecontext)); $coursecompetencysettings = api::read_course_competency_settings($course->id); $data->pushratingstouserplans = $coursecompetencysettings->get('pushratingstouserplans'); $data->course = $exporter->export($output); $data->usercompetencies = array(); $user = core_user::get_user($this->userid); $exporter = new user_summary_exporter($user); $data->user = $exporter->export($output); $data->usercompetencies = array(); $coursecompetencies = api::list_course_competencies($this->courseid); $usercompetencycourses = api::list_user_competencies_in_course($this->courseid, $user->id); if ($this->moduleid > 0) { $modulecompetencies = api::list_course_module_competencies_in_course_module($this->moduleid); foreach ($usercompetencycourses as $ucid => $usercompetency) { $found = false; foreach ($modulecompetencies as $mcid => $modulecompetency) { if ($modulecompetency->get('competencyid') == $usercompetency->get('competencyid')) { $found = true; break; } } if (!$found) { // We need to filter out this competency. unset($usercompetencycourses[$ucid]); } } } $helper = new performance_helper(); foreach ($usercompetencycourses as $usercompetencycourse) { $onerow = new stdClass(); $competency = null; foreach ($coursecompetencies as $coursecompetency) { if ($coursecompetency['competency']->get('id') == $usercompetencycourse->get('competencyid')) { $competency = $coursecompetency['competency']; break; } } if (!$competency) { continue; } $framework = $helper->get_framework_from_competency($competency); $scale = $helper->get_scale_from_competency($competency); $exporter = new user_competency_course_exporter($usercompetencycourse, array('scale' => $scale)); $record = $exporter->export($output); $onerow->usercompetencycourse = $record; $exporter = new competency_summary_exporter(null, array( 'competency' => $competency, 'framework' => $framework, 'context' => $framework->get_context(), 'relatedcompetencies' => array(), 'linkedcourses' => array() )); $onerow->competency = $exporter->export($output); array_push($data->usercompetencies, $onerow); } return $data; } } PK{\Q4competency/classes/output/user_course_navigation.phpnu[. /** * User navigation class. * * @package report_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_competency\output; use renderable; use renderer_base; use templatable; use context_course; use core_user\external\user_summary_exporter; use core_course\external\course_module_summary_exporter; use stdClass; /** * User course navigation class. * * @package report_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class user_course_navigation implements renderable, templatable { /** @var userid */ protected $userid; /** @var courseid */ protected $courseid; /** @var moduleid */ protected $moduleid; /** @var baseurl */ protected $baseurl; /** * Construct. * * @param int $userid * @param int $courseid * @param int $moduleid * @param string $baseurl */ public function __construct($userid, $courseid, $baseurl, $moduleid) { $this->userid = $userid; $this->courseid = $courseid; $this->moduleid = $moduleid; $this->baseurl = $baseurl; } /** * Export the data. * * @param renderer_base $output * @return stdClass */ public function export_for_template(renderer_base $output) { global $CFG, $DB, $SESSION, $PAGE; $context = context_course::instance($this->courseid); $data = new stdClass(); $data->userid = $this->userid; $data->courseid = $this->courseid; $data->moduleid = $this->moduleid; if (empty($data->moduleid)) { // Moduleid is optional. $data->moduleid = 0; } $data->baseurl = $this->baseurl; $data->groupselector = ''; if (has_any_capability(array('moodle/competency:usercompetencyview', 'moodle/competency:coursecompetencymanage'), $context)) { $course = $DB->get_record('course', array('id' => $this->courseid)); $currentgroup = groups_get_course_group($course, true); if ($currentgroup !== false) { $select = groups_allgroups_course_menu($course, $PAGE->url, true, $currentgroup); $data->groupselector = $select; } // Fetch showactive. $defaultgradeshowactiveenrol = !empty($CFG->grade_report_showonlyactiveenrol); $showonlyactiveenrol = get_user_preferences('grade_report_showonlyactiveenrol', $defaultgradeshowactiveenrol); $showonlyactiveenrol = $showonlyactiveenrol || !has_capability('moodle/course:viewsuspendedusers', $context); // Fetch current active group. $groupmode = groups_get_course_groupmode($course); $users = get_enrolled_users($context, 'moodle/competency:coursecompetencygradable', $currentgroup, 'u.*', null, 0, 0, $showonlyactiveenrol); $data->users = array(); foreach ($users as $user) { $data->users[] = (object)[ 'id' => $user->id, 'fullname' => fullname($user, has_capability('moodle/site:viewfullnames', $context)), 'selected' => $user->id == $this->userid ]; } $data->hasusers = true; $data->hasmodules = true; $data->modules = array(); $empty = (object)['id' => 0, 'name' => get_string('nofiltersapplied')]; $data->modules[] = $empty; $modinfo = get_fast_modinfo($this->courseid); foreach ($modinfo->get_cms() as $cm) { if ($cm->uservisible) { $exporter = new course_module_summary_exporter(null, ['cm' => $cm]); $module = $exporter->export($output); if ($module->id == $this->moduleid) { $module->selected = true; } $data->modules[] = $module; $data->hasmodules = true; } } } else { $data->users = array(); $data->hasusers = false; } return $data; } } PK{\uY Y 4competency/templates/user_course_navigation.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 report_competency/user_course_navigation Moodle navigation control allowing to jump to a user or filter to an activity. Example context (json): { "hasusers": false, "hasmodules": false} }}

    {{{groupselector}}}

    {{#hasusers}} {{/hasusers}} {{#hasmodules}} {{/hasmodules}}
    {{#js}} require(['core/form-autocomplete', 'report_competency/user_course_navigation'], function(autocomplete, nav) { (new nav('#user-nav-{{uniqid}}', '#module-nav-{{uniqid}}', '{{baseurl}}', {{userid}}, {{courseid}}, {{moduleid}})); {{#hasusers}} autocomplete.enhance('#user-nav-{{uniqid}}', false, false, {{# quote }}{{# str }}jumptouser, tool_lp{{/ str }}{{/ quote }}); {{/hasusers}} {{#hasmodules}} autocomplete.enhance('#module-nav-{{uniqid}}', false, false, {{# quote }}{{# str }}filterbyactivity, tool_lp{{/ str }}{{/ quote }}); {{/hasmodules}} }); {{/js}} PK{\%> $competency/templates/report.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 report_competency/report Moodle template competency breakdown report. This template includes ajax functionality, so it cannot be shown in the template library. }}
    {{#pushratingstouserplans}}

    {{#str}}coursecompetencyratingsarepushedtouserplans, tool_lp{{/str}}

    {{/pushratingstouserplans}} {{^pushratingstouserplans}}

    {{#str}}coursecompetencyratingsarenotpushedtouserplans, tool_lp{{/str}}

    {{/pushratingstouserplans}}

    {{#str}}coursecompetencybreakdownsummary, report_competency{{/str}}

    {{#usercompetencies}} {{#competency}} {{/competency}} {{#usercompetencycourse}} {{/usercompetencycourse}} {{/usercompetencies}}
    {{#str}}competency, report_competency{{/str}} {{#str}}rating, report_competency{{/str}}
    {{{competency.shortname}}} {{competency.idnumber}} {{> report_competency/user_competency_summary}}
    {{#js}} require(['tool_lp/competencydialogue'], function(Compdialogue) { Compdialogue.init(); }); require(['report_competency/grading_popup'], function(Popup) { (new Popup('[data-region=competency-breakdown-report]', '[data-user-competency=true]')); }); {{/js}} PK{\d@5competency/templates/user_competency_summary.mustachenu[ {{#grade}}{{gradename}}{{/grade}} {{^grade}}{{#str}}notrated, report_competency{{/str}}{{/grade}} PK{\<2xxcompetency/index.phpnu[. /** * This page lets users to manage site wide competencies. * * @package report_competency * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\report_helper; require_once(__DIR__ . '/../../config.php'); $id = required_param('id', PARAM_INT); $params = array('id' => $id); $course = $DB->get_record('course', $params, '*', MUST_EXIST); require_login($course); $context = context_course::instance($course->id); $currentuser = optional_param('user', null, PARAM_INT); $currentmodule = optional_param('mod', null, PARAM_INT); if ($currentmodule > 0) { $cm = get_coursemodule_from_id('', $currentmodule, 0, false, MUST_EXIST); $context = context_module::instance($cm->id); } // Fetch current active group. $groupmode = groups_get_course_groupmode($course); $currentgroup = groups_get_course_group($course, true); if (empty($currentuser)) { $gradable = get_enrolled_users($context, 'moodle/competency:coursecompetencygradable', $currentgroup, 'u.id', null, 0, 1); if (empty($gradable)) { $currentuser = 0; } else { $currentuser = array_pop($gradable)->id; } } else { $gradable = get_enrolled_users($context, 'moodle/competency:coursecompetencygradable', $currentgroup, 'u.id'); if (count($gradable) == 0) { $currentuser = 0; } else if (!in_array($currentuser, array_keys($gradable))) { $currentuser = array_shift($gradable)->id; } } $urlparams = array('id' => $id); $navurl = new moodle_url('/report/competency/index.php', $urlparams); $urlparams['user'] = $currentuser; $urlparams['mod'] = $currentmodule; $url = new moodle_url('/report/competency/index.php', $urlparams); $title = get_string('pluginname', 'report_competency'); $coursename = format_string($course->fullname, true, array('context' => $context)); $PAGE->navigation->override_active_url($navurl); $PAGE->set_url($url); $PAGE->set_title($title); $PAGE->set_heading($coursename); $PAGE->set_pagelayout('incourse'); $output = $PAGE->get_renderer('report_competency'); echo $output->header(); $pluginname = get_string('pluginname', 'report_competency'); report_helper::print_report_selector($pluginname); $baseurl = new moodle_url('/report/competency/index.php'); $nav = new \report_competency\output\user_course_navigation($currentuser, $course->id, $baseurl, $currentmodule); $top = $output->render($nav); if ($currentuser > 0) { $user = core_user::get_user($currentuser); $usercontext = context_user::instance($currentuser); $userheading = array( 'heading' => fullname($user, has_capability('moodle/site:viewfullnames', $context)), 'user' => $user, 'usercontext' => $usercontext ); if ($currentmodule > 0) { $title = get_string('filtermodule', 'report_competency', format_string($cm->name)); } $top .= $output->context_header($userheading, 3); } echo $output->container($top, 'clearfix'); if ($currentuser > 0) { $page = new \report_competency\output\report($course->id, $currentuser, $currentmodule); echo $output->render($page); } else { echo $output->container('', 'clearfix'); echo $output->notify_problem(get_string('noparticipants', 'tool_lp')); } echo $output->footer(); PK{\3#competency/amd/src/grading_popup.jsnu[// 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 . /** * Module to enable inline editing of a comptency grade. * * @module report_competency/grading_popup * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/notification', 'core/str', 'core/ajax', 'core/log', 'core/templates', 'tool_lp/dialogue'], function($, notification, str, ajax, log, templates, Dialogue) { /** * GradingPopup * * @class report_competency/grading_popup * @param {String} regionSelector The regionSelector * @param {String} userCompetencySelector The userCompetencySelector */ var GradingPopup = function(regionSelector, userCompetencySelector) { this._regionSelector = regionSelector; this._userCompetencySelector = userCompetencySelector; $(this._regionSelector).on('click', this._userCompetencySelector, this._handleClick.bind(this)); }; /** * Get the data from the clicked cell and open the popup. * * @method _handleClick * @param {Event} e The event */ GradingPopup.prototype._handleClick = function(e) { var cell = $(e.target).closest(this._userCompetencySelector); var competencyId = $(cell).data('competencyid'); var courseId = $(cell).data('courseid'); var userId = $(cell).data('userid'); log.debug('Clicked on cell: competencyId=' + competencyId + ', courseId=' + courseId + ', userId=' + userId); var requests = ajax.call([{ methodname: 'tool_lp_data_for_user_competency_summary_in_course', args: {userid: userId, competencyid: competencyId, courseid: courseId}, }, { methodname: 'core_competency_user_competency_viewed_in_course', args: {userid: userId, competencyid: competencyId, courseid: courseId}, }]); $.when(requests[0], requests[1]) .then(this._contextLoaded.bind(this)) .catch(notification.exception); }; /** * We loaded the context, now render the template. * * @method _contextLoaded * @param {Object} context * @returns {Promise} */ GradingPopup.prototype._contextLoaded = function(context) { // We have to display user info in popup. context.displayuser = true; M.util.js_pending('report_competency/grading_popup:_contextLoaded'); return $.when( str.get_string('usercompetencysummary', 'report_competency'), templates.render('tool_lp/user_competency_summary_in_course', context) ) .then(function(title, templateData) { return new Dialogue( title, templateData[0], function() { templates.runTemplateJS(templateData[1]); M.util.js_complete('report_competency/grading_popup:_contextLoaded'); }, this._refresh.bind(this), true ); }.bind(this)); }; /** * Refresh the page. * * @method _refresh * @returns {Promise} */ GradingPopup.prototype._refresh = function() { var region = $(this._regionSelector); var courseId = region.data('courseid'); var moduleId = region.data('moduleid'); var userId = region.data('userid'); // The module id is expected to be an integer, so don't pass empty string. if (moduleId === '') { moduleId = 0; } return ajax.call([{ methodname: 'report_competency_data_for_report', args: {courseid: courseId, userid: userId, moduleid: moduleId}, done: this._pageContextLoaded.bind(this), fail: notification.exception }]); }; /** * We loaded the context, now render the template. * * @method _pageContextLoaded * @param {Object} context */ GradingPopup.prototype._pageContextLoaded = function(context) { templates.render('report_competency/report', context) .then(function(html, js) { templates.replaceNode(this._regionSelector, html, js); return; }.bind(this)) .catch(notification.exception); }; /** @property {String} The selector for the region with the user competencies */ GradingPopup.prototype._regionSelector = null; /** @property {String} The selector for the region with a single user competencies */ GradingPopup.prototype._userCompetencySelector = null; return GradingPopup; }); PK{\ATt t ,competency/amd/src/user_course_navigation.jsnu[// 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 . /** * Module to navigation between users in a course. * * @module report_competency/user_course_navigation * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery'], function($) { /** * UserCourseNavigation * * @class report_competency/user_course_navigation * @param {String} userSelector The selector of the user element. * @param {String} moduleSelector The selector of the module element. * @param {String} baseUrl The base url for the page (no params). * @param {Number} userId The course id * @param {Number} courseId The user id * @param {Number} moduleId The activity module (filter) */ var UserCourseNavigation = function(userSelector, moduleSelector, baseUrl, userId, courseId, moduleId) { this._baseUrl = baseUrl; this._userId = userId + ''; this._courseId = courseId; this._moduleId = moduleId; $(userSelector).on('change', this._userChanged.bind(this)); $(moduleSelector).on('change', this._moduleChanged.bind(this)); }; /** * The user was changed in the select list. * * @method _userChanged * @param {Event} e the event */ UserCourseNavigation.prototype._userChanged = function(e) { // Note: This change causes a page reload and is intentionally not paired with a js_complete call. M.util.js_pending('report_competency/user_course_navigation:_userChanged'); var newUserId = $(e.target).val(); var queryStr = '?user=' + newUserId + '&id=' + this._courseId + '&mod=' + this._moduleId; document.location = this._baseUrl + queryStr; }; /** * The module was changed in the select list. * * @method _moduleChanged * @param {Event} e the event */ UserCourseNavigation.prototype._moduleChanged = function(e) { // Note: This change causes a page reload and is intentionally not paired with a js_complete call. M.util.js_pending('report_competency/user_course_navigation:_moduleChanged'); var newModuleId = $(e.target).val(); var queryStr = '?mod=' + newModuleId + '&id=' + this._courseId + '&user=' + this._userId; document.location = this._baseUrl + queryStr; }; /** @property {Number} The id of the user. */ UserCourseNavigation.prototype._userId = null; /** @property {Number} The id of the module. */ UserCourseNavigation.prototype._moduleId = null; /** @property {Number} The id of the course. */ UserCourseNavigation.prototype._courseId = null; /** @property {String} Plugin base url. */ UserCourseNavigation.prototype._baseUrl = null; return UserCourseNavigation; }); PK{\ 7^ )competency/amd/build/grading_popup.min.jsnu[/** * Module to enable inline editing of a comptency grade. * * @module report_competency/grading_popup * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("report_competency/grading_popup",["jquery","core/notification","core/str","core/ajax","core/log","core/templates","tool_lp/dialogue"],(function($,notification,str,ajax,log,templates,Dialogue){var GradingPopup=function(regionSelector,userCompetencySelector){this._regionSelector=regionSelector,this._userCompetencySelector=userCompetencySelector,$(this._regionSelector).on("click",this._userCompetencySelector,this._handleClick.bind(this))};return GradingPopup.prototype._handleClick=function(e){var cell=$(e.target).closest(this._userCompetencySelector),competencyId=$(cell).data("competencyid"),courseId=$(cell).data("courseid"),userId=$(cell).data("userid");log.debug("Clicked on cell: competencyId="+competencyId+", courseId="+courseId+", userId="+userId);var requests=ajax.call([{methodname:"tool_lp_data_for_user_competency_summary_in_course",args:{userid:userId,competencyid:competencyId,courseid:courseId}},{methodname:"core_competency_user_competency_viewed_in_course",args:{userid:userId,competencyid:competencyId,courseid:courseId}}]);$.when(requests[0],requests[1]).then(this._contextLoaded.bind(this)).catch(notification.exception)},GradingPopup.prototype._contextLoaded=function(context){return context.displayuser=!0,M.util.js_pending("report_competency/grading_popup:_contextLoaded"),$.when(str.get_string("usercompetencysummary","report_competency"),templates.render("tool_lp/user_competency_summary_in_course",context)).then(function(title,templateData){return new Dialogue(title,templateData[0],(function(){templates.runTemplateJS(templateData[1]),M.util.js_complete("report_competency/grading_popup:_contextLoaded")}),this._refresh.bind(this),!0)}.bind(this))},GradingPopup.prototype._refresh=function(){var region=$(this._regionSelector),courseId=region.data("courseid"),moduleId=region.data("moduleid"),userId=region.data("userid");return""===moduleId&&(moduleId=0),ajax.call([{methodname:"report_competency_data_for_report",args:{courseid:courseId,userid:userId,moduleid:moduleId},done:this._pageContextLoaded.bind(this),fail:notification.exception}])},GradingPopup.prototype._pageContextLoaded=function(context){templates.render("report_competency/report",context).then(function(html,js){templates.replaceNode(this._regionSelector,html,js)}.bind(this)).catch(notification.exception)},GradingPopup.prototype._regionSelector=null,GradingPopup.prototype._userCompetencySelector=null,GradingPopup})); //# sourceMappingURL=grading_popup.min.js.mapPK{\DC%%6competency/amd/build/user_course_navigation.min.js.mapnu[{"version":3,"file":"user_course_navigation.min.js","sources":["../src/user_course_navigation.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to navigation between users in a course.\n *\n * @module report_competency/user_course_navigation\n * @copyright 2015 Damyon Wiese\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery'], function($) {\n\n /**\n * UserCourseNavigation\n *\n * @class report_competency/user_course_navigation\n * @param {String} userSelector The selector of the user element.\n * @param {String} moduleSelector The selector of the module element.\n * @param {String} baseUrl The base url for the page (no params).\n * @param {Number} userId The course id\n * @param {Number} courseId The user id\n * @param {Number} moduleId The activity module (filter)\n */\n var UserCourseNavigation = function(userSelector, moduleSelector, baseUrl, userId, courseId, moduleId) {\n this._baseUrl = baseUrl;\n this._userId = userId + '';\n this._courseId = courseId;\n this._moduleId = moduleId;\n\n $(userSelector).on('change', this._userChanged.bind(this));\n $(moduleSelector).on('change', this._moduleChanged.bind(this));\n };\n\n /**\n * The user was changed in the select list.\n *\n * @method _userChanged\n * @param {Event} e the event\n */\n UserCourseNavigation.prototype._userChanged = function(e) {\n // Note: This change causes a page reload and is intentionally not paired with a js_complete call.\n M.util.js_pending('report_competency/user_course_navigation:_userChanged');\n var newUserId = $(e.target).val();\n var queryStr = '?user=' + newUserId + '&id=' + this._courseId + '&mod=' + this._moduleId;\n document.location = this._baseUrl + queryStr;\n };\n\n /**\n * The module was changed in the select list.\n *\n * @method _moduleChanged\n * @param {Event} e the event\n */\n UserCourseNavigation.prototype._moduleChanged = function(e) {\n // Note: This change causes a page reload and is intentionally not paired with a js_complete call.\n M.util.js_pending('report_competency/user_course_navigation:_moduleChanged');\n var newModuleId = $(e.target).val();\n var queryStr = '?mod=' + newModuleId + '&id=' + this._courseId + '&user=' + this._userId;\n document.location = this._baseUrl + queryStr;\n };\n\n /** @property {Number} The id of the user. */\n UserCourseNavigation.prototype._userId = null;\n /** @property {Number} The id of the module. */\n UserCourseNavigation.prototype._moduleId = null;\n /** @property {Number} The id of the course. */\n UserCourseNavigation.prototype._courseId = null;\n /** @property {String} Plugin base url. */\n UserCourseNavigation.prototype._baseUrl = null;\n\n return UserCourseNavigation;\n});\n"],"names":["define","$","UserCourseNavigation","userSelector","moduleSelector","baseUrl","userId","courseId","moduleId","_baseUrl","_userId","_courseId","_moduleId","on","this","_userChanged","bind","_moduleChanged","prototype","e","M","util","js_pending","queryStr","target","val","document","location"],"mappings":";;;;;;;AAuBAA,kDAAO,CAAC,WAAW,SAASC,OAapBC,qBAAuB,SAASC,aAAcC,eAAgBC,QAASC,OAAQC,SAAUC,eACpFC,SAAWJ,aACXK,QAAUJ,OAAS,QACnBK,UAAYJ,cACZK,UAAYJ,SAEjBP,EAAEE,cAAcU,GAAG,SAAUC,KAAKC,aAAaC,KAAKF,OACpDb,EAAEG,gBAAgBS,GAAG,SAAUC,KAAKG,eAAeD,KAAKF,eAS5DZ,qBAAqBgB,UAAUH,aAAe,SAASI,GAEnDC,EAAEC,KAAKC,WAAW,6DAEdC,SAAW,SADCtB,EAAEkB,EAAEK,QAAQC,MACU,OAASX,KAAKH,UAAY,QAAUG,KAAKF,UAC/Ec,SAASC,SAAWb,KAAKL,SAAWc,UASxCrB,qBAAqBgB,UAAUD,eAAiB,SAASE,GAErDC,EAAEC,KAAKC,WAAW,+DAEdC,SAAW,QADGtB,EAAEkB,EAAEK,QAAQC,MACS,OAASX,KAAKH,UAAY,SAAWG,KAAKJ,QACjFgB,SAASC,SAAWb,KAAKL,SAAWc,UAIxCrB,qBAAqBgB,UAAUR,QAAU,KAEzCR,qBAAqBgB,UAAUN,UAAY,KAE3CV,qBAAqBgB,UAAUP,UAAY,KAE3CT,qBAAqBgB,UAAUT,SAAW,KAEnCP"}PK{\2Qmm2competency/amd/build/user_course_navigation.min.jsnu[/** * Module to navigation between users in a course. * * @module report_competency/user_course_navigation * @copyright 2015 Damyon Wiese * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("report_competency/user_course_navigation",["jquery"],(function($){var UserCourseNavigation=function(userSelector,moduleSelector,baseUrl,userId,courseId,moduleId){this._baseUrl=baseUrl,this._userId=userId+"",this._courseId=courseId,this._moduleId=moduleId,$(userSelector).on("change",this._userChanged.bind(this)),$(moduleSelector).on("change",this._moduleChanged.bind(this))};return UserCourseNavigation.prototype._userChanged=function(e){M.util.js_pending("report_competency/user_course_navigation:_userChanged");var queryStr="?user="+$(e.target).val()+"&id="+this._courseId+"&mod="+this._moduleId;document.location=this._baseUrl+queryStr},UserCourseNavigation.prototype._moduleChanged=function(e){M.util.js_pending("report_competency/user_course_navigation:_moduleChanged");var queryStr="?mod="+$(e.target).val()+"&id="+this._courseId+"&user="+this._userId;document.location=this._baseUrl+queryStr},UserCourseNavigation.prototype._userId=null,UserCourseNavigation.prototype._moduleId=null,UserCourseNavigation.prototype._courseId=null,UserCourseNavigation.prototype._baseUrl=null,UserCourseNavigation})); //# sourceMappingURL=user_course_navigation.min.js.mapPK{\_-competency/amd/build/grading_popup.min.js.mapnu[{"version":3,"file":"grading_popup.min.js","sources":["../src/grading_popup.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to enable inline editing of a comptency grade.\n *\n * @module report_competency/grading_popup\n * @copyright 2015 Damyon Wiese\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['jquery', 'core/notification', 'core/str', 'core/ajax', 'core/log', 'core/templates', 'tool_lp/dialogue'],\n function($, notification, str, ajax, log, templates, Dialogue) {\n\n /**\n * GradingPopup\n *\n * @class report_competency/grading_popup\n * @param {String} regionSelector The regionSelector\n * @param {String} userCompetencySelector The userCompetencySelector\n */\n var GradingPopup = function(regionSelector, userCompetencySelector) {\n this._regionSelector = regionSelector;\n this._userCompetencySelector = userCompetencySelector;\n\n $(this._regionSelector).on('click', this._userCompetencySelector, this._handleClick.bind(this));\n };\n\n /**\n * Get the data from the clicked cell and open the popup.\n *\n * @method _handleClick\n * @param {Event} e The event\n */\n GradingPopup.prototype._handleClick = function(e) {\n var cell = $(e.target).closest(this._userCompetencySelector);\n var competencyId = $(cell).data('competencyid');\n var courseId = $(cell).data('courseid');\n var userId = $(cell).data('userid');\n\n log.debug('Clicked on cell: competencyId=' + competencyId + ', courseId=' + courseId + ', userId=' + userId);\n\n var requests = ajax.call([{\n methodname: 'tool_lp_data_for_user_competency_summary_in_course',\n args: {userid: userId, competencyid: competencyId, courseid: courseId},\n }, {\n methodname: 'core_competency_user_competency_viewed_in_course',\n args: {userid: userId, competencyid: competencyId, courseid: courseId},\n }]);\n\n $.when(requests[0], requests[1])\n .then(this._contextLoaded.bind(this))\n .catch(notification.exception);\n };\n\n /**\n * We loaded the context, now render the template.\n *\n * @method _contextLoaded\n * @param {Object} context\n * @returns {Promise}\n */\n GradingPopup.prototype._contextLoaded = function(context) {\n // We have to display user info in popup.\n context.displayuser = true;\n\n M.util.js_pending('report_competency/grading_popup:_contextLoaded');\n\n return $.when(\n str.get_string('usercompetencysummary', 'report_competency'),\n templates.render('tool_lp/user_competency_summary_in_course', context)\n )\n .then(function(title, templateData) {\n return new Dialogue(\n title,\n templateData[0],\n function() {\n templates.runTemplateJS(templateData[1]);\n M.util.js_complete('report_competency/grading_popup:_contextLoaded');\n },\n this._refresh.bind(this),\n true\n );\n }.bind(this));\n };\n\n /**\n * Refresh the page.\n *\n * @method _refresh\n * @returns {Promise}\n */\n GradingPopup.prototype._refresh = function() {\n var region = $(this._regionSelector);\n var courseId = region.data('courseid');\n var moduleId = region.data('moduleid');\n var userId = region.data('userid');\n\n // The module id is expected to be an integer, so don't pass empty string.\n if (moduleId === '') {\n moduleId = 0;\n }\n\n return ajax.call([{\n methodname: 'report_competency_data_for_report',\n args: {courseid: courseId, userid: userId, moduleid: moduleId},\n done: this._pageContextLoaded.bind(this),\n fail: notification.exception\n }]);\n };\n\n /**\n * We loaded the context, now render the template.\n *\n * @method _pageContextLoaded\n * @param {Object} context\n */\n GradingPopup.prototype._pageContextLoaded = function(context) {\n templates.render('report_competency/report', context)\n .then(function(html, js) {\n templates.replaceNode(this._regionSelector, html, js);\n\n return;\n }.bind(this))\n .catch(notification.exception);\n };\n\n /** @property {String} The selector for the region with the user competencies */\n GradingPopup.prototype._regionSelector = null;\n /** @property {String} The selector for the region with a single user competencies */\n GradingPopup.prototype._userCompetencySelector = null;\n\n return GradingPopup;\n});\n"],"names":["define","$","notification","str","ajax","log","templates","Dialogue","GradingPopup","regionSelector","userCompetencySelector","_regionSelector","_userCompetencySelector","this","on","_handleClick","bind","prototype","e","cell","target","closest","competencyId","data","courseId","userId","debug","requests","call","methodname","args","userid","competencyid","courseid","when","then","_contextLoaded","catch","exception","context","displayuser","M","util","js_pending","get_string","render","title","templateData","runTemplateJS","js_complete","_refresh","region","moduleId","moduleid","done","_pageContextLoaded","fail","html","js","replaceNode"],"mappings":";;;;;;;AAuBAA,yCAAO,CAAC,SAAU,oBAAqB,WAAY,YAAa,WAAY,iBAAkB,qBACvF,SAASC,EAAGC,aAAcC,IAAKC,KAAMC,IAAKC,UAAWC,cASpDC,aAAe,SAASC,eAAgBC,6BACnCC,gBAAkBF,oBAClBG,wBAA0BF,uBAE/BT,EAAEY,KAAKF,iBAAiBG,GAAG,QAASD,KAAKD,wBAAyBC,KAAKE,aAAaC,KAAKH,eAS7FL,aAAaS,UAAUF,aAAe,SAASG,OACvCC,KAAOlB,EAAEiB,EAAEE,QAAQC,QAAQR,KAAKD,yBAChCU,aAAerB,EAAEkB,MAAMI,KAAK,gBAC5BC,SAAWvB,EAAEkB,MAAMI,KAAK,YACxBE,OAASxB,EAAEkB,MAAMI,KAAK,UAE1BlB,IAAIqB,MAAM,iCAAmCJ,aAAe,cAAgBE,SAAW,YAAcC,YAEjGE,SAAWvB,KAAKwB,KAAK,CAAC,CACtBC,WAAY,qDACZC,KAAM,CAACC,OAAQN,OAAQO,aAAcV,aAAcW,SAAUT,WAC9D,CACCK,WAAY,mDACZC,KAAM,CAACC,OAAQN,OAAQO,aAAcV,aAAcW,SAAUT,aAGjEvB,EAAEiC,KAAKP,SAAS,GAAIA,SAAS,IAC5BQ,KAAKtB,KAAKuB,eAAepB,KAAKH,OAC9BwB,MAAMnC,aAAaoC,YAUxB9B,aAAaS,UAAUmB,eAAiB,SAASG,gBAE7CA,QAAQC,aAAc,EAEtBC,EAAEC,KAAKC,WAAW,kDAEX1C,EAAEiC,KACL/B,IAAIyC,WAAW,wBAAyB,qBACxCtC,UAAUuC,OAAO,4CAA6CN,UAEjEJ,KAAK,SAASW,MAAOC,qBACX,IAAIxC,SACPuC,MACAC,aAAa,IACb,WACIzC,UAAU0C,cAAcD,aAAa,IACrCN,EAAEC,KAAKO,YAAY,oDAEvBpC,KAAKqC,SAASlC,KAAKH,OACnB,IAENG,KAAKH,QASXL,aAAaS,UAAUiC,SAAW,eAC1BC,OAASlD,EAAEY,KAAKF,iBAChBa,SAAW2B,OAAO5B,KAAK,YACvB6B,SAAWD,OAAO5B,KAAK,YACvBE,OAAS0B,OAAO5B,KAAK,gBAGR,KAAb6B,WACAA,SAAW,GAGRhD,KAAKwB,KAAK,CAAC,CACdC,WAAY,oCACZC,KAAM,CAACG,SAAUT,SAAUO,OAAQN,OAAQ4B,SAAUD,UACrDE,KAAMzC,KAAK0C,mBAAmBvC,KAAKH,MACnC2C,KAAMtD,aAAaoC,cAU3B9B,aAAaS,UAAUsC,mBAAqB,SAAShB,SACjDjC,UAAUuC,OAAO,2BAA4BN,SAC5CJ,KAAK,SAASsB,KAAMC,IACjBpD,UAAUqD,YAAY9C,KAAKF,gBAAiB8C,KAAMC,KAGpD1C,KAAKH,OACNwB,MAAMnC,aAAaoC,YAIxB9B,aAAaS,UAAUN,gBAAkB,KAEzCH,aAAaS,UAAUL,wBAA0B,KAE1CJ"}PK{\Z)?bb7competency/tests/behat/course_report_competency.featurenu[@report @report_competency Feature: In a course administration page, navigate through report page, test for course competency page In order to navigate through report page As an admin Go to course administration -> reports -> competency breackdown Background: Given the following "courses" exist: | fullname | shortname | category | groupmode | | Course 1 | C1 | 0 | 1 | And the following "users" exist: | username | firstname | lastname | email | | student1 | Student | 1 | student1@example.com | And the following "course enrolments" exist: | user | course | role | | admin | C1 | editingteacher | | student1 | C1 | student | @javascript Scenario: Selector should be available in the course competency page Given I log in as "admin" And I am on "Course 1" course homepage When I navigate to "Reports" in current page administration And I click on "Competency breakdown" "link" Then "Report" "field" should exist in the "tertiary-navigation" "region" And I should see "Competency breakdown" in the "tertiary-navigation" "region" PK{\Hz!!4competency/tests/behat/breakdown_by_activity.featurenu[@report @javascript @report_competency Feature: See the competencies for an activity As a competency grader In order to perform mark all competencies for an activity I need to see the competencies linked to one activity in the breakdown report. Background: Given the following "core_competency > frameworks" exist: | shortname | idnumber | | Test-Framework | ID-FW1 | And the following "core_competency > competencies" exist: | shortname | competencyframework | | Test-Comp1 | ID-FW1 | | Test-Comp2 | ID-FW1 | Given the following "courses" exist: | shortname | fullname | | C1 | Course 1 | And the following "users" exist: | username | firstname | lastname | email | idnumber | middlename | alternatename | firstnamephonetic | lastnamephonetic | | student1 | Grainne | Beauchamp | student1@example.com | s1 | Ann | Jill | Gronya | Beecham | | student2 | Niamh | Cholmondely | student2@example.com | s2 | Jane | Nina | Nee | Chumlee | And the following "course enrolments" exist: | user | course | role | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | intro | course | idnumber | | page | PageName1 | PageDesc1 | C1 | PAGE1 | And the following config values are set as admin: | fullnamedisplay | firstname | | alternativefullnameformat | middlename, alternatename, firstname, lastname | And I log in as "admin" And I am on site homepage And I follow "Course 1" And I navigate to "Competencies" in current page administration And I press "Add competencies to course" And "Competency picker" "dialogue" should be visible And I select "Test-Comp1" of the competency tree And I click on "Add" "button" in the "Competency picker" "dialogue" And I press "Add competencies to course" And "Competency picker" "dialogue" should be visible And I select "Test-Comp2" of the competency tree And I click on "Add" "button" in the "Competency picker" "dialogue" And I am on the PageName1 "page activity editing" page And I click on "Expand all" "link" in the "region-main" "region" And I set the field "Course competencies" to "Test-Comp1" And I press "Save and return to course" @javascript Scenario: Go to the competency breakdown report When I navigate to "Reports" in current page administration And I click on "Competency breakdown" "link" And I set the competency filter "Filter competencies by resource or activity" to "PageName1" Then I should see "Test-Comp1" And I should not see "Test-Comp2" And I should see "Ann, Jill, Grainne, Beauchamp" And I should see "Ann, Jill, Grainne, Beauchamp" in the ".form-autocomplete-selection" "css_element" And I open the autocomplete suggestions list And I should see "Jane, Nina, Niamh, Cholmondely" in the ".form-autocomplete-suggestions" "css_element" And I click on "Not rated" "link" And I click on "Rate" "button" And I set the field "Rating" to "A" And I click on "Rate" "button" in the ".competency-grader" "css_element" And I click on "Close" "button" in the "User competency summary" "dialogue" And I click on "PageName1" "autocomplete_selection" And I should see "Test-Comp1" And I should see "Test-Comp2" @accessibility Scenario: Evaluate the accessibility of the user competency summary dialogue Given I navigate to "Reports" in current page administration When I click on "Competency breakdown" "link" And I click on "Not rated" "link" And the page should meet accessibility standards PK{\{)$rr2competency/tests/behat/behat_report_competency.phpnu[. /** * Behat competency report definitions. * * @package report_competency * @category test * @copyright 2022 Noel De Martin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); /** * Competency report definitions. * * @package report_competency * @category test * @copyright 2022 Noel De Martin * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_report_competency extends behat_base { /** * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. * * Recognised page names are: * | pagetype | name meaning | description | * | Breakdown | Course name | The course competencies breakdown page | * * @param string $page identifies which type of page this is, e.g. 'Breakdown'. * @param string $identifier identifies the particular page, e.g. 'C1'. * @return moodle_url the corresponding URL. * @throws Exception with a meaningful error message if the specified page cannot be found. */ protected function resolve_page_instance_url(string $page, string $identifier): moodle_url { switch (strtolower($page)) { case 'breakdown': $courseid = $this->get_course_id($identifier); return new moodle_url('/report/competency/index.php', [ 'id' => $courseid, ]); default: throw new Exception("Unrecognised page type '{$page}'"); } } /** * Return a list of the exact named selectors for the component. * * @return behat_component_named_selector[] */ public static function get_exact_named_selectors(): array { return [ new behat_component_named_selector('breakdown', [ "//*[@data-region='competency-breakdown-report']//table". "//tr[contains(., //a[@data-action='competency-dialogue'][contains(., %locator%)])]", ]), new behat_component_named_selector('breakdown rating', [ "//td[position()=2][contains(., //a[@title='User competency summary'][contains(., %locator%)])]", ]), ]; } /** * Set the value of a competency filter. * * @When /^I set the competency filter "([^"]*)" to "([^"]*)"$/ * @param string $fieldlocator The field locator. * @param string $value The value to set. * @throws Exception */ public function set_competency_filter( string $fieldlocator, string $value, ): void { $field = behat_field_manager::get_form_field_from_label($fieldlocator, $this)->get_node(); $session = $this->getSession(); $value = trim($value); // Click into the field. $field->click(); // Remove any existing text. do { behat_base::type_keys($session, [behat_keys::BACKSPACE, behat_keys::DELETE]); } while (strlen($field->getValue()) > 0); $this->wait_for_pending_js(); // Type in the new value. behat_base::type_keys($session, str_split($value)); $this->wait_for_pending_js(); // If the autocomplete found suggestions, then it will have: // 1) marked itself as expanded; and // 2) have an aria-selected suggestion in the list. $expanded = $field->getAttribute('aria-expanded'); $suggestion = $field->getParent()->getParent()->find('css', '.form-autocomplete-suggestions > [aria-selected="true"]'); if ($expanded && null !== $suggestion) { // A suggestion was found. // Click on the first item in the list. $suggestion->click(); } else { throw new \InvalidArgumentException( "Unable to find '{$value}' in the list of options." ); } $this->wait_for_pending_js(); } } PK{\G7bBBinsights/db/services.phpnu[. /** * Report insights webservice definitions. * * @package report_insights * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $functions = array( 'report_insights_action_executed' => array( 'classname' => 'report_insights\external', 'methodname' => 'action_executed', 'description' => 'Stores an action executed over a group of predictions.', 'type' => 'write', 'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE), 'ajax' => true, ) ); PK{\urD $insights/lang/en/report_insights.phpnu[. /** * Strings for report_insights. * * @package report_insights * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['actionsaved'] = 'Your feedback of \'{$a}\' has been saved.'; $string['confirmbulkaction'] = 'Are you sure you want to flag the {$a->nitems} selected predictions as {$a->action}?'; $string['disabledmodel'] = 'This model has been disabled by an administrator.'; $string['indicators'] = 'Indicators'; $string['insight'] = 'Insight'; $string['insights'] = 'Insights'; $string['justpredictions'] = 'Please note that the following insights are only predictions. It is not possible to predict the future with any certainty. The insights are provided so that action can be taken as necessary to prevent any negative predictions becoming reality.'; $string['outcome'] = 'Outcome'; $string['outcomenegative'] = 'Negative outcome'; $string['outcomeneutral'] = 'Neutral outcome'; $string['outcomeok'] = 'OK outcome'; $string['outcomepositive'] = 'Positive outcome'; $string['outcomeverypositive'] = 'Very positive outcome'; $string['outcomeverynegative'] = 'Very negative outcome'; $string['pluginname'] = 'Insights'; $string['prediction'] = 'Prediction'; $string['predictiondetails'] = 'Prediction details'; $string['nodetailsavailable'] = 'No prediction details are relevant.'; $string['selectprediction'] = 'Select {$a} for bulk action'; $string['timecreated'] = 'Time predicted'; $string['timerange'] = 'Analysis interval'; $string['timerangewithdata'] = '{$a->timestart} to {$a->timeend}'; $string['selectotherinsights'] = 'Select other insights...'; $string['privacy:metadata'] = 'The Insights plugin does not store any personal data.'; PK{\LN N insights/action.phpnu[. /** * Forwards the user to the action they selected. * * @package report_insights * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../config.php'); $predictionid = required_param('predictionid', PARAM_INT); $actionname = required_param('action', PARAM_ALPHANUMEXT); $forwardurl = required_param('forwardurl', PARAM_LOCALURL); if (!\core_analytics\manager::is_analytics_enabled()) { $PAGE->set_context(\context_system::instance()); $renderer = $PAGE->get_renderer('report_insights'); echo $renderer->render_analytics_disabled(); exit(0); } list($model, $prediction, $context) = \core_analytics\manager::get_prediction($predictionid, true); if ($context->contextlevel < CONTEXT_COURSE) { // Only for higher levels than course. $PAGE->set_context($context); } if (empty($forwardurl)) { $params = array('modelid' => $model->get_id(), 'contextid' => $context->id); $forwardurl = new \moodle_url('/report/insights/insights.php', $params); } $params = array('predictionid' => $prediction->get_prediction_data()->id, 'action' => $actionname, 'forwardurl' => $forwardurl); $url = new \moodle_url('/report/insights/action.php', $params); $PAGE->set_url($url); $modelready = $model->is_enabled() && $model->is_trained() && $model->predictions_exist($context); if (!$modelready) { $PAGE->set_pagelayout('report'); // We don't want to disclose the name of the model if it has not been enabled. $PAGE->set_title($context->get_context_name()); $PAGE->set_heading($context->get_context_name()); echo $OUTPUT->header(); echo $OUTPUT->notification(get_string('disabledmodel', 'report_insights'), \core\output\notification::NOTIFY_INFO); echo $OUTPUT->footer(); exit(0); } $prediction->action_executed($actionname, $model->get_target()); redirect($forwardurl); PK{\^insights/lib.phpnu[. /** * This page lists public api for tool_monitor plugin. * * @package report_insights * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; /** * This function extends the navigation with the tool items * * @param navigation_node $navigation The navigation node to extend * @param stdClass $course The course to object for the tool * @param context $context The context of the course * @return void */ function report_insights_extend_navigation_course($navigation, $course, $context) { if (\core_analytics\manager::is_analytics_enabled() && has_capability('moodle/analytics:listinsights', $context)) { $modelids = \core_analytics\manager::cached_models_with_insights($context); if (!empty($modelids)) { $url = new moodle_url('/report/insights/insights.php', array('contextid' => $context->id)); $node = navigation_node::create(get_string('insights', 'report_insights'), $url, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/report', get_string('insights', 'report_insights'))); $navigation->add_node($node); } } } /** * Add nodes to myprofile page. * * @param \core_user\output\myprofile\tree $tree Tree object * @param stdClass $user user object * @param bool $iscurrentuser * @param stdClass $course Course object * * @return bool */ function report_insights_myprofile_navigation(core_user\output\myprofile\tree $tree, $user, $iscurrentuser, $course) { if (\core_analytics\manager::is_analytics_enabled()) { $context = \context_user::instance($user->id); if (\core_analytics\manager::check_can_list_insights($context, true)) { $modelids = \core_analytics\manager::cached_models_with_insights($context); if (!empty($modelids)) { $url = new moodle_url('/report/insights/insights.php', array('contextid' => $context->id)); $node = new core_user\output\myprofile\node('reports', 'insights', get_string('insights', 'report_insights'), null, $url); $tree->add_node($node); } } } } /** * Adds nodes to category navigation * * @param navigation_node $navigation The navigation node to extend * @param context $context The context of the course * @return void|null return null if we don't want to display the node. */ function report_insights_extend_navigation_category_settings($navigation, $context) { if (\core_analytics\manager::is_analytics_enabled() && has_capability('moodle/analytics:listinsights', $context)) { $modelids = \core_analytics\manager::cached_models_with_insights($context); if (!empty($modelids)) { $url = new moodle_url('/report/insights/insights.php', array('contextid' => $context->id)); $node = navigation_node::create( get_string('insights', 'report_insights'), $url, navigation_node::NODETYPE_LEAF, null, 'insights', new pix_icon('i/report', get_string('insights', 'report_insights')) ); $navigation->add_node($node); } } } PK{\3Җinsights/version.phpnu[. /** * Version details. * * @package report_insights * @copyright 2017 David Monllao {@link http://www.davidmonllao.com/} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_insights'; // Full name of the plugin (used for diagnostics). PK{\d insights/classes/external.phpnu[. namespace report_insights; use core_external\external_api; use core_external\external_value; use core_external\external_single_structure; use core_external\external_multiple_structure; use core_external\external_function_parameters; use core_external\external_warnings; /** * This is the external API for this component. * * @package report_insights * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class external extends external_api { /** * action_executed parameters. * * @return external_function_parameters * @since Moodle 3.8 */ public static function action_executed_parameters() { return new external_function_parameters ( array( 'actionname' => new external_value(PARAM_ALPHANUMEXT, 'The name of the action', VALUE_REQUIRED), 'predictionids' => new external_multiple_structure( new external_value(PARAM_INT, 'Prediction id', VALUE_REQUIRED), 'Array of prediction ids' ), ) ); } /** * Stores an action executed over a group of predictions. * * @param string $actionname * @param array $predictionids * @return array an array of warnings and a boolean * @since Moodle 3.8 */ public static function action_executed(string $actionname, array $predictionids) { $params = self::validate_parameters(self::action_executed_parameters(), array('actionname' => $actionname, 'predictionids' => $predictionids)); foreach ($params['predictionids'] as $predictionid) { list($model, $prediction, $context) = self::validate_prediction($predictionid); // The method action_executed checks that the provided action is valid. $prediction->action_executed($actionname, $model->get_target()); } return array('warnings' => array()); } /** * action_executed return * * @return \core_external\external_description * @since Moodle 3.8 */ public static function action_executed_returns() { return new external_single_structure( array( 'warnings' => new external_warnings(), ) ); } /** * Validates access to the prediction and returns it. * * @param int $predictionid * @return array array($model, $prediction, $context) */ protected static function validate_prediction($predictionid) { list($model, $prediction, $context) = \core_analytics\manager::get_prediction($predictionid); self::validate_context($context); return array($model, $prediction, $context); } } PK{\Dz/%insights/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_insights. * * @package report_insights * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_insights\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_insights implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\ľ$insights/classes/output/renderer.phpnu[. /** * Renderer. * * @package report_insights * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_insights\output; defined('MOODLE_INTERNAL') || die(); use plugin_renderer_base; use templatable; use renderable; /** * Renderer class. * * @package report_insights * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class renderer extends plugin_renderer_base { /** * Renders the list of insights * * @param renderable $renderable * @return string HTML */ protected function render_insights_list(renderable $renderable) { $data = $renderable->export_for_template($this); return parent::render_from_template('report_insights/insights_list', $data); } /** * Renders an insight * * @param renderable $renderable * @return string HTML */ protected function render_insight(renderable $renderable) { $data = $renderable->export_for_template($this); $renderable->get_model()->get_target()->add_bulk_actions_js(); return parent::render_from_template('report_insights/insight_details', $data); } /** * Model disabled info. * * @param \stdClass $insightinfo * @return string HTML */ public function render_model_disabled($insightinfo) { // We don't want to disclose the name of the model if it has not been enabled. $this->page->set_title($insightinfo->contextname); $this->page->set_heading($insightinfo->contextname); $output = $this->output->header(); $output .= $this->output->notification(get_string('disabledmodel', 'report_insights'), \core\output\notification::NOTIFY_INFO); $output .= $this->output->footer(); return $output; } /** * Model without insights info. * * @param \context $context * @return string HTML */ public function render_no_insights(\context $context) { // We don't want to disclose the name of the model if it has not been enabled. $this->page->set_title($context->get_context_name()); $this->page->set_heading($context->get_context_name()); $output = $this->output->header(); $output .= $this->output->notification(get_string('noinsights', 'analytics'), \core\output\notification::NOTIFY_INFO); $output .= $this->output->footer(); return $output; } /** * Model which target does not generate insights. * * @param \context $context * @return string HTML */ public function render_no_insights_model(\context $context) { // We don't want to disclose the name of the model if it has not been enabled. $this->page->set_title($context->get_context_name()); $this->page->set_heading($context->get_context_name()); $output = $this->output->header(); $output .= $this->output->notification(get_string('noinsightsmodel', 'analytics'), \core\output\notification::NOTIFY_INFO); $output .= $this->output->footer(); return $output; } /** * Renders an analytics disabled notification. * * @return string HTML */ public function render_analytics_disabled() { global $FULLME; $this->page->set_url($FULLME); $this->page->set_title(get_string('pluginname', 'report_insights')); $this->page->set_heading(get_string('pluginname', 'report_insights')); $output = $this->output->header(); $output .= $this->output->notification(get_string('analyticsdisabled', 'analytics'), \core\output\notification::NOTIFY_INFO); $output .= \html_writer::tag('a', get_string('continue'), ['class' => 'btn btn-primary', 'href' => (new \moodle_url('/'))->out()]); $output .= $this->output->footer(); return $output; } } PK{\-z,insights/classes/output/actions_exporter.phpnu[. /** * Output helper to export actions for rendering. * * @package report_insights * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_insights\output; defined('MOODLE_INTERNAL') || die(); /** * Output helper to export actions for rendering. * * @package report_insights * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class actions_exporter { /** * Add the prediction actions renderable. * * @param \core_analytics\local\target\base $target * @param \renderer_base $output * @param \core_analytics\prediction $prediction * @param bool $includedetailsaction * @return \stdClass|false */ public static function add_prediction_actions(\core_analytics\local\target\base $target, \renderer_base $output, \core_analytics\prediction $prediction, bool $includedetailsaction = false) { $actions = $target->prediction_actions($prediction, $includedetailsaction); if ($actions) { $actionsmenu = new \action_menu(); // Add all actions defined by the target. foreach ($actions as $action) { $actionsmenu->add_primary_action($action->get_action_link()); } return $actionsmenu->export_for_template($output); } return false; } /** * Add bulk actions renderables. * * Note that if you are planning to render the bulk actions, the provided predictions must share the same predicted value. * * @param \core_analytics\local\target\base $target * @param \renderer_base $output * @param \core_analytics\prediction[] $predictions Bulk actions for this set of predictions. * @param \context $context The context of these predictions. * @return \stdClass[]|false */ public static function add_bulk_actions(\core_analytics\local\target\base $target, \renderer_base $output, array $predictions, \context $context) { global $USER; $bulkactions = $target->bulk_actions($predictions); if ($context->contextlevel === CONTEXT_USER) { // Remove useful / notuseful if the current user is not part of the users who receive the insight (e.g. a site manager // who looks at the generated insights for a particular user). $insightusers = $target->get_insights_users($context); if (empty($insightusers[$USER->id])) { foreach ($bulkactions as $key => $action) { if ($action->get_action_name() === 'useful' || $action->get_action_name() === 'notuseful') { unset($bulkactions[$key]); } } } } if (!$bulkactions) { return false; } $actionsmenu = []; // All the predictions share a common predicted value. $predictionvalue = reset($predictions)->get_prediction_data()->prediction; // Add all actions defined by the target. foreach ($bulkactions as $action) { $action->get_action_link()->set_attribute('data-togglegroup', 'insight-bulk-action-' . $predictionvalue); $actionsmenu[] = $action->get_action_link()->export_for_template($output); } if (empty($actionsmenu)) { return false; } return $actionsmenu; } } PK{\t**#insights/classes/output/insight.phpnu[. /** * Single insight view page. * * @package report_insights * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_insights\output; use core_analytics\prediction; defined('MOODLE_INTERNAL') || die(); /** * Single insight view page. * * @package report_insights * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class insight implements \renderable, \templatable { /** * @var \core_analytics\model */ protected $model; /** * @var \core_analytics\prediction */ protected $prediction; /** * @var bool */ protected $includedetailsaction = false; /** * @var \context */ protected $context; /** * Constructor * * @param \core_analytics\prediction $prediction * @param \core_analytics\model $model * @param bool $includedetailsaction * @param \context $context * @return void */ public function __construct(\core_analytics\prediction $prediction, \core_analytics\model $model, $includedetailsaction, \context $context) { $this->prediction = $prediction; $this->model = $model; $this->includedetailsaction = $includedetailsaction; $this->context = $context; } /** * Exports the data. * * @param \renderer_base $output * @return \stdClass */ public function export_for_template(\renderer_base $output) { // Get the prediction data. $predictiondata = $this->prediction->get_prediction_data(); $target = $this->model->get_target(); $data = new \stdClass(); $data->modelid = $this->model->get_id(); $data->contextid = $this->context->id; $data->predictionid = $predictiondata->id; $targetname = $target->get_name(); $data->insightname = format_string($targetname); $targetinfostr = $targetname->get_identifier() . 'info'; if (get_string_manager()->string_exists($targetinfostr, $targetname->get_component())) { $data->insightdescription = get_string($targetinfostr, $targetname->get_component()); } $data->showpredictionheading = true; if (!$target->is_linear()) { $nclasses = count($target::get_classes()); $nignoredclasses = count($target->ignored_predicted_classes()); if ($nclasses - $nignoredclasses <= 1) { // Hide the prediction heading if there is only 1 class displayed. Otherwise it is redundant with the insight name. $data->showpredictionheading = false; } } // Get the details. $data->timecreated = userdate($predictiondata->timecreated); $data->timerange = ''; if (!empty($predictiondata->timestart) && !empty($predictiondata->timeend)) { $timerange = new \stdClass(); $timerange->timestart = userdate($predictiondata->timestart); $timerange->timeend = userdate($predictiondata->timeend); $data->timerange = get_string('timerangewithdata', 'report_insights', $timerange); } // Sample info (determined by the analyser). list($data->sampledescription, $samplerenderable) = $this->model->prediction_sample_description($this->prediction); // Sampleimage is a renderable we should pass it to HTML. if ($samplerenderable) { $data->sampleimage = $output->render($samplerenderable); } // Prediction info. $predictedvalue = $predictiondata->prediction; $data->predictiondisplayvalue = $target->get_display_value($predictedvalue); list($data->style, $data->outcomeicon) = self::get_calculation_display($target, floatval($predictedvalue), $output); $data->actions = actions_exporter::add_prediction_actions($target, $output, $this->prediction, $this->includedetailsaction); $data->bulkactions = actions_exporter::add_bulk_actions($target, $output, [$this->prediction], $this->context); // Calculated indicators values. $data->calculations = array(); $calculations = $this->prediction->get_calculations(); foreach ($calculations as $calculation) { // Hook for indicators with extra features that should not be displayed (e.g. discrete indicators). if (!$calculation->indicator->should_be_displayed($calculation->value, $calculation->subtype)) { continue; } if ($calculation->value === null) { // We don't show values that could not be calculated. continue; } $obj = new \stdClass(); $obj->name = call_user_func(array($calculation->indicator, 'get_name')); $obj->displayvalue = $calculation->indicator->get_display_value($calculation->value, $calculation->subtype); list($obj->style, $obj->outcomeicon) = self::get_calculation_display($calculation->indicator, floatval($calculation->value), $output, $calculation->subtype); $identifier = $calculation->indicator->get_name()->get_identifier() . 'def'; $component = $calculation->indicator->get_name()->get_component(); if (get_string_manager()->string_exists($identifier, $component)) { $obj->outcomehelp = (new \help_icon($identifier, $component))->export_for_template($output); } $data->calculations[] = $obj; } if (empty($data->calculations)) { $data->nocalculations = (object)array( 'message' => get_string('nodetailsavailable', 'report_insights'), 'closebutton' => false ); } // This is only rendered in report_insights/insight_details template for predictions with no action. // We need it to automatically enable the bulk action buttons in report/insights/prediction.php. $filtered = [ \core_analytics\prediction::ACTION_FIXED, \core_analytics\prediction::ACTION_NOT_USEFUL, \core_analytics\prediction::ACTION_USEFUL, \core_analytics\prediction::ACTION_NOT_APPLICABLE, \core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, ]; if (!$this->prediction->get_executed_actions($filtered)) { $toggleall = new \core\output\checkbox_toggleall('insight-bulk-action-' . $predictedvalue, true, [ 'id' => 'id-toggle-all-' . $predictedvalue, 'name' => 'toggle-all-' . $predictedvalue, 'classes' => 'hidden', 'label' => get_string('selectall'), 'labelclasses' => 'visually-hidden', 'checked' => false, ]); $data->hiddencheckboxtoggleall = $output->render($toggleall); $toggle = new \core\output\checkbox_toggleall('insight-bulk-action-' . $predictedvalue, false, [ 'id' => 'id-select-' . $data->predictionid, 'name' => 'select-' . $data->predictionid, 'label' => get_string('selectprediction', 'report_insights', $data->sampledescription), 'labelclasses' => 'accesshide', ]); $data->toggleslave = $output->render($toggle); } return $data; } /** * Returns display info for the calculated value outcome. * * @param \core_analytics\calculable $calculable * @param float $value * @param \renderer_base $output * @param string|false $subtype * @return array The style as 'success', 'info', 'warning' or 'danger' and pix_icon */ public static function get_calculation_display(\core_analytics\calculable $calculable, $value, $output, $subtype = false) { $outcome = $calculable->get_calculation_outcome($value, $subtype); switch ($outcome) { case \core_analytics\calculable::OUTCOME_NEUTRAL: $style = ''; $text = get_string('outcomeneutral', 'report_insights'); $icon = 't/check'; break; case \core_analytics\calculable::OUTCOME_VERY_POSITIVE: $style = 'success'; $text = get_string('outcomeverypositive', 'report_insights'); $icon = 't/approve'; break; case \core_analytics\calculable::OUTCOME_OK: $style = 'info'; $text = get_string('outcomeok', 'report_insights'); $icon = 't/check'; break; case \core_analytics\calculable::OUTCOME_NEGATIVE: $style = 'warning'; $text = get_string('outcomenegative', 'report_insights'); $icon = 'i/warning'; break; case \core_analytics\calculable::OUTCOME_VERY_NEGATIVE: $style = 'danger'; $text = get_string('outcomeverynegative', 'report_insights'); $icon = 'i/warning'; break; default: throw new \coding_exception('The outcome returned by ' . get_class($calculable) . '::get_calculation_outcome is ' . 'not one of the accepted values. Please use \core_analytics\calculable::OUTCOME_VERY_POSITIVE, ' . '\core_analytics\calculable::OUTCOME_OK, \core_analytics\calculable::OUTCOME_NEGATIVE, ' . '\core_analytics\calculable::OUTCOME_VERY_NEGATIVE or \core_analytics\calculable::OUTCOME_NEUTRAL'); } $icon = new \pix_icon($icon, $text); return array($style, $icon->export_for_template($output)); } /** * Model getter. * * @return \core_analytics\model */ public function get_model(): \core_analytics\model { return $this->model; } } PK{\$Ӻ )insights/classes/output/insights_list.phpnu[. /** * Insights list page. * * @package report_insights * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_insights\output; defined('MOODLE_INTERNAL') || die(); /** * Shows report_insights insights list. * * @package report_insights * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class insights_list implements \renderable, \templatable { /** * @var \core_analytics\model */ protected $model; /** * @var \context */ protected $context; /** * @var \core_analytics\model[] */ protected $othermodels; /** * @var int */ protected $page; /** * @var int */ protected $perpage; /** * Constructor * * @param \core_analytics\model $model * @param \context $context * @param \core_analytics\model[] $othermodels * @param int $page * @param int $perpage The max number of results to fetch * @return void */ public function __construct(\core_analytics\model $model, \context $context, $othermodels, $page = 0, $perpage = 100) { $this->model = $model; $this->context = $context; $this->othermodels = $othermodels; $this->page = $page; $this->perpage = $perpage; } /** * Exports the data. * * @param \renderer_base $output * @return \stdClass */ public function export_for_template(\renderer_base $output) { global $PAGE; $target = $this->model->get_target(); $data = new \stdClass(); $data->modelid = $this->model->get_id(); $data->contextid = $this->context->id; $targetname = $target->get_name(); $data->insightname = format_string($targetname); $targetinfostr = $targetname->get_identifier() . 'info'; if (get_string_manager()->string_exists($targetinfostr, $targetname->get_component())) { $data->insightdescription = get_string($targetinfostr, $targetname->get_component()); } $data->showpredictionheading = true; if (!$target->is_linear()) { $nclasses = count($target::get_classes()); $nignoredclasses = count($target->ignored_predicted_classes()); if ($nclasses - $nignoredclasses <= 1) { // Hide the prediction heading if there is only 1 class displayed. Otherwise it is redundant with the insight name. $data->showpredictionheading = false; } } $total = 0; if ($this->model->uses_insights()) { $target->add_bulk_actions_js(); $predictionsdata = $this->model->get_predictions($this->context, true, $this->page, $this->perpage); if (!$this->model->is_static()) { $notification = new \core\output\notification(get_string('justpredictions', 'report_insights')); $data->nostaticmodelnotification = $notification->export_for_template($output); } $data->predictions = array(); $predictionvalues = array(); $insights = array(); if ($predictionsdata) { list($total, $predictions) = $predictionsdata; if ($predictions) { // No bulk actions if no predictions. $data->bulkactions = actions_exporter::add_bulk_actions($target, $output, $predictions, $this->context); } $data->multiplepredictions = count($predictions) > 1 ? true : false; foreach ($predictions as $prediction) { $predictedvalue = $prediction->get_prediction_data()->prediction; // Only need to fill this data once. if (!isset($predictionvalues[$predictedvalue])) { $preddata = array(); $preddata['predictiondisplayvalue'] = $target->get_display_value($predictedvalue); list($preddata['style'], $preddata['outcomeicon']) = insight::get_calculation_display($target, floatval($predictedvalue), $output); $predictionvalues[$predictedvalue] = $preddata; } $insightrenderable = new \report_insights\output\insight($prediction, $this->model, true, $this->context); $insights[$predictedvalue][] = $insightrenderable->export_for_template($output); } // Order predicted values. if ($target->is_linear()) { // During regression what we will be interested on most of the time is in low values so let's show them first. ksort($predictionvalues); } else { // During classification targets flag "not that important" samples as 0 so let's show them at the end. krsort($predictionvalues); } // Ok, now we have all the data we want, put it into a format that mustache can handle. foreach ($predictionvalues as $key => $prediction) { if (isset($insights[$key])) { $toggleall = new \core\output\checkbox_toggleall('insight-bulk-action-' . $key, true, [ 'id' => 'id-toggle-all-' . $key, 'name' => 'toggle-all-' . $key, 'label' => get_string('selectall'), 'labelclasses' => 'visually-hidden', 'checked' => false ]); $prediction['checkboxtoggleall'] = $output->render($toggleall); $prediction['predictedvalue'] = $key; $prediction['insights'] = $insights[$key]; } $data->predictions[] = $prediction; } } if (empty($insights) && $this->page == 0) { if ($this->model->any_prediction_obtained()) { $data->noinsights = get_string('noinsights', 'analytics'); } else { $data->noinsights = get_string('nopredictionsyet', 'analytics'); } } } else { $data->noinsights = get_string('noinsights', 'analytics'); } if (!empty($data->noinsights)) { $notification = new \core\output\notification($data->noinsights); $data->noinsights = $notification->export_for_template($output); } $url = $PAGE->url; if ($this->othermodels) { $options = array(); foreach ($this->othermodels as $model) { $options[$model->get_id()] = $model->get_target()->get_name(); } // New moodle_url instance returned by magic_get_url. $url->remove_params('modelid'); $modelselector = new \single_select($url, 'modelid', $options, '', array('' => get_string('selectotherinsights', 'report_insights'))); $data->modelselector = $modelselector->export_for_template($output); } // Add the 'perpage' parameter to the url which is later used to generate the pagination links. $url->param('perpage', $this->perpage); $data->pagingbar = $output->render(new \paging_bar($total, $this->page, $this->perpage, $url)); return $data; } } PK{\M(insights/templates/bulk_actions.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 report_insights/bulk_actions Actions panel at the bottom of the assignment grading UI. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { } }}
    {{#bulkactions}} {{> report_insights/bulk_action_button}} {{/bulkactions}}
    PK{\p=n.insights/templates/bulk_action_button.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 report_insights/bulk_action_button Extension of core/single_button to show the label with HTML Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * classes - a list of classes to wrap the form. * url - the action url to submit to * params - array of params with name and value attributes * primary - true if this is a primary action button * id - id for the element * tooltip - tooltip text for the button * disabled - true if this element is disabled * label - text to show on the button Example context (json): { "url" : "#", "primary" : true, "tooltip" : "This is a tooltip", "label" : "This is a the button text, it can include HTML", "attributes": [ { "name": "data-attribute", "value": "yeah" } ] } }}
    {{#params}} {{/params}}
    PK{\Zꃽ)insights/templates/insights_list.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 report_insights/insights_list Template for the insights list. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { "contextid": 123, "modelid": 321, "insightname": "Best insight ever", "nostaticmodelnotification": { "message": "This is just a prediction." }, "showpredictionheading": "true", "predictions": [ { "predictiondisplayvalue": "This dev will understand it", "style": "success", "outcomeicon": { "attributes": [ {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" } ] }, "multiplepredictions": "true", "insights": [ { "sampleimage": "Link", "sampledescription": "Sample description", "actions": [ { "classes": "", "primary": { "items": [{"rawhtml": "

    View details

    "}] }, "secondary": { "items": [{"rawhtml": "

    Not useful

    "}] } } ] }, { "sampleimage": "Link", "sampledescription": "Sample description", "actions": [ { "classes": "", "primary": { "items": [{"rawhtml": "

    View details

    "}] }, "secondary": { "items": [{"rawhtml": "

    Not useful

    "}] } } ] } ] }, { "predictiondisplayvalue": "This dev will not understand it", "style": "danger", "outcomeicon": { "attributes": [ {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" } ] }, "insights": [ { "sampleimage": "Any renderable", "sampledescription": "Another sample description" } ] } ], "noinsights": false } }} {{#modelselector}}
    {{> core/single_select}}
    {{/modelselector}}

    {{insightname}}

    {{{insightdescription}}} {{^noinsights}} {{#nostaticmodelnotification}}
    {{> core/notification_info}}
    {{/nostaticmodelnotification}} {{{ pagingbar }}} {{> report_insights/bulk_actions}} {{#predictions}} {{#showpredictionheading}} {{/showpredictionheading}} {{#multiplepredictions}} {{/multiplepredictions}} {{^multiplepredictions}} {{#insights}} {{> report_insights/insight}} {{/insights}}
    {{#str}}prediction, report_insights{{/str}}: {{#outcomeicon}} {{> core/pix_icon}} {{/outcomeicon}} {{predictiondisplayvalue}}
    {{{checkboxtoggleall}}} {{/multiplepredictions}} {{#str}}description{{/str}} {{#str}}actions{{/str}}
    {{/predictions}} {{#multiplepredictions}} {{> report_insights/bulk_actions}} {{/multiplepredictions}} {{{ pagingbar }}} {{/noinsights}} {{#noinsights}}
    {{> core/notification_info}}
    {{/noinsights}}
    PK{\lt77+insights/templates/insight_details.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 report_insights/insight_details Actions panel at the bottom of the assignment grading UI. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { "contextid": 123, "modelid": 321, "insightname": "Best insight ever", "timecreated": "Thursday, 5 October 2017, 4:16 PM", "timerange": "Monday, 4 September 2017, 6:00 PM to Thursday, 5 October 2017, 12:00 AM", "sampleimage": "Link", "sampledescription": "Sample description", "actions": [ { "classes": "", "primary": { "items": [{"rawhtml": "

    View details

    "}] }, "secondary": { "items": [{"rawhtml": "

    Not useful

    "}] } } ], "style": "success", "outcomeicon": { "attributes": [ {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" } ] }, "predictiondisplayvalue": "This dev will understand it", "calculations": [ { "style": "success", "outcomeicon": { "attributes": [ {"name": "src", "value": "https://moodle.org/logo/moodle-logo.svg" } ] }, "name": "Indicator 1", "displayvalue": "yes" }, { "style": "info", "name": "Indicator 2", "displayvalue": "20%" }, { "style": "", "name": "Indicator 2", "displayvalue": "20%" }, { "style": "warning", "name": "Indicator 2", "displayvalue": "20%" }, { "style": "danger", "name": "Indicator 2", "displayvalue": "20%" } ] } }}

    {{insightname}}

    {{{insightdescription}}} {{> report_insights/bulk_actions }} {{#showpredictionheading}} {{/showpredictionheading}} {{> report_insights/insight}}
    {{#str}}prediction, report_insights{{/str}}: {{#outcomeicon}} {{> core/pix_icon}} {{/outcomeicon}} {{predictiondisplayvalue}}
    {{{hiddencheckboxtoggleall}}} {{#str}}description{{/str}} {{#str}}actions{{/str}}
    {{#timerange}} {{/timerange}}
    {{#str}}predictiondetails, report_insights{{/str}}
    {{#str}}timecreated, report_insights{{/str}} {{timecreated}}
    {{#str}}timerange, report_insights{{/str}} {{.}}
    {{#calculations}} {{/calculations}}
    {{#str}}indicators, report_insights{{/str}}
    {{name}} {{#outcomeicon}}{{> core/pix_icon}}{{/outcomeicon}} {{displayvalue}} {{#outcomehelp}}{{> core/help_icon}}{{/outcomehelp}}
    {{#nocalculations}} {{> core/notification_info}} {{/nocalculations}}
    PK{\$--#insights/templates/insight.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 report_insights/insight Template for a insight. Classes required for JS: * none Data attributes required for JS: * none Context variables required for this template: * none Example context (json): { "sampleimage": "Link", "sampledescription": "Sample description", "actions": [ { "classes": "", "primary": { "items": [{"rawhtml": "

    View details

    "}] }, "secondary": { "items": [{"rawhtml": "

    Not useful

    "}] } } ] } }} {{{toggleslave}}} {{#sampleimage}} {{{sampleimage}}} {{/sampleimage}} {{{sampledescription}}} {{#actions}} {{> core/action_menu}} {{/actions}} PK{\DQ insights/prediction.phpnu[. /** * View an insight. * * @package report_insights * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../config.php'); require_once($CFG->libdir . '/adminlib.php'); $predictionid = required_param('id', PARAM_INT); if (!\core_analytics\manager::is_analytics_enabled()) { $PAGE->set_context(\context_system::instance()); $renderer = $PAGE->get_renderer('report_insights'); echo $renderer->render_analytics_disabled(); exit(0); } list($model, $prediction, $context) = \core_analytics\manager::get_prediction($predictionid, true); if ($context->contextlevel < CONTEXT_COURSE) { // Only for higher levels than course. $PAGE->set_context($context); } $params = array('id' => $prediction->get_prediction_data()->id); $url = new \moodle_url('/report/insights/prediction.php', $params); $PAGE->set_url($url); $PAGE->set_pagelayout('report'); $navurl = new \moodle_url('/report/insights/insights.php', array('contextid' => $context->id)); if ($context->contextlevel === CONTEXT_SYSTEM) { admin_externalpage_setup('reportinsights', '', null, '', array('pagelayout' => 'report')); } else if ($context->contextlevel === CONTEXT_USER) { $user = \core_user::get_user($context->instanceid, '*', MUST_EXIST); $PAGE->navigation->extend_for_user($user); $modelinsightsurl = clone $navurl; $modelinsightsurl->param('modelid', $model->get_id()); $PAGE->add_report_nodes($user->id, array( 'name' => get_string('insights', 'report_insights'), 'url' => $url )); } $PAGE->navigation->override_active_url($navurl); $renderer = $PAGE->get_renderer('report_insights'); $insightinfo = new stdClass(); $insightinfo->contextname = $context->get_context_name(); $insightinfo->insightname = $model->get_target()->get_name(); $modelready = $model->is_enabled() && $model->is_trained() && $model->predictions_exist($context); if (!$modelready) { echo $renderer->render_model_disabled($insightinfo); exit(0); } if (!$model->uses_insights()) { echo $renderer->render_no_insights_model($context); exit(0); } if ($context->id == SYSCONTEXTID) { $PAGE->set_heading(get_site()->shortname); } else { $PAGE->set_heading($insightinfo->contextname); } $PAGE->set_title($insightinfo->insightname); echo $OUTPUT->header(); $renderable = new \report_insights\output\insight($prediction, $model, false, $context); echo $renderer->render($renderable); echo $OUTPUT->footer(); PK{\)!gginsights/insights.phpnu[. /** * View model insights. * * @package report_insights * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ use core\report_helper; require_once(__DIR__ . '/../../config.php'); require_once($CFG->libdir . '/adminlib.php'); $contextid = required_param('contextid', PARAM_INT); $modelid = optional_param('modelid', false, PARAM_INT); $page = optional_param('page', 0, PARAM_INT); $perpage = optional_param('perpage', 100, PARAM_INT); if ($perpage > 1000) { $perpage = 1000; } list($context, $course, $cm) = get_context_info_array($contextid); require_login($course, false, $cm); if ($context->contextlevel < CONTEXT_COURSE) { // Only for higher levels than course. $PAGE->set_context($context); } if (!\core_analytics\manager::is_analytics_enabled()) { $renderer = $PAGE->get_renderer('report_insights'); echo $renderer->render_analytics_disabled(); exit(0); } \core_analytics\manager::check_can_list_insights($context); // Get all models that are enabled, trained and have predictions at this context. $othermodels = \core_analytics\manager::get_all_models(true, true, $context); array_filter($othermodels, function($model) use ($context) { // Discard insights that are not linked unless you are a manager. if (!$model->get_target()->link_insights_report()) { try { \core_analytics\manager::check_can_manage_models(); } catch (\required_capability_exception $e) { return false; } } return true; }); if (!$modelid && count($othermodels)) { // Autoselect the only available model. $model = reset($othermodels); $modelid = $model->get_id(); } if ($modelid) { unset($othermodels[$modelid]); } // The URL in navigation only contains the contextid. $params = array('contextid' => $contextid); $navurl = new \moodle_url('/report/insights/insights.php', $params); // This is the real page url, we need it to include the modelid so pagination and // other stuff works as expected. $url = clone $navurl; if ($modelid) { $url->param('modelid', $modelid); } $PAGE->set_url($url); $PAGE->set_pagelayout('report'); if ($context->contextlevel === CONTEXT_SYSTEM) { admin_externalpage_setup('reportinsights', '', $url->params(), $url->out(false), array('pagelayout' => 'report')); } else if ($context->contextlevel === CONTEXT_USER) { $user = \core_user::get_user($context->instanceid, '*', MUST_EXIST); $PAGE->navigation->extend_for_user($user); $PAGE->add_report_nodes($user->id, array( 'name' => get_string('insights', 'report_insights'), 'url' => $url )); } $PAGE->navigation->override_active_url($navurl); $renderer = $PAGE->get_renderer('report_insights'); // No models with insights available at this context level. if (!$modelid) { echo $renderer->render_no_insights($context); exit(0); } $model = new \core_analytics\model($modelid); if (!$model->get_target()->link_insights_report()) { // Only manager access if this target does not link the insights report. \core_analytics\manager::check_can_manage_models(); } $insightinfo = new stdClass(); // Don't show prefix for course-level context. $withprefix = $context->contextlevel <> CONTEXT_COURSE; $insightinfo->contextname = $context->get_context_name($withprefix); $insightinfo->insightname = $model->get_target()->get_name(); if (!$model->is_enabled()) { echo $renderer->render_model_disabled($insightinfo); exit(0); } if (!$model->uses_insights()) { echo $renderer->render_no_insights_model($context); exit(0); } if ($context->id == SYSCONTEXTID) { $PAGE->set_heading(get_site()->shortname); } else { $PAGE->set_heading($insightinfo->contextname); } $PAGE->set_title($insightinfo->insightname); // Some models generate one single prediction per context. We can directly show the prediction details in this case. if ($model->get_analyser()::one_sample_per_analysable()) { // Param $perpage to 2 so we can detect if this model's analyser is using one_sample_per_analysable incorrectly. $predictionsdata = $model->get_predictions($context, true, 0, 2); if ($predictionsdata) { list($total, $predictions) = $predictionsdata; if ($total > 1) { throw new \coding_exception('This model\'s analyser processed more than one sample for a single analysable element.' . 'Therefore, the analyser\'s one_sample_per_analysable() method should return false.'); } $prediction = reset($predictions); $redirecturl = new \moodle_url('/report/insights/prediction.php', ['id' => $prediction->get_prediction_data()->id]); redirect($redirecturl); } } echo $OUTPUT->header(); if ($course) { // Print selected drop down. $pluginname = get_string('pluginname', 'report_insights'); report_helper::print_report_selector($pluginname); } $renderable = new \report_insights\output\insights_list($model, $context, $othermodels, $page, $perpage); echo $renderer->render($renderable); $eventdata = array ( 'context' => $context, 'other' => array('modelid' => $model->get_id()) ); \core\event\insights_viewed::create($eventdata)->trigger(); echo $OUTPUT->footer(); PK{\C<88!insights/amd/src/message_users.jsnu[// 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 . /** * Message users. * * @module report_insights/message_users * @copyright 2019 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define(['jquery', 'core/str', 'core/log', 'core/modal_save_cancel', 'core/modal_events', 'core/templates', 'core/notification', 'core/ajax'], function($, Str, Log, ModalSaveCancel, ModalEvents, Templates, Notification, Ajax) { var SELECTORS = { BULKACTIONSELECT: "#formactionid" }; /** * Constructor. * * @param {String} rootNode * @param {String} actionName */ var MessageUsers = function(rootNode, actionName) { this.actionName = actionName; this.attachEventListeners(rootNode); }; /** * @var {String} actionName * @private */ MessageUsers.prototype.actionName = null; /** * @var {Modal} modal * @private */ MessageUsers.prototype.modal = null; /** * Attach the event listener to the send message bulk action. * @param {String} rootNode */ MessageUsers.prototype.attachEventListeners = function(rootNode) { $(rootNode + ' button[data-bulk-sendmessage]').on('click', function(e) { e.preventDefault(); var cTarget = $(e.currentTarget); // Using an associative array in case there is more than 1 prediction for the same user. var users = {}; var predictionToUserMapping = cTarget.data('prediction-to-user-id'); var checkedSelector = '.insights-list input[data-togglegroup^="insight-bulk-action"][data-toggle="slave"]:checked'; $(checkedSelector).each(function(index, value) { var predictionId = $(value).closest('tr[data-prediction-id]').data('prediction-id'); if (typeof predictionToUserMapping[predictionId] === 'undefined') { Log.error('Unknown user for prediction ' + predictionId); return; } var userId = predictionToUserMapping[predictionId]; users[predictionId] = userId; }); if (Object.keys(users).length === 0) { return this; } this.showSendMessage(users); return this; }.bind(this)); }; /** * Show the send message popup. * * @method showSendMessage * @private * @param {Object} users Prediction id to user id mapping. * @returns {Promise} */ MessageUsers.prototype.showSendMessage = function(users) { var userIds = new Set(Object.values(users)); if (userIds.length == 0) { // Nothing to do. return $.Deferred().resolve().promise(); } var titlePromise = null; if (userIds.size == 1) { titlePromise = Str.get_string('sendbulkmessagesingle', 'core_message'); } else { titlePromise = Str.get_string('sendbulkmessage', 'core_message', userIds.size); } // eslint-disable-next-line promise/catch-or-return ModalSaveCancel.create({ body: Templates.render('core_user/send_bulk_message', {}), title: titlePromise, buttons: { save: titlePromise, }, show: true, }) .then(function(modal) { // Keep a reference to the modal. this.modal = modal; // We want to focus on the action select when the dialog is closed. this.modal.getRoot().on(ModalEvents.hidden, function() { $(SELECTORS.BULKACTIONSELECT).focus(); this.modal.getRoot().remove(); }.bind(this)); this.modal.getRoot().on(ModalEvents.save, this.submitSendMessage.bind(this, users)); return this.modal; }.bind(this)); }; /** * Send a message to these users. * * @method submitSendMessage * @private * @param {Object} users Prediction id to user id mapping. * @returns {Promise} */ MessageUsers.prototype.submitSendMessage = function(users) { var messageText = this.modal.getRoot().find('form textarea').val(); var messages = []; var userIds = new Set(Object.values(users)); userIds.forEach(function(userId) { messages.push({touserid: userId, text: messageText}); }); var actionName = this.actionName; var message = null; return Ajax.call([{ methodname: 'core_message_send_instant_messages', args: {messages: messages} }])[0].then(function(messageIds) { if (messageIds.length == 1) { return Str.get_string('sendbulkmessagesentsingle', 'core_message'); } else { return Str.get_string('sendbulkmessagesent', 'core_message', messageIds.length); } }).then(function(msg) { // Save this for the following callback. Now that we got everything // done we can flag this action as executed. message = msg; return Ajax.call([{ methodname: 'report_insights_action_executed', args: { actionname: actionName, predictionids: Object.keys(users) } }])[0]; }).then(function() { Notification.addNotification({ message: message, type: "success" }); return true; }).catch(Notification.exception); }; return /** @alias module:report_insights/message_users */ { // Public variables and functions. /** * @method init * @param {String} rootNode * @param {String} actionName * @returns {MessageUsers} */ 'init': function(rootNode, actionName) { return new MessageUsers(rootNode, actionName); } }; }); PK{\Khminsights/amd/src/actions.jsnu[// 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 . /** * Module to manage report insights actions that are executed using AJAX. * * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * This module manages prediction actions that require AJAX requests. * * @module report_insights/actions */ import {get_string as getString} from 'core/str'; import Ajax from 'core/ajax'; import Notification from 'core/notification'; import Url from 'core/url'; import ModalEvents from 'core/modal_events'; import ModalSaveCancel from 'core/modal_save_cancel'; /** * Executes the provided action. * * @param {Array} predictionids * @param {String} actionname * @return {Promise} */ const markActionExecuted = (predictionids, actionname) => Ajax.call([ { methodname: 'report_insights_action_executed', args: { actionname, predictionids, }, } ])[0]; const getPredictionTable = (predictionContainers) => { for (const el of predictionContainers) { if (el.closest('table')) { return el.closest('table'); } } return null; }; const executeAction = (predictionIds, predictionContainers, actionName) => { markActionExecuted(predictionIds, actionName).then(() => { // Remove the selected elements from the list. const tableNode = getPredictionTable(predictionContainers); predictionContainers.forEach((el) => el.remove()); if (!tableNode.querySelector('tbody > tr')) { const params = { contextid: tableNode.closest('div.insight-container').dataset.contextId, modelid: tableNode.closest('div.insight-container').dataset.modelId, }; window.location.assign(Url.relativeUrl("report/insights/insights.php", params, false)); } return; }).catch(Notification.exception); }; /** * Attach on click handlers for bulk actions. * * @param {String} rootNode * @access public */ export const initBulk = (rootNode) => { document.addEventListener('click', (e) => { const action = e.target.closest(`${rootNode} [data-bulk-actionname]`); if (!action) { return; } e.preventDefault(); const actionName = action.dataset.bulkActionname; const actionVisibleName = action.textContent.trim(); const predictionContainers = Array.from(document.querySelectorAll( '.insights-list input[data-togglegroup^="insight-bulk-action-"][data-toggle="slave"]:checked', )).map((checkbox) => checkbox.closest('tr[data-prediction-id]')); const predictionIds = predictionContainers.map((el) => el.dataset.predictionId); if (predictionIds.length === 0) { // No items selected message. return; } const stringParams = { action: actionVisibleName, nitems: predictionIds.length, }; ModalSaveCancel.create({ title: actionVisibleName, body: getString('confirmbulkaction', 'report_insights', stringParams), buttons: { save: getString('confirm'), }, show: true, }).then((modal) => { modal.getRoot().on(ModalEvents.save, function() { // The action is now confirmed, sending an action for it. return executeAction(predictionIds, predictionContainers, actionName); }); return modal; }).catch(Notification.exception); }); }; PK{\r !insights/amd/build/actions.min.jsnu[define("report_insights/actions",["exports","core/str","core/ajax","core/notification","core/url","core/modal_events","core/modal_save_cancel"],(function(_exports,_str,_ajax,_notification,_url,_modal_events,_modal_save_cancel){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Module to manage report insights actions that are executed using AJAX. * * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.initBulk=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_url=_interopRequireDefault(_url),_modal_events=_interopRequireDefault(_modal_events),_modal_save_cancel=_interopRequireDefault(_modal_save_cancel);const executeAction=(predictionIds,predictionContainers,actionName)=>{var predictionids,actionname;(predictionids=predictionIds,actionname=actionName,_ajax.default.call([{methodname:"report_insights_action_executed",args:{actionname:actionname,predictionids:predictionids}}])[0]).then((()=>{const tableNode=(predictionContainers=>{for(const el of predictionContainers)if(el.closest("table"))return el.closest("table");return null})(predictionContainers);if(predictionContainers.forEach((el=>el.remove())),!tableNode.querySelector("tbody > tr")){const params={contextid:tableNode.closest("div.insight-container").dataset.contextId,modelid:tableNode.closest("div.insight-container").dataset.modelId};window.location.assign(_url.default.relativeUrl("report/insights/insights.php",params,!1))}})).catch(_notification.default.exception)};_exports.initBulk=rootNode=>{document.addEventListener("click",(e=>{const action=e.target.closest("".concat(rootNode," [data-bulk-actionname]"));if(!action)return;e.preventDefault();const actionName=action.dataset.bulkActionname,actionVisibleName=action.textContent.trim(),predictionContainers=Array.from(document.querySelectorAll('.insights-list input[data-togglegroup^="insight-bulk-action-"][data-toggle="slave"]:checked')).map((checkbox=>checkbox.closest("tr[data-prediction-id]"))),predictionIds=predictionContainers.map((el=>el.dataset.predictionId));if(0===predictionIds.length)return;const stringParams={action:actionVisibleName,nitems:predictionIds.length};_modal_save_cancel.default.create({title:actionVisibleName,body:(0,_str.get_string)("confirmbulkaction","report_insights",stringParams),buttons:{save:(0,_str.get_string)("confirm")},show:!0}).then((modal=>(modal.getRoot().on(_modal_events.default.save,(function(){return executeAction(predictionIds,predictionContainers,actionName)})),modal))).catch(_notification.default.exception)}))}})); //# sourceMappingURL=actions.min.js.mapPK{\!aa%insights/amd/build/actions.min.js.mapnu[{"version":3,"file":"actions.min.js","sources":["../src/actions.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module to manage report insights actions that are executed using AJAX.\n *\n * @copyright 2017 David Monllao {@link http://www.davidmonllao.com}\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * This module manages prediction actions that require AJAX requests.\n *\n * @module report_insights/actions\n */\n\nimport {get_string as getString} from 'core/str';\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\nimport Url from 'core/url';\nimport ModalEvents from 'core/modal_events';\nimport ModalSaveCancel from 'core/modal_save_cancel';\n\n\n/**\n * Executes the provided action.\n *\n * @param {Array} predictionids\n * @param {String} actionname\n * @return {Promise}\n */\nconst markActionExecuted = (predictionids, actionname) => Ajax.call([\n {\n methodname: 'report_insights_action_executed',\n args: {\n actionname,\n predictionids,\n },\n }\n])[0];\n\nconst getPredictionTable = (predictionContainers) => {\n for (const el of predictionContainers) {\n if (el.closest('table')) {\n return el.closest('table');\n }\n }\n\n return null;\n};\n\nconst executeAction = (predictionIds, predictionContainers, actionName) => {\n markActionExecuted(predictionIds, actionName).then(() => {\n // Remove the selected elements from the list.\n const tableNode = getPredictionTable(predictionContainers);\n predictionContainers.forEach((el) => el.remove());\n\n if (!tableNode.querySelector('tbody > tr')) {\n const params = {\n contextid: tableNode.closest('div.insight-container').dataset.contextId,\n modelid: tableNode.closest('div.insight-container').dataset.modelId,\n };\n window.location.assign(Url.relativeUrl(\"report/insights/insights.php\", params, false));\n }\n return;\n }).catch(Notification.exception);\n};\n\n/**\n * Attach on click handlers for bulk actions.\n *\n * @param {String} rootNode\n * @access public\n */\nexport const initBulk = (rootNode) => {\n document.addEventListener('click', (e) => {\n const action = e.target.closest(`${rootNode} [data-bulk-actionname]`);\n if (!action) {\n return;\n }\n\n e.preventDefault();\n const actionName = action.dataset.bulkActionname;\n const actionVisibleName = action.textContent.trim();\n\n const predictionContainers = Array.from(document.querySelectorAll(\n '.insights-list input[data-togglegroup^=\"insight-bulk-action-\"][data-toggle=\"slave\"]:checked',\n )).map((checkbox) => checkbox.closest('tr[data-prediction-id]'));\n const predictionIds = predictionContainers.map((el) => el.dataset.predictionId);\n\n if (predictionIds.length === 0) {\n // No items selected message.\n return;\n }\n\n const stringParams = {\n action: actionVisibleName,\n nitems: predictionIds.length,\n };\n\n ModalSaveCancel.create({\n title: actionVisibleName,\n body: getString('confirmbulkaction', 'report_insights', stringParams),\n buttons: {\n save: getString('confirm'),\n },\n show: true,\n }).then((modal) => {\n modal.getRoot().on(ModalEvents.save, function() {\n // The action is now confirmed, sending an action for it.\n return executeAction(predictionIds, predictionContainers, actionName);\n });\n\n return modal;\n }).catch(Notification.exception);\n });\n};\n"],"names":["executeAction","predictionIds","predictionContainers","actionName","predictionids","actionname","Ajax","call","methodname","args","then","tableNode","el","closest","getPredictionTable","forEach","remove","querySelector","params","contextid","dataset","contextId","modelid","modelId","window","location","assign","Url","relativeUrl","catch","Notification","exception","rootNode","document","addEventListener","e","action","target","preventDefault","bulkActionname","actionVisibleName","textContent","trim","Array","from","querySelectorAll","map","checkbox","predictionId","length","stringParams","nitems","create","title","body","buttons","save","show","modal","getRoot","on","ModalEvents"],"mappings":";;;;;;wUA+DMA,cAAgB,CAACC,cAAeC,qBAAsBC,cApBjC,IAACC,cAAeC,YAAfD,cAqBLH,cArBoBI,WAqBLF,WArBoBG,cAAKC,KAAK,CAChE,CACIC,WAAY,kCACZC,KAAM,CACFJ,WAAAA,WACAD,cAAAA,kBAGT,IAa+CM,MAAK,WAEzCC,UAbcT,CAAAA,2BACnB,MAAMU,MAAMV,wBACTU,GAAGC,QAAQ,gBACJD,GAAGC,QAAQ,gBAInB,MAMeC,CAAmBZ,yBACrCA,qBAAqBa,SAASH,IAAOA,GAAGI,YAEnCL,UAAUM,cAAc,cAAe,OAClCC,OAAS,CACXC,UAAWR,UAAUE,QAAQ,yBAAyBO,QAAQC,UAC9DC,QAASX,UAAUE,QAAQ,yBAAyBO,QAAQG,SAEhEC,OAAOC,SAASC,OAAOC,aAAIC,YAAY,+BAAgCV,QAAQ,QAGpFW,MAAMC,sBAAaC,8BASDC,WACrBC,SAASC,iBAAiB,SAAUC,UAC1BC,OAASD,EAAEE,OAAOxB,kBAAWmB,yCAC9BI,cAILD,EAAEG,uBACInC,WAAaiC,OAAOhB,QAAQmB,eAC5BC,kBAAoBJ,OAAOK,YAAYC,OAEvCxC,qBAAuByC,MAAMC,KAAKX,SAASY,iBAC7C,gGACDC,KAAKC,UAAaA,SAASlC,QAAQ,4BAChCZ,cAAgBC,qBAAqB4C,KAAKlC,IAAOA,GAAGQ,QAAQ4B,kBAErC,IAAzB/C,cAAcgD,oBAKZC,aAAe,CACjBd,OAAQI,kBACRW,OAAQlD,cAAcgD,mCAGVG,OAAO,CACnBC,MAAOb,kBACPc,MAAM,mBAAU,oBAAqB,kBAAmBJ,cACxDK,QAAS,CACLC,MAAM,mBAAU,YAEpBC,MAAM,IACP/C,MAAMgD,QACLA,MAAMC,UAAUC,GAAGC,sBAAYL,MAAM,kBAE1BxD,cAAcC,cAAeC,qBAAsBC,eAGvDuD,SACR7B,MAAMC,sBAAaC"}PK{\%D%%+insights/amd/build/message_users.min.js.mapnu[{"version":3,"file":"message_users.min.js","sources":["../src/message_users.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Message users.\n *\n * @module report_insights/message_users\n * @copyright 2019 David Monllao\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/str', 'core/log', 'core/modal_save_cancel', 'core/modal_events', 'core/templates',\n 'core/notification', 'core/ajax'],\n function($, Str, Log, ModalSaveCancel, ModalEvents, Templates, Notification, Ajax) {\n\n var SELECTORS = {\n BULKACTIONSELECT: \"#formactionid\"\n };\n\n /**\n * Constructor.\n *\n * @param {String} rootNode\n * @param {String} actionName\n */\n var MessageUsers = function(rootNode, actionName) {\n this.actionName = actionName;\n this.attachEventListeners(rootNode);\n };\n\n /**\n * @var {String} actionName\n * @private\n */\n MessageUsers.prototype.actionName = null;\n\n /**\n * @var {Modal} modal\n * @private\n */\n MessageUsers.prototype.modal = null;\n\n /**\n * Attach the event listener to the send message bulk action.\n * @param {String} rootNode\n */\n MessageUsers.prototype.attachEventListeners = function(rootNode) {\n $(rootNode + ' button[data-bulk-sendmessage]').on('click', function(e) {\n e.preventDefault();\n var cTarget = $(e.currentTarget);\n\n // Using an associative array in case there is more than 1 prediction for the same user.\n var users = {};\n var predictionToUserMapping = cTarget.data('prediction-to-user-id');\n\n var checkedSelector = '.insights-list input[data-togglegroup^=\"insight-bulk-action\"][data-toggle=\"slave\"]:checked';\n $(checkedSelector).each(function(index, value) {\n var predictionId = $(value).closest('tr[data-prediction-id]').data('prediction-id');\n\n if (typeof predictionToUserMapping[predictionId] === 'undefined') {\n Log.error('Unknown user for prediction ' + predictionId);\n return;\n }\n\n var userId = predictionToUserMapping[predictionId];\n users[predictionId] = userId;\n\n });\n\n if (Object.keys(users).length === 0) {\n return this;\n }\n\n this.showSendMessage(users);\n\n return this;\n }.bind(this));\n };\n\n /**\n * Show the send message popup.\n *\n * @method showSendMessage\n * @private\n * @param {Object} users Prediction id to user id mapping.\n * @returns {Promise}\n */\n MessageUsers.prototype.showSendMessage = function(users) {\n\n var userIds = new Set(Object.values(users));\n\n if (userIds.length == 0) {\n // Nothing to do.\n return $.Deferred().resolve().promise();\n }\n var titlePromise = null;\n if (userIds.size == 1) {\n titlePromise = Str.get_string('sendbulkmessagesingle', 'core_message');\n } else {\n titlePromise = Str.get_string('sendbulkmessage', 'core_message', userIds.size);\n }\n\n // eslint-disable-next-line promise/catch-or-return\n ModalSaveCancel.create({\n body: Templates.render('core_user/send_bulk_message', {}),\n title: titlePromise,\n buttons: {\n save: titlePromise,\n },\n show: true,\n })\n .then(function(modal) {\n // Keep a reference to the modal.\n this.modal = modal;\n\n // We want to focus on the action select when the dialog is closed.\n this.modal.getRoot().on(ModalEvents.hidden, function() {\n $(SELECTORS.BULKACTIONSELECT).focus();\n this.modal.getRoot().remove();\n }.bind(this));\n\n this.modal.getRoot().on(ModalEvents.save, this.submitSendMessage.bind(this, users));\n\n return this.modal;\n }.bind(this));\n };\n\n /**\n * Send a message to these users.\n *\n * @method submitSendMessage\n * @private\n * @param {Object} users Prediction id to user id mapping.\n * @returns {Promise}\n */\n MessageUsers.prototype.submitSendMessage = function(users) {\n\n var messageText = this.modal.getRoot().find('form textarea').val();\n\n var messages = [];\n\n var userIds = new Set(Object.values(users));\n userIds.forEach(function(userId) {\n messages.push({touserid: userId, text: messageText});\n });\n\n var actionName = this.actionName;\n var message = null;\n return Ajax.call([{\n methodname: 'core_message_send_instant_messages',\n args: {messages: messages}\n }])[0].then(function(messageIds) {\n if (messageIds.length == 1) {\n return Str.get_string('sendbulkmessagesentsingle', 'core_message');\n } else {\n return Str.get_string('sendbulkmessagesent', 'core_message', messageIds.length);\n }\n }).then(function(msg) {\n\n // Save this for the following callback. Now that we got everything\n // done we can flag this action as executed.\n message = msg;\n\n return Ajax.call([{\n methodname: 'report_insights_action_executed',\n args: {\n actionname: actionName,\n predictionids: Object.keys(users)\n }\n }])[0];\n }).then(function() {\n Notification.addNotification({\n message: message,\n type: \"success\"\n });\n return true;\n }).catch(Notification.exception);\n };\n\n return /** @alias module:report_insights/message_users */ {\n // Public variables and functions.\n\n /**\n * @method init\n * @param {String} rootNode\n * @param {String} actionName\n * @returns {MessageUsers}\n */\n 'init': function(rootNode, actionName) {\n return new MessageUsers(rootNode, actionName);\n }\n };\n});\n"],"names":["define","$","Str","Log","ModalSaveCancel","ModalEvents","Templates","Notification","Ajax","SELECTORS","MessageUsers","rootNode","actionName","attachEventListeners","prototype","modal","on","e","preventDefault","cTarget","currentTarget","users","predictionToUserMapping","data","each","index","value","predictionId","closest","userId","error","Object","keys","length","showSendMessage","this","bind","userIds","Set","values","Deferred","resolve","promise","titlePromise","size","get_string","create","body","render","title","buttons","save","show","then","getRoot","hidden","focus","remove","submitSendMessage","messageText","find","val","messages","forEach","push","touserid","text","message","call","methodname","args","messageIds","msg","actionname","predictionids","addNotification","type","catch","exception"],"mappings":";;;;;;;AAsBAA,uCAAO,CAAC,SAAU,WAAY,WAAY,yBAA0B,oBAAqB,iBACrF,oBAAqB,cACjB,SAASC,EAAGC,IAAKC,IAAKC,gBAAiBC,YAAaC,UAAWC,aAAcC,UAE7EC,2BACkB,gBASlBC,aAAe,SAASC,SAAUC,iBAC7BA,WAAaA,gBACbC,qBAAqBF,kBAO9BD,aAAaI,UAAUF,WAAa,KAMpCF,aAAaI,UAAUC,MAAQ,KAM/BL,aAAaI,UAAUD,qBAAuB,SAASF,UACnDV,EAAEU,SAAW,kCAAkCK,GAAG,QAAS,SAASC,GAChEA,EAAEC,qBACEC,QAAUlB,EAAEgB,EAAEG,eAGdC,MAAQ,GACRC,wBAA0BH,QAAQI,KAAK,gCAG3CtB,EADsB,8FACHuB,MAAK,SAASC,MAAOC,WAChCC,aAAe1B,EAAEyB,OAAOE,QAAQ,0BAA0BL,KAAK,yBAEd,IAA1CD,wBAAwBK,mBAK/BE,OAASP,wBAAwBK,cACrCN,MAAMM,cAAgBE,YALlB1B,IAAI2B,MAAM,+BAAiCH,iBASjB,IAA9BI,OAAOC,KAAKX,OAAOY,aAIlBC,gBAAgBb,OAHVc,MAMbC,KAAKD,QAWXzB,aAAaI,UAAUoB,gBAAkB,SAASb,WAE1CgB,QAAU,IAAIC,IAAIP,OAAOQ,OAAOlB,WAEd,GAAlBgB,QAAQJ,cAEDhC,EAAEuC,WAAWC,UAAUC,cAE9BC,aAAe,KAEfA,aADgB,GAAhBN,QAAQO,KACO1C,IAAI2C,WAAW,wBAAyB,gBAExC3C,IAAI2C,WAAW,kBAAmB,eAAgBR,QAAQO,MAI7ExC,gBAAgB0C,OAAO,CACnBC,KAAMzC,UAAU0C,OAAO,8BAA+B,IACtDC,MAAON,aACPO,QAAS,CACLC,KAAMR,cAEVS,MAAM,IAETC,KAAK,SAAStC,mBAENA,MAAQA,WAGRA,MAAMuC,UAAUtC,GAAGX,YAAYkD,OAAQ,WACxCtD,EAAEQ,4BAA4B+C,aACzBzC,MAAMuC,UAAUG,UACvBrB,KAAKD,YAEFpB,MAAMuC,UAAUtC,GAAGX,YAAY8C,KAAMhB,KAAKuB,kBAAkBtB,KAAKD,KAAMd,QAErEc,KAAKpB,OACdqB,KAAKD,QAWXzB,aAAaI,UAAU4C,kBAAoB,SAASrC,WAE5CsC,YAAcxB,KAAKpB,MAAMuC,UAAUM,KAAK,iBAAiBC,MAEzDC,SAAW,GAED,IAAIxB,IAAIP,OAAOQ,OAAOlB,QAC5B0C,SAAQ,SAASlC,QACrBiC,SAASE,KAAK,CAACC,SAAUpC,OAAQqC,KAAMP,qBAGvC/C,WAAauB,KAAKvB,WAClBuD,QAAU,YACP3D,KAAK4D,KAAK,CAAC,CACdC,WAAY,qCACZC,KAAM,CAACR,SAAUA,aACjB,GAAGT,MAAK,SAASkB,mBACQ,GAArBA,WAAWtC,OACJ/B,IAAI2C,WAAW,4BAA6B,gBAE5C3C,IAAI2C,WAAW,sBAAuB,eAAgB0B,WAAWtC,WAE7EoB,MAAK,SAASmB,YAIbL,QAAUK,IAEHhE,KAAK4D,KAAK,CAAC,CACdC,WAAY,kCACZC,KAAM,CACFG,WAAY7D,WACZ8D,cAAe3C,OAAOC,KAAKX,WAE/B,MACLgC,MAAK,kBACJ9C,aAAaoE,gBAAgB,CACzBR,QAASA,QACTS,KAAM,aAEH,KACRC,MAAMtE,aAAauE,YAGgC,MAS9C,SAASnE,SAAUC,mBAChB,IAAIF,aAAaC,SAAUC"}PK{\qA A 'insights/amd/build/message_users.min.jsnu[/** * Message users. * * @module report_insights/message_users * @copyright 2019 David Monllao * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ define("report_insights/message_users",["jquery","core/str","core/log","core/modal_save_cancel","core/modal_events","core/templates","core/notification","core/ajax"],(function($,Str,Log,ModalSaveCancel,ModalEvents,Templates,Notification,Ajax){var SELECTORS_BULKACTIONSELECT="#formactionid",MessageUsers=function(rootNode,actionName){this.actionName=actionName,this.attachEventListeners(rootNode)};return MessageUsers.prototype.actionName=null,MessageUsers.prototype.modal=null,MessageUsers.prototype.attachEventListeners=function(rootNode){$(rootNode+" button[data-bulk-sendmessage]").on("click",function(e){e.preventDefault();var cTarget=$(e.currentTarget),users={},predictionToUserMapping=cTarget.data("prediction-to-user-id");return $('.insights-list input[data-togglegroup^="insight-bulk-action"][data-toggle="slave"]:checked').each((function(index,value){var predictionId=$(value).closest("tr[data-prediction-id]").data("prediction-id");if(void 0!==predictionToUserMapping[predictionId]){var userId=predictionToUserMapping[predictionId];users[predictionId]=userId}else Log.error("Unknown user for prediction "+predictionId)})),0===Object.keys(users).length||this.showSendMessage(users),this}.bind(this))},MessageUsers.prototype.showSendMessage=function(users){var userIds=new Set(Object.values(users));if(0==userIds.length)return $.Deferred().resolve().promise();var titlePromise=null;titlePromise=1==userIds.size?Str.get_string("sendbulkmessagesingle","core_message"):Str.get_string("sendbulkmessage","core_message",userIds.size),ModalSaveCancel.create({body:Templates.render("core_user/send_bulk_message",{}),title:titlePromise,buttons:{save:titlePromise},show:!0}).then(function(modal){return this.modal=modal,this.modal.getRoot().on(ModalEvents.hidden,function(){$(SELECTORS_BULKACTIONSELECT).focus(),this.modal.getRoot().remove()}.bind(this)),this.modal.getRoot().on(ModalEvents.save,this.submitSendMessage.bind(this,users)),this.modal}.bind(this))},MessageUsers.prototype.submitSendMessage=function(users){var messageText=this.modal.getRoot().find("form textarea").val(),messages=[];new Set(Object.values(users)).forEach((function(userId){messages.push({touserid:userId,text:messageText})}));var actionName=this.actionName,message=null;return Ajax.call([{methodname:"core_message_send_instant_messages",args:{messages:messages}}])[0].then((function(messageIds){return 1==messageIds.length?Str.get_string("sendbulkmessagesentsingle","core_message"):Str.get_string("sendbulkmessagesent","core_message",messageIds.length)})).then((function(msg){return message=msg,Ajax.call([{methodname:"report_insights_action_executed",args:{actionname:actionName,predictionids:Object.keys(users)}}])[0]})).then((function(){return Notification.addNotification({message:message,type:"success"}),!0})).catch(Notification.exception)},{init:function(rootNode,actionName){return new MessageUsers(rootNode,actionName)}}})); //# sourceMappingURL=message_users.min.js.mapPK{\Տ=insights/UPGRADING.mdnu[# report_insights Upgrade notes ## 5.0 ### Removed - report_insights_set_notuseful_prediction() external function has been fully removed. For more information see [MDL-84036](https://tracker.moodle.org/browse/MDL-84036) - report_insights_set_fixed_prediction() external function has been fully removed. For more information see [MDL-84036](https://tracker.moodle.org/browse/MDL-84036) PK{\/#insights/tests/externallib_test.phpnu[. /** * Unit tests for report_insights externallib. * * @package report_insights * @copyright 2019 David Monllaó {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_insights; defined('MOODLE_INTERNAL') || die(); require_once(__DIR__ . '/../../../analytics/tests/fixtures/test_indicator_max.php'); require_once(__DIR__ . '/../../../analytics/tests/fixtures/test_target_shortname.php'); /** * Unit tests for report_insights externallib. * * @package report_insights * @copyright 2019 David Monllaó {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class externallib_test extends \advanced_testcase { /** * test_action_executed */ public function test_action_executed(): void { global $DB; $this->setAdminUser(); $target = \core_analytics\manager::get_target('test_target_shortname'); $indicators = array('test_indicator_max'); foreach ($indicators as $key => $indicator) { $indicators[$key] = \core_analytics\manager::get_indicator($indicator); } $model = \core_analytics\model::create($target, $indicators); $modelobj = $model->get_model_obj(); $model->enable('\core\analytics\time_splitting\single_range'); $this->resetAfterTest(true); $course1 = $this->getDataGenerator()->create_course(); $course2 = $this->getDataGenerator()->create_course(); $context = \context_course::instance($course1->id); $teacher1 = $this->getDataGenerator()->create_user(); $teacher2 = $this->getDataGenerator()->create_user(); $this->getDataGenerator()->enrol_user($teacher1->id, $course1->id, 'editingteacher'); $this->getDataGenerator()->enrol_user($teacher2->id, $course1->id, 'editingteacher'); // The only relevant fields are modelid, contextid and sampleid. I'm cheating and setting // contextid as the course context so teachers can access these predictions. $pred = new \stdClass(); $pred->modelid = $model->get_id(); $pred->contextid = $context->id; $pred->sampleid = $course1->id; $pred->rangeindex = 1; $pred->prediction = 1; $pred->predictionscore = 1; $pred->calculations = json_encode(array('test_indicator_max' => 1)); $pred->timecreated = time(); $DB->insert_record('analytics_predictions', $pred); $pred->sampleid = $course2->id; $DB->insert_record('analytics_predictions', $pred); $this->assertEquals(0, $DB->count_records('analytics_prediction_actions')); // Teacher 2 flags a prediction (it doesn't matter which one) as fixed. $this->setUser($teacher2); list($ignored, $predictions) = $model->get_predictions($context, true); $prediction = reset($predictions); \report_insights\external::action_executed(\core_analytics\prediction::ACTION_FIXED, [$prediction->get_prediction_data()->id]); $recordset = $model->get_prediction_actions($context); $this->assertCount(1, $recordset); $recordset->close(); $this->assertEquals(1, $DB->count_records('analytics_prediction_actions')); $action = $DB->get_record('analytics_prediction_actions', array('userid' => $teacher2->id)); $this->assertEquals(\core_analytics\prediction::ACTION_FIXED, $action->actionname); \report_insights\external::action_executed(\core_analytics\prediction::ACTION_INCORRECTLY_FLAGGED, [$prediction->get_prediction_data()->id]); $recordset = $model->get_prediction_actions($context); $this->assertCount(2, $recordset); $recordset->close(); $this->assertEquals(2, $DB->count_records('analytics_prediction_actions')); } } PK{\Цinsights/done.phpnu[. /** * Forwards the user to the action they selected. * * @package report_insights * @copyright 2019 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../config.php'); require_login(); $actionvisiblename = required_param('actionvisiblename', PARAM_NOTAGS); $PAGE->set_pagelayout('popup'); $PAGE->set_context(\context_system::instance()); if (!\core_analytics\manager::is_analytics_enabled()) { $renderer = $PAGE->get_renderer('report_insights'); echo $renderer->render_analytics_disabled(); exit(0); } $PAGE->set_title(get_string('insights', 'report_insights')); $PAGE->set_url(new \moodle_url('/report/insights/done.php')); echo $OUTPUT->header(); $notification = new \core\output\notification(get_string('actionsaved', 'report_insights', $actionvisiblename), \core\output\notification::NOTIFY_SUCCESS); $notification->set_show_closebutton(false); echo $OUTPUT->render($notification); echo $OUTPUT->footer(); PK{\^)OOinsights/settings.phpnu[. /** * Links and settings * * Contains settings used by insights report. * * @package report_insights * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die; if (\core_analytics\manager::is_analytics_enabled()) { // Just a link to course report. $ADMIN->add('reports', new admin_externalpage('reportinsights', get_string('insights', 'report_insights'), $CFG->wwwroot . "/report/insights/insights.php?contextid=" . SYSCONTEXTID, 'moodle/analytics:listinsights')); // No report settings. $settings = null; } PK{\p/?eventlist/styles.cssnu[.report-eventlist-name { color: #888; font-size: 0.75em; } .report-eventlist-datatable-table > div > table { width: 100%; } #page-admin-report-eventlist-index dt { float: left; text-align: right; width: 20em; } #page-admin-report-eventlist-index dd { display: block; text-align: left; margin-left: 21em; } #page-admin-report-eventlist-index dd + dd { clear: left; } @media (max-width: 767px) { #page-admin-report-eventlist-index dt { width: 100%; text-align: left; } #page-admin-report-eventlist-index dd { margin-left: 0; } #page-admin-report-eventlist-index dd + dd { margin-left: 0; } } PK{\g|q &eventlist/lang/en/report_eventlist.phpnu[. /** * Strings for component 'report_event', language 'en', branch 'MOODLE_27_STABLE' * * @package report_eventlist * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['abstractclass'] = 'Abstract class'; $string['action'] = 'Action'; $string['affectedtable'] = 'Affected table'; $string['all'] = 'All'; $string['clear'] = 'Clear'; $string['component'] = 'Component'; $string['core'] = 'Core'; $string['coresubsystem'] = 'Subsystem ({$a})'; $string['create'] = 'create'; $string['crud'] = 'Database query type'; $string['delete'] = 'delete'; $string['details'] = 'Details'; $string['dname'] = 'Name'; $string['edulevel'] = 'Education level'; $string['errorinvalidevent'] = 'The event provided is not a valid event.'; $string['errorinvaliddirectory'] = 'The event directory does not exist or is invalid.'; $string['eventcode'] = 'Event Code'; $string['eventexplanation'] = 'Explanation of the event'; $string['eventname'] = 'Event name'; $string['filter'] = 'Filter'; $string['legacyevent'] = 'Legacy event'; $string['name'] = 'Name'; $string['objecttable'] = 'Object table'; $string['other'] = 'Other'; $string['otherinformation'] = 'Other information:'; $string['othereventparameters'] = 'Other event parameters'; $string['parentevent'] = 'Parent Event'; $string['participating'] = 'Participating'; $string['pluginname'] = 'Events list'; $string['read'] = 'read'; $string['relatedobservers'] = 'Plugins observing this event'; $string['since'] = 'Since'; $string['teaching'] = 'Teaching'; $string['typedeclaration'] = 'Other event parameters'; $string['update'] = 'update'; $string['yes'] = 'yes'; $string['privacy:metadata'] = 'The Events list plugin does not store any personal data.'; PK{\̇eventlist/version.phpnu[. /** * Version details. * * @package report_eventlist * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'report_eventlist'; // Full name of the plugin (used for diagnostics). PK{\Ӕ]wweventlist/classes/renderer.phpnu[. /** * Event report renderer. * * @package report_eventlist * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /** * Renderer for event report. * * @package report_eventlist * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_eventlist_renderer extends plugin_renderer_base { /** * Renders the event list page with filter form and datatable. * * @param eventfilter_form $form Event filter form. * @param array $tabledata An array of event data to be used by the datatable. * @return string HTML to be displayed. */ public function render_event_list($form, $tabledata) { $title = get_string('pluginname', 'report_eventlist'); // Header. $html = $this->output->header(); $html .= $this->output->heading($title); // Form. ob_start(); $form->display(); $html .= ob_get_contents(); ob_end_clean(); $this->page->requires->yui_module('moodle-report_eventlist-eventfilter', 'Y.M.report_eventlist.EventFilter.init', array(array('tabledata' => $tabledata))); $this->page->requires->strings_for_js(array( 'eventname', 'component', 'action', 'crud', 'edulevel', 'affectedtable', 'dname', 'legacyevent', 'since' ), 'report_eventlist'); $html .= html_writer::start_div('report-eventlist-data-table', array('id' => 'report-eventlist-table')); $html .= html_writer::end_div(); $html .= $this->output->footer(); return $html; } /** * Event detail renderer. * * @param array $observerlist A list of observers that consume this event. * @param array $eventinformation A list of information about the event. * @return string HTML to be displayed. */ public function render_event_detail($observerlist, $eventinformation) { $titlehtml = $this->output->header(); $titlehtml .= $this->output->heading($eventinformation['title']); $html = html_writer::start_tag('dl', array('class' => 'list')); $explanation = nl2br($eventinformation['explanation']); $html .= html_writer::tag('dt', get_string('eventexplanation', 'report_eventlist')); $html .= html_writer::tag('dd', $explanation); if (isset($eventinformation['crud'])) { $html .= html_writer::tag('dt', get_string('crud', 'report_eventlist')); $html .= html_writer::tag('dd', $eventinformation['crud']); } if (isset($eventinformation['edulevel'])) { $html .= html_writer::tag('dt', get_string('edulevel', 'report_eventlist')); $html .= html_writer::tag('dd', $eventinformation['edulevel']); } if (isset($eventinformation['objecttable'])) { $html .= html_writer::tag('dt', get_string('affectedtable', 'report_eventlist')); $html .= html_writer::tag('dd', $eventinformation['objecttable']); } if (isset($eventinformation['legacyevent'])) { $html .= html_writer::tag('dt', get_string('legacyevent', 'report_eventlist')); $html .= html_writer::tag('dd', $eventinformation['legacyevent']); } if (isset($eventinformation['parentclass'])) { $url = new moodle_url('eventdetail.php', array('eventname' => $eventinformation['parentclass'])); $html .= html_writer::tag('dt', get_string('parentevent', 'report_eventlist')); $html .= html_writer::tag('dd', html_writer::link($url, $eventinformation['parentclass'])); } if (isset($eventinformation['abstract'])) { $html .= html_writer::tag('dt', get_string('abstractclass', 'report_eventlist')); $html .= html_writer::tag('dd', get_string('yes', 'report_eventlist')); } if (isset($eventinformation['typeparameter'])) { $html .= html_writer::tag('dt', get_string('typedeclaration', 'report_eventlist')); foreach ($eventinformation['typeparameter'] as $typeparameter) { $html .= html_writer::tag('dd', $typeparameter); } } if (isset($eventinformation['otherparameter'])) { $html .= html_writer::tag('dt', get_string('othereventparameters', 'report_eventlist')); foreach ($eventinformation['otherparameter'] as $otherparameter) { $html .= html_writer::tag('dd', $otherparameter); } } // List observers consuming this event if there are any. if (!empty($observerlist)) { $html .= html_writer::tag('dt', get_string('relatedobservers', 'report_eventlist')); foreach ($observerlist as $observer) { if ($observer->plugin == 'core') { $html .= html_writer::tag('dd', $observer->plugin); } else { $manager = get_string_manager(); $pluginstring = $observer->plugintype . '_' . $observer->plugin; if ($manager->string_exists('pluginname', $pluginstring)) { if (!empty($observer->parentplugin)) { $string = get_string('pluginname', $pluginstring) . ' (' . $observer->parentplugin . ' ' . $pluginstring . ')'; } else { $string = get_string('pluginname', $pluginstring) . ' (' . $pluginstring . ')'; } } else { $string = $observer->plugintype . ' ' . $observer->plugin; } $html .= html_writer::tag('dd', $string); } } } $html .= html_writer::end_div(); $html .= html_writer::end_tag('dl'); $pagecontent = new html_table(); $pagecontent->data = array(array($html)); $pagehtml = $titlehtml . html_writer::table($pagecontent); $pagehtml .= $this->output->footer(); return $pagehtml; } } PK{\t*!eventlist/classes/filter_form.phpnu[. defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/formslib.php'); /** * Event list filter form. * * @package report_eventlist * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_eventlist_filter_form extends moodleform { /** * Form definition method. */ public function definition() { $mform = $this->_form; $mform->disable_form_change_checker(); $componentarray = $this->_customdata['components']; $edulevelarray = $this->_customdata['edulevel']; $crudarray = $this->_customdata['crud']; $mform->addElement('header', 'displayinfo', get_string('filter', 'report_eventlist')); $mform->addElement('text', 'eventname', get_string('name', 'report_eventlist')); $mform->setType('eventname', PARAM_RAW); $mform->addElement('selectgroups', 'eventcomponent', get_string('component', 'report_eventlist'), self::group_components_by_type($componentarray)); $mform->addElement('select', 'eventedulevel', get_string('edulevel', 'report_eventlist'), $edulevelarray); $mform->addElement('select', 'eventcrud', get_string('crud', 'report_eventlist'), $crudarray); $buttonarray = array(); $buttonarray[] = $mform->createElement('button', 'filterbutton', get_string('filter', 'report_eventlist')); $buttonarray[] = $mform->createElement('button', 'clearbutton', get_string('clear', 'report_eventlist')); $mform->addGroup($buttonarray, 'filterbuttons', '', array(' '), false); } /** * Group list of component names by type for use in grouped select element * * @param string[] $components * @return array[] Component type => [...Components] */ private static function group_components_by_type(array $components): array { $pluginmanager = core_plugin_manager::instance(); $result = []; foreach ($components as $component) { // Core sub-systems are grouped together and are denoted by a distinct lang string. if (strpos($component, 'core') === 0) { $componenttype = get_string('core', 'report_eventlist'); $componentname = get_string('coresubsystem', 'report_eventlist', $component); } else { [$type] = core_component::normalize_component($component); $componenttype = $pluginmanager->plugintype_name_plural($type); $componentname = $pluginmanager->plugin_name($component); } $result[$componenttype][$component] = $componentname; } // Sort returned components according to their type, followed by name. core_collator::ksort($result); array_walk($result, function(array &$componenttype) { core_collator::asort($componenttype); }); // Prepend "All" option. array_unshift($result, [0 => get_string('all', 'report_eventlist')]); return $result; } } PK{\ˑ:&eventlist/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for report_eventlist. * * @package report_eventlist * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace report_eventlist\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for report_eventlist implementing null_provider. * * @copyright 2018 Zig Tan * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } }PK{\ 8!!$eventlist/classes/list_generator.phpnu[. /** * Class for returning system event information. * * @package report_eventlist * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_eventlist_list_generator { /** * Convenience method. Returns all of the core events either with or without details. * * @param bool $detail True will return details, but no abstract classes, False will return all events, but no details. * @return array All events. */ public static function get_all_events_list($detail = true) { global $CFG; // Disable developer debugging as deprecated events will fire warnings. // Setup backup variables to restore the following settings back to what they were when we are finished. $debuglevel = $CFG->debug; $debugdisplay = $CFG->debugdisplay; $debugdeveloper = $CFG->debugdeveloper; $CFG->debug = 0; $CFG->debugdisplay = false; $CFG->debugdeveloper = false; // List of exceptional events that will cause problems if displayed. $eventsignore = [ \core\event\unknown_logged::class, ]; $eventinformation = []; $events = core_component::get_component_classes_in_namespace(null, 'event'); foreach (array_keys($events) as $event) { // We need to filter all classes that extend event base, or the base class itself. if (is_a($event, \core\event\base::class, true) && !in_array($event, $eventsignore)) { if ($detail) { $reflectionclass = new ReflectionClass($event); if (!$reflectionclass->isAbstract()) { $eventinformation = self::format_data($eventinformation, "\\{$event}"); } } else { $parts = explode('\\', $event); $eventinformation["\\{$event}"] = array_shift($parts); } } } // Now enable developer debugging as event information has been retrieved. $CFG->debug = $debuglevel; $CFG->debugdisplay = $debugdisplay; $CFG->debugdeveloper = $debugdeveloper; return $eventinformation; } /** * @deprecated since 4.0 use {@see get_all_events_list} instead */ #[\core\attribute\deprecated('::get_all_events_list', since: '4.0', mdl: 'MDL-72498', final: true)] public static function get_core_events_list() { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } /** * Returns the appropriate string for the CRUD character. * * @param string $crudcharacter The CRUD character. * @return string get_string for the specific CRUD character. */ public static function get_crud_string($crudcharacter) { switch ($crudcharacter) { case 'c': return get_string('create', 'report_eventlist'); break; case 'u': return get_string('update', 'report_eventlist'); break; case 'd': return get_string('delete', 'report_eventlist'); break; case 'r': default: return get_string('read', 'report_eventlist'); break; } } /** * Returns the appropriate string for the event education level. * * @param int $edulevel Takes either the edulevel constant or string. * @return string get_string for the specific education level. */ public static function get_edulevel_string($edulevel) { switch ($edulevel) { case \core\event\base::LEVEL_PARTICIPATING: return get_string('participating', 'report_eventlist'); break; case \core\event\base::LEVEL_TEACHING: return get_string('teaching', 'report_eventlist'); break; case \core\event\base::LEVEL_OTHER: default: return get_string('other', 'report_eventlist'); break; } } /** * @deprecated since 4.0 use {@see get_all_events_list} instead */ #[\core\attribute\deprecated('::get_all_events_list', since: '4.0', mdl: 'MDL-72498', final: true)] public static function get_non_core_event_list() { \core\deprecation::emit_deprecation([self::class, __FUNCTION__]); } /** * Get the full list of observers for the system. * * @return array An array of observers in the system. */ public static function get_observer_list() { $events = \core\event\manager::get_all_observers(); foreach ($events as $key => $observers) { foreach ($observers as $observerskey => $observer) { $events[$key][$observerskey]->parentplugin = \core_plugin_manager::instance()->get_parent_of_subplugin($observer->plugintype); } } return $events; } /** * Returns the event data list section with url links and other formatting. * * @param array $eventdata The event data list section. * @param string $eventfullpath Full path to the events for this plugin / subplugin. * @return array The event data list section with additional formatting. */ private static function format_data($eventdata, $eventfullpath) { // Get general event information. $eventdata[$eventfullpath] = $eventfullpath::get_static_info(); // Create a link for further event detail. $url = new \moodle_url('eventdetail.php', array('eventname' => $eventfullpath)); $link = \html_writer::link($url, $eventfullpath::get_name_with_info()); $eventdata[$eventfullpath]['fulleventname'] = \html_writer::span($link); $eventdata[$eventfullpath]['fulleventname'] .= \html_writer::empty_tag('br'); $eventdata[$eventfullpath]['fulleventname'] .= \html_writer::span($eventdata[$eventfullpath]['eventname'], 'report-eventlist-name'); $eventdata[$eventfullpath]['crud'] = self::get_crud_string($eventdata[$eventfullpath]['crud']); $eventdata[$eventfullpath]['edulevel'] = self::get_edulevel_string($eventdata[$eventfullpath]['edulevel']); // Mess around getting since information. $ref = new \ReflectionClass($eventdata[$eventfullpath]['eventname']); $eventdocbloc = $ref->getDocComment(); $sincepattern = "/since\s*Moodle\s([0-9]+.[0-9]+)/i"; preg_match($sincepattern, $eventdocbloc, $result); if (isset($result[1])) { $eventdata[$eventfullpath]['since'] = $result[1]; } else { $eventdata[$eventfullpath]['since'] = null; } // Human readable plugin information to go with the component. $pluginstring = explode('\\', $eventfullpath); if ($pluginstring[1] !== 'core') { $component = $eventdata[$eventfullpath]['component']; $manager = get_string_manager(); if ($manager->string_exists('pluginname', $pluginstring[1])) { $eventdata[$eventfullpath]['component'] = \html_writer::span(get_string('pluginname', $pluginstring[1])); } } // Raw event data to be used to sort the "Event name" column. $eventdata[$eventfullpath]['raweventname'] = $eventfullpath::get_name_with_info() . ' ' . $eventdata[$eventfullpath]['eventname']; // Unset information that is not currently required. unset($eventdata[$eventfullpath]['action']); unset($eventdata[$eventfullpath]['target']); return $eventdata; } } PK{\\`eventlist/upgrade.txtnu[=== 4.5 Onwards === This file has been replaced by UPGRADING.md. See MDL-81125 for further information. === This file describes API changes in /report/eventlist/*, information provided here is intended especially for developers. === 4.0 === * The following methods have been deprecated in favour of a single `get_all_events_list` method: - report_eventlist_list_generator::get_core_events_list - report_eventlist_list_generator::get_non_core_event_list PK{\(eventlist/yui/src/eventfilter/build.jsonnu[{ "name": "moodle-report_eventlist-eventfilter", "builds": { "moodle-report_eventlist-eventfilter": { "jsfiles": [ "eventfilter.js" ] } } }PK{\ҿ3eventlist/yui/src/eventfilter/meta/eventfilter.jsonnu[{ "moodle-report_eventlist-eventfilter": { "requires": [ "base", "event", "node", "node-event-delegate", "datatable", "autocomplete", "autocomplete-filters" ] } }PK{\p{$$/eventlist/yui/src/eventfilter/js/eventfilter.jsnu[/** * A tool for displaying and filtering system events. * * @module moodle-report_eventlist-eventfilter */ /** * A tool for displaying and filtering system events. * * @class M.report_eventlist.EventFilter * @extends Base * @constructor */ function EventFilter() { EventFilter.superclass.constructor.apply(this, arguments); } var SELECTORS = { EVENTNAME: '#id_eventname', EVENTCOMPONENT: '#id_eventcomponent', EVENTEDULEVEL: '#id_eventedulevel', EVENTCRUD: '#id_eventcrud', FILTERBUTTON: '#id_filterbutton', CLEARBUTTON: '#id_clearbutton' }; Y.extend(EventFilter, Y.Base, { /** * A reference to the datatable. * * @property _table * @type DataTable * @private */ _table: null, /** * A reference to the eventname text element. * * @property _eventName * @type node * @private */ _eventName: null, /** * A reference to the component select box element. * * @property _component * @type node * @private */ _component: null, /** * A reference to the education level select box element. * * @property _eduLevel * @type node * @private */ _eduLevel: null, /** * A reference to the CRUD select box element. * * @property _crud * @type node * @private */ _crud: null, /** * Initializer. * Basic setup and delegations. * * @method initializer */ initializer: function() { var filterButton = Y.one(SELECTORS.FILTERBUTTON), clearButton = Y.one(SELECTORS.CLEARBUTTON); this._createTable(this.get('tabledata')); this._eventName = Y.one(SELECTORS.EVENTNAME); this._component = Y.one(SELECTORS.EVENTCOMPONENT); this._eduLevel = Y.one(SELECTORS.EVENTEDULEVEL); this._crud = Y.one(SELECTORS.EVENTCRUD); this._eventName.on('valuechange', this._totalFilter, this); filterButton.on('click', this._totalFilter, this); clearButton.on('click', this._clearFilter, this); }, /** * Create the table for displaying all of the event information. * * @param {array} tableData Event data for populating the table. * @method _createTable * @private * @chainable */ _createTable: function(tableData) { var table = new Y.DataTable({ columns: [ { key: "fulleventname", label: M.util.get_string('eventname', 'report_eventlist'), allowHTML: true, sortable: true, /** * Custom sort of the fulleventname column. * This will sort via the event name rather than the event path. * * @param {object} eventDataListA Event data record module A. * @param {object} eventDataListB Event data record module B. * @param {boolean} desc True sorts list in descending order and false sorts in Ascending order. * @return {number} order for which the column should be sorted. * @method sortFn */ sortFn: function(eventDataListA, eventDataListB, desc) { var rawEventDataA = eventDataListA.getAttrs().raweventname, rawEventDataB = eventDataListB.getAttrs().raweventname, order = (rawEventDataA > rawEventDataB ? 1 : -1); return desc ? -order : order; }, title: M.util.get_string('eventname', 'report_eventlist') }, { key: "component", label: M.util.get_string('component', 'report_eventlist'), allowHTML: true, sortable: true, title: M.util.get_string('component', 'report_eventlist') }, { key: "edulevel", label: M.util.get_string('edulevel', 'report_eventlist'), sortable: true, title: M.util.get_string('edulevel', 'report_eventlist') }, { key: "crud", label: M.util.get_string('crud', 'report_eventlist'), sortable: true, title: M.util.get_string('crud', 'report_eventlist') }, { key: "objecttable", label: M.util.get_string('affectedtable', 'report_eventlist'), sortable: true, title: M.util.get_string('affectedtable', 'report_eventlist') }, { key: "since", label: M.util.get_string('since', 'report_eventlist'), sortable: true, title: M.util.get_string('since', 'report_eventlist') }, { key: "legacyevent", label: M.util.get_string('legacyevent', 'report_eventlist'), sortable: true, title: M.util.get_string('legacyevent', 'report_eventlist') } ], data: tableData, strings: { sortBy: '{title}', reverseSortBy: '{title}' } }); // Display the table. table.render("#report-eventlist-table"); table.get('boundingBox').addClass('report-eventlist-datatable-table'); this._table = table; return this; }, /** * Filters the entries being displayed in the table. * * @method totalFilter * @private */ _totalFilter: function() { // Get all of the details of the filter elements var eventNameFilter = this._eventName.get('value').toLowerCase(), // Component selected value. componentValue = this._component.get('value'), // Education level selected text. eduLevelFilter = this._eduLevel.get('options').item(this._eduLevel.get('selectedIndex')).get('text').toLowerCase(), // Education level selected value. eduLevelValue = this._eduLevel.get('value'), // CRUD selected text. crudFilter = this._crud.get('options').item(this._crud.get('selectedIndex')).get('text').toLowerCase(), // CRUD selected value. crudValue = this._crud.get('value'), i, filtered = []; // Loop through the rows and put the ones we want into the filter. for (i = 0; i < this.get('tabledata').length; i++) { // These variables will either be false or true depending on the statement outcome. var fullEventText = Y.Node.create(this.get('tabledata')[i].fulleventname).get('text'), eventNameValue = fullEventText.toLowerCase().indexOf(eventNameFilter) >= 0, componentFilterValue = this.get('tabledata')[i].eventname.indexOf('\\' + componentValue + '\\event\\') >= 0, eduLevelFilterValue = this.get('tabledata')[i].edulevel.toLowerCase().indexOf(eduLevelFilter) >= 0, crudFilterValue = this.get('tabledata')[i].crud.toLowerCase().indexOf(crudFilter) >= 0; // If the name field is empty then add to the filter. if (eventNameFilter === '') { eventNameValue = true; } // If the component is set to 'all' then add to the filter. if (componentValue === '0') { componentFilterValue = true; } // If the education level is set to 'all' then add to the filter. if (eduLevelValue === '0') { eduLevelFilterValue = true; } // If the CRUD field is set to 'all' then add to the filter. if (crudValue === '0') { crudFilterValue = true; } // If any of the Values here is false then don't add to the filter (all must be true). if (eventNameValue && componentFilterValue && eduLevelFilterValue && crudFilterValue) { filtered.push(this.get('tabledata')[i]); } } // Display the table again with the new data. this._table.set('data', filtered); }, /** * Clears the filtered table data and changes the filter form to default. * * @method _clearFilter * @private */ _clearFilter: function() { // Reset filter form elements this._eventName.set('value', ''); this._component.set('value', '0'); this._eduLevel.set('value', '0'); this._crud.set('value', '0'); // Reset the table data back to the original. this._table.set('data', this.get('tabledata')); } }, { NAME: 'eventFilter', ATTRS: { /** * Data for the table. * * @attribute tabledata. * @type Array * @writeOnce */ tabledata: { value: null } } }); Y.namespace('M.report_eventlist.EventFilter').init = function(config) { return new EventFilter(config); }; PK{\Q;ʌ beventlist/yui/build/moodle-report_eventlist-eventfilter/moodle-report_eventlist-eventfilter-min.jsnu[YUI.add("moodle-report_eventlist-eventfilter",function(d,e){function t(){t.superclass.constructor.apply(this,arguments)}var l="#id_eventname",n="#id_eventcomponent",i="#id_eventedulevel",a="#id_eventcrud",r="#id_filterbutton",s="#id_clearbutton";d.extend(t,d.Base,{_table:null,_eventName:null,_component:null,_eduLevel:null,_crud:null,initializer:function(){var e=d.one(r),t=d.one(s);this._createTable(this.get("tabledata")),this._eventName=d.one(l),this._component=d.one(n),this._eduLevel=d.one(i),this._crud=d.one(a),this._eventName.on("valuechange",this._totalFilter,this),e.on("click",this._totalFilter,this),t.on("click",this._clearFilter,this)},_createTable:function(e){e=new d.DataTable({columns:[{key:"fulleventname",label:M.util.get_string("eventname","report_eventlist"),allowHTML:!0,sortable:!0,sortFn:function(e,t,l){e=e.getAttrs().raweventname,t=t.getAttrs().raweventname rawEventDataB ? 1 : -1); return desc ? -order : order; }, title: M.util.get_string('eventname', 'report_eventlist') }, { key: "component", label: M.util.get_string('component', 'report_eventlist'), allowHTML: true, sortable: true, title: M.util.get_string('component', 'report_eventlist') }, { key: "edulevel", label: M.util.get_string('edulevel', 'report_eventlist'), sortable: true, title: M.util.get_string('edulevel', 'report_eventlist') }, { key: "crud", label: M.util.get_string('crud', 'report_eventlist'), sortable: true, title: M.util.get_string('crud', 'report_eventlist') }, { key: "objecttable", label: M.util.get_string('affectedtable', 'report_eventlist'), sortable: true, title: M.util.get_string('affectedtable', 'report_eventlist') }, { key: "since", label: M.util.get_string('since', 'report_eventlist'), sortable: true, title: M.util.get_string('since', 'report_eventlist') }, { key: "legacyevent", label: M.util.get_string('legacyevent', 'report_eventlist'), sortable: true, title: M.util.get_string('legacyevent', 'report_eventlist') } ], data: tableData, strings: { sortBy: '{title}', reverseSortBy: '{title}' } }); // Display the table. table.render("#report-eventlist-table"); table.get('boundingBox').addClass('report-eventlist-datatable-table'); this._table = table; return this; }, /** * Filters the entries being displayed in the table. * * @method totalFilter * @private */ _totalFilter: function() { // Get all of the details of the filter elements var eventNameFilter = this._eventName.get('value').toLowerCase(), // Component selected value. componentValue = this._component.get('value'), // Education level selected text. eduLevelFilter = this._eduLevel.get('options').item(this._eduLevel.get('selectedIndex')).get('text').toLowerCase(), // Education level selected value. eduLevelValue = this._eduLevel.get('value'), // CRUD selected text. crudFilter = this._crud.get('options').item(this._crud.get('selectedIndex')).get('text').toLowerCase(), // CRUD selected value. crudValue = this._crud.get('value'), i, filtered = []; // Loop through the rows and put the ones we want into the filter. for (i = 0; i < this.get('tabledata').length; i++) { // These variables will either be false or true depending on the statement outcome. var fullEventText = Y.Node.create(this.get('tabledata')[i].fulleventname).get('text'), eventNameValue = fullEventText.toLowerCase().indexOf(eventNameFilter) >= 0, componentFilterValue = this.get('tabledata')[i].eventname.indexOf('\\' + componentValue + '\\event\\') >= 0, eduLevelFilterValue = this.get('tabledata')[i].edulevel.toLowerCase().indexOf(eduLevelFilter) >= 0, crudFilterValue = this.get('tabledata')[i].crud.toLowerCase().indexOf(crudFilter) >= 0; // If the name field is empty then add to the filter. if (eventNameFilter === '') { eventNameValue = true; } // If the component is set to 'all' then add to the filter. if (componentValue === '0') { componentFilterValue = true; } // If the education level is set to 'all' then add to the filter. if (eduLevelValue === '0') { eduLevelFilterValue = true; } // If the CRUD field is set to 'all' then add to the filter. if (crudValue === '0') { crudFilterValue = true; } // If any of the Values here is false then don't add to the filter (all must be true). if (eventNameValue && componentFilterValue && eduLevelFilterValue && crudFilterValue) { filtered.push(this.get('tabledata')[i]); } } // Display the table again with the new data. this._table.set('data', filtered); }, /** * Clears the filtered table data and changes the filter form to default. * * @method _clearFilter * @private */ _clearFilter: function() { // Reset filter form elements this._eventName.set('value', ''); this._component.set('value', '0'); this._eduLevel.set('value', '0'); this._crud.set('value', '0'); // Reset the table data back to the original. this._table.set('data', this.get('tabledata')); } }, { NAME: 'eventFilter', ATTRS: { /** * Data for the table. * * @attribute tabledata. * @type Array * @writeOnce */ tabledata: { value: null } } }); Y.namespace('M.report_eventlist.EventFilter').init = function(config) { return new EventFilter(config); }; }, '@VERSION@', { "requires": [ "base", "event", "node", "node-event-delegate", "datatable", "autocomplete", "autocomplete-filters" ] }); PK{\V,%%deventlist/yui/build/moodle-report_eventlist-eventfilter/moodle-report_eventlist-eventfilter-debug.jsnu[YUI.add('moodle-report_eventlist-eventfilter', function (Y, NAME) { /** * A tool for displaying and filtering system events. * * @module moodle-report_eventlist-eventfilter */ /** * A tool for displaying and filtering system events. * * @class M.report_eventlist.EventFilter * @extends Base * @constructor */ function EventFilter() { EventFilter.superclass.constructor.apply(this, arguments); } var SELECTORS = { EVENTNAME: '#id_eventname', EVENTCOMPONENT: '#id_eventcomponent', EVENTEDULEVEL: '#id_eventedulevel', EVENTCRUD: '#id_eventcrud', FILTERBUTTON: '#id_filterbutton', CLEARBUTTON: '#id_clearbutton' }; Y.extend(EventFilter, Y.Base, { /** * A reference to the datatable. * * @property _table * @type DataTable * @private */ _table: null, /** * A reference to the eventname text element. * * @property _eventName * @type node * @private */ _eventName: null, /** * A reference to the component select box element. * * @property _component * @type node * @private */ _component: null, /** * A reference to the education level select box element. * * @property _eduLevel * @type node * @private */ _eduLevel: null, /** * A reference to the CRUD select box element. * * @property _crud * @type node * @private */ _crud: null, /** * Initializer. * Basic setup and delegations. * * @method initializer */ initializer: function() { var filterButton = Y.one(SELECTORS.FILTERBUTTON), clearButton = Y.one(SELECTORS.CLEARBUTTON); this._createTable(this.get('tabledata')); this._eventName = Y.one(SELECTORS.EVENTNAME); this._component = Y.one(SELECTORS.EVENTCOMPONENT); this._eduLevel = Y.one(SELECTORS.EVENTEDULEVEL); this._crud = Y.one(SELECTORS.EVENTCRUD); this._eventName.on('valuechange', this._totalFilter, this); filterButton.on('click', this._totalFilter, this); clearButton.on('click', this._clearFilter, this); }, /** * Create the table for displaying all of the event information. * * @param {array} tableData Event data for populating the table. * @method _createTable * @private * @chainable */ _createTable: function(tableData) { var table = new Y.DataTable({ columns: [ { key: "fulleventname", label: M.util.get_string('eventname', 'report_eventlist'), allowHTML: true, sortable: true, /** * Custom sort of the fulleventname column. * This will sort via the event name rather than the event path. * * @param {object} eventDataListA Event data record module A. * @param {object} eventDataListB Event data record module B. * @param {boolean} desc True sorts list in descending order and false sorts in Ascending order. * @return {number} order for which the column should be sorted. * @method sortFn */ sortFn: function(eventDataListA, eventDataListB, desc) { var rawEventDataA = eventDataListA.getAttrs().raweventname, rawEventDataB = eventDataListB.getAttrs().raweventname, order = (rawEventDataA > rawEventDataB ? 1 : -1); return desc ? -order : order; }, title: M.util.get_string('eventname', 'report_eventlist') }, { key: "component", label: M.util.get_string('component', 'report_eventlist'), allowHTML: true, sortable: true, title: M.util.get_string('component', 'report_eventlist') }, { key: "edulevel", label: M.util.get_string('edulevel', 'report_eventlist'), sortable: true, title: M.util.get_string('edulevel', 'report_eventlist') }, { key: "crud", label: M.util.get_string('crud', 'report_eventlist'), sortable: true, title: M.util.get_string('crud', 'report_eventlist') }, { key: "objecttable", label: M.util.get_string('affectedtable', 'report_eventlist'), sortable: true, title: M.util.get_string('affectedtable', 'report_eventlist') }, { key: "since", label: M.util.get_string('since', 'report_eventlist'), sortable: true, title: M.util.get_string('since', 'report_eventlist') }, { key: "legacyevent", label: M.util.get_string('legacyevent', 'report_eventlist'), sortable: true, title: M.util.get_string('legacyevent', 'report_eventlist') } ], data: tableData, strings: { sortBy: '{title}', reverseSortBy: '{title}' } }); // Display the table. table.render("#report-eventlist-table"); table.get('boundingBox').addClass('report-eventlist-datatable-table'); this._table = table; return this; }, /** * Filters the entries being displayed in the table. * * @method totalFilter * @private */ _totalFilter: function() { // Get all of the details of the filter elements var eventNameFilter = this._eventName.get('value').toLowerCase(), // Component selected value. componentValue = this._component.get('value'), // Education level selected text. eduLevelFilter = this._eduLevel.get('options').item(this._eduLevel.get('selectedIndex')).get('text').toLowerCase(), // Education level selected value. eduLevelValue = this._eduLevel.get('value'), // CRUD selected text. crudFilter = this._crud.get('options').item(this._crud.get('selectedIndex')).get('text').toLowerCase(), // CRUD selected value. crudValue = this._crud.get('value'), i, filtered = []; // Loop through the rows and put the ones we want into the filter. for (i = 0; i < this.get('tabledata').length; i++) { // These variables will either be false or true depending on the statement outcome. var fullEventText = Y.Node.create(this.get('tabledata')[i].fulleventname).get('text'), eventNameValue = fullEventText.toLowerCase().indexOf(eventNameFilter) >= 0, componentFilterValue = this.get('tabledata')[i].eventname.indexOf('\\' + componentValue + '\\event\\') >= 0, eduLevelFilterValue = this.get('tabledata')[i].edulevel.toLowerCase().indexOf(eduLevelFilter) >= 0, crudFilterValue = this.get('tabledata')[i].crud.toLowerCase().indexOf(crudFilter) >= 0; // If the name field is empty then add to the filter. if (eventNameFilter === '') { eventNameValue = true; } // If the component is set to 'all' then add to the filter. if (componentValue === '0') { componentFilterValue = true; } // If the education level is set to 'all' then add to the filter. if (eduLevelValue === '0') { eduLevelFilterValue = true; } // If the CRUD field is set to 'all' then add to the filter. if (crudValue === '0') { crudFilterValue = true; } // If any of the Values here is false then don't add to the filter (all must be true). if (eventNameValue && componentFilterValue && eduLevelFilterValue && crudFilterValue) { filtered.push(this.get('tabledata')[i]); } } // Display the table again with the new data. this._table.set('data', filtered); }, /** * Clears the filtered table data and changes the filter form to default. * * @method _clearFilter * @private */ _clearFilter: function() { // Reset filter form elements this._eventName.set('value', ''); this._component.set('value', '0'); this._eduLevel.set('value', '0'); this._crud.set('value', '0'); // Reset the table data back to the original. this._table.set('data', this.get('tabledata')); } }, { NAME: 'eventFilter', ATTRS: { /** * Data for the table. * * @attribute tabledata. * @type Array * @writeOnce */ tabledata: { value: null } } }); Y.namespace('M.report_eventlist.EventFilter').init = function(config) { return new EventFilter(config); }; }, '@VERSION@', { "requires": [ "base", "event", "node", "node-event-delegate", "datatable", "autocomplete", "autocomplete-filters" ] }); PK{\ _~~eventlist/index.phpnu[. /** * Event documentation. * * @package report_eventlist * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../config.php'); require_once($CFG->libdir . '/adminlib.php'); admin_externalpage_setup('reporteventlists'); // Retrieve all events in a list. $completelist = report_eventlist_list_generator::get_all_events_list(); $tabledata = array(); $components = array(); $edulevel = array('0' => get_string('all', 'report_eventlist')); $crud = array('0' => get_string('all', 'report_eventlist')); foreach ($completelist as $value) { $components[] = explode('\\', $value['eventname'])[1]; $edulevel[] = $value['edulevel']; $crud[] = $value['crud']; $tabledata[] = (object)$value; } $components = array_unique($components); $edulevel = array_unique($edulevel); $crud = array_unique($crud); // Create the filter form for the table. $filtersection = new report_eventlist_filter_form(null, array('components' => $components, 'edulevel' => $edulevel, 'crud' => $crud)); // Output. $renderer = $PAGE->get_renderer('report_eventlist'); echo $renderer->render_event_list($filtersection, $tabledata); PK{\ kkeventlist/UPGRADING.mdnu[# report_eventlist Upgrade notes ## 4.5 ### Deprecated - The following deprecated methods in `report_eventlist_list_generator` have been removed: - `\report_eventlist_list_generator::get_core_events_list()` - `\report_eventlist_list_generator::get_non_core_event_list()` For more information see [MDL-72786](https://tracker.moodle.org/browse/MDL-72786) PK{\*. )eventlist/tests/behat/mainsection.featurenu[@report @report_eventlist Feature: Page contains a list of events In order to find information about events As a user I need to check the page contents @javascript Scenario: Event list page is viewable and filtering works Given I log in as "admin" And I navigate to "Reports > Events list" in site administration And I should see "Event name" And I set the field "eventname" to "phase" And I press "filterbutton" And I should see "Phase switched" And I should not see "Comment created" And I press "clearbutton" And I set the field "eventcomponent" to "URL" And I press "filterbutton" And I should see "Course module instance list viewed" And I should not see "User added to cohort" And I press "clearbutton" And I set the field "eventedulevel" to "Teaching" And I press "filterbutton" And I should see "Attempt deleted" And I should not see "Quiz attempt abandoned" And I press "clearbutton" And I set the field "eventcrud" to "delete" And I press "filterbutton" And I should see "Cohort deleted" And I should not see "Cohort updated" And I press "clearbutton" And I set the field "eventcomponent" to "Assignment" And I set the field "eventedulevel" to "Participating" And I press "filterbutton" And I should see "A submission has been submitted" And I should not see "An extension has been granted" And I press "clearbutton" And I set the field "eventedulevel" to "Other" And I set the field "eventcrud" to "read" And I press "filterbutton" And I should see "Notes viewed" And I should not see "Highscores viewed" And I press "clearbutton" And I set the field "eventname" to "viewed" And I set the field "eventcomponent" to "Forum" And I set the field "eventedulevel" to "Participating" And I set the field "eventcrud" to "read" And I press "filterbutton" Then I should see "User report viewed" And I should not see "Subscribers viewed" @javascript Scenario: Details of an event are viewable Given I log in as "admin" And I navigate to "Reports > Events list" in site administration And I should see "Event name" And I follow "Blog association created" And I should see "Blog association created" And I should see "blog_association" And I should see "create" And I should see "Log store manager (tool_log)" And I follow "\core\event\base" Then I should see "core: base" PK{\PIIeventlist/eventdetail.phpnu[. /** * Event developer detail. * * @package report_eventlist * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once(__DIR__ . '/../../config.php'); require_once($CFG->libdir . '/adminlib.php'); // Required parameters. $eventname = required_param('eventname', PARAM_RAW); admin_externalpage_setup('reporteventlists'); // Retrieve all events in a list. $completelist = report_eventlist_list_generator::get_all_events_list(false); // Check that $eventname is a valid event. if (!array_key_exists($eventname, $completelist)) { throw new \moodle_exception('errorinvalidevent', 'report_eventlist'); } // Break up the full event name to usable parts. $component = explode('\\', $eventname); $directory = core_component::get_component_directory($component[1]); // File and directory information. $directory = $directory . '/classes/event'; // Verify that the directory is valid. if (!is_dir($directory)) { throw new \moodle_exception('errorinvaliddirectory', 'report_eventlist'); } $filename = end($component); $eventfiles = $directory . '/' . $filename . '.php'; $title = $eventname::get_name_with_info(); // Define event information. $eventinformation = array('title' => $title); $eventcontents = file_get_contents($eventfiles); $eventinformation['filecontents'] = $eventcontents; $ref = new \ReflectionClass($eventname); $eventinformation['explanation'] = $eventname::get_explanation($eventname); // Get event information nicely if we can. if (!$ref->isAbstract()) { $eventinformation = array_merge($eventinformation, $eventname::get_static_info()); $eventinformation['crud'] = report_eventlist_list_generator::get_crud_string($eventinformation['crud']); $eventinformation['edulevel'] = report_eventlist_list_generator::get_edulevel_string($eventinformation['edulevel']); } else { $eventinformation['abstract'] = true; if ($eventname != '\core\event\base') { // No choice but to get information the hard way. // Strip out CRUD information. $crudpattern = "/(\['crud'\]\s=\s')(\w)/"; $result = array(); preg_match($crudpattern, $eventcontents, $result); if (!empty($result[2])) { $eventinformation['crud'] = report_eventlist_list_generator::get_crud_string($result[2]); } // Strip out edulevel information. $edulevelpattern = "/(\['edulevel'\]\s=\sself\:\:)(\w*)/"; $result = array(); preg_match($edulevelpattern, $eventcontents, $result); if (!empty($result[2])) { $educationlevel = constant('\core\event\base::' . $result[2]); $eventinformation['edulevel'] = report_eventlist_list_generator::get_edulevel_string($educationlevel); } // Retrieve object table information. $affectedtablepattern = "/(\['objecttable'\]\s=\s')(\w*)/"; $result = array(); preg_match($affectedtablepattern, $eventcontents, $result); if (!empty($result[2])) { $eventinformation['objecttable'] = $result[2]; } } } // I can't think of a nice way to get the following information. // Searching to see if @type has been used for the 'other' field in the event. $othertypepattern = "/(@type\s([\w|\s|.]*))+/"; $typeparams = array(); preg_match_all($othertypepattern, $eventcontents, $typeparams); if (!empty($typeparams[2])) { $eventinformation['typeparameter'] = array(); foreach ($typeparams[2] as $typeparameter) { $eventinformation['typeparameter'][] = $typeparameter; } } // Retrieving the 'other' event field information. $otherpattern = "/(\*\s{5,}-([\w|\s]*\:[\w|\s|\(|\)|.]*))/"; $typeparams = array(); preg_match_all($otherpattern, $eventcontents, $typeparams); if (!empty($typeparams[2])) { $eventinformation['otherparameter'] = array(); foreach ($typeparams[2] as $typeparameter) { $eventinformation['otherparameter'][] = $typeparameter; } } // Get parent class information. if ($parentclass = get_parent_class($eventname)) { $eventinformation['parentclass'] = '\\' . $parentclass; } // Fetch all the observers to be matched with this event. $allobserverslist = report_eventlist_list_generator::get_observer_list(); $observers = array(); if (isset($allobserverslist['\\core\\event\\base'])) { $observers = $allobserverslist['\\core\\event\\base']; } if (isset($allobserverslist[$eventname])) { $observers = array_merge($observers, $allobserverslist[$eventname]); } $PAGE->set_primary_active_tab('siteadminnode'); $PAGE->set_secondary_active_tab('reports'); // OUTPUT. $renderer = $PAGE->get_renderer('report_eventlist'); echo $renderer->render_event_detail($observers, $eventinformation); PK{\eventlist/settings.phpnu[. /** * Adds the event list link to the admin tree * * @package report_eventlist * @copyright 2014 Adrian Greeve * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); if ($hassiteconfig) { $url = $CFG->wwwroot . '/report/eventlist/index.php'; $ADMIN->add('reports', new admin_externalpage('reporteventlists', get_string('pluginname', 'report_eventlist'), $url)); // No report settings. $settings = null; } PK\,TTlib.phpnu[. /** * This file contains functions used by course reports * * @since Moodle 2.1 * @package coursereport * @copyright 2011 Andrew Davis * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ /** * Return a list of page types * @param string $pagetype current page type * @param stdClass $parentcontext Block's parent context * @param stdClass $currentcontext Current context of block */ function coursereport_page_type_list($pagetype, $parentcontext, $currentcontext) { $array = array( '*' => get_string('page-x', 'pagetype'), 'course-report-*' => get_string('page-course-report-x', 'pagetype') ); return $array; } PK5\f'#basic/lang/en/scormreport_basic.phpnu[. /** * Strings for component 'scorm_basic' report plugin * * @package scormreport * @subpackage basic * @author Ankit Kumar Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['pluginname'] = 'Basic report'; $string['privacy:metadata:preference:scorm_report_detailed'] = 'Whether to track details in the SCORM basic report'; $string['privacy:metadata:preference:scorm_report_pagesize'] = 'Number of users to display in the SCORM reports'; PK5\basic/version.phpnu[. /** * Defines the version of scorm_basic * * @package scormreport * @subpackage basic * @author Ankit Kumar Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'scormreport_basic'; PK5\Y "basic/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for scormreport_basic. * * @package scormreport_basic * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace scormreport_basic\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\transform; use \core_privacy\local\request\writer; /** * Privacy Subsystem for scormreport_basic. * * @copyright 2018 Sara Arjona * @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\user_preference_provider { /** * Returns meta data about this system. * * @param collection $collection The initialised item collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { // User preferences shared between different scorm reports. $collection->add_user_preference('scorm_report_pagesize', 'privacy:metadata:preference:scorm_report_pagesize'); // User preferences specific for this scorm report. $collection->add_user_preference('scorm_report_detailed', 'privacy:metadata:preference:scorm_report_detailed'); return $collection; } /** * Store all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { static::get_and_export_user_preference($userid, 'scorm_report_pagesize'); static::get_and_export_user_preference($userid, 'scorm_report_detailed', true); } /** * Get and export a user preference. * * @param int $userid The userid of the user whose data is to be exported. * @param string $userpreference The user preference to export. * @param boolean $transform If true, transform value to yesno. */ protected static function get_and_export_user_preference(int $userid, string $userpreference, $transform = false) { $prefvalue = get_user_preferences($userpreference, null, $userid); if ($prefvalue !== null) { if ($transform) { $transformedvalue = transform::yesno($prefvalue); } else { $transformedvalue = $prefvalue; } writer::export_user_preference( 'scormreport_basic', $userpreference, $transformedvalue, get_string('privacy:metadata:preference:'.$userpreference, 'scormreport_basic') ); } } } PK5\{:baabasic/classes/report.phpnu[. /** * Core Report class of basic reporting plugin * @package scormreport * @subpackage basic * @author Dan Marsden and Ankit Kumar Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace scormreport_basic; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/csvlib.class.php'); class report extends \mod_scorm\report { /** * displays the full report * @param \stdClass $scorm full SCORM object * @param \stdClass $cm - full course_module object * @param \stdClass $course - full course object * @param string $download - type of download being requested */ public function display($scorm, $cm, $course, $download) { global $CFG, $DB, $OUTPUT, $PAGE; $contextmodule = \context_module::instance($cm->id); $attemptsmode = optional_param('attemptsmode', SCORM_REPORT_ATTEMPTS_ALL_STUDENTS, PARAM_INT); $PAGE->set_url(new \moodle_url($PAGE->url, array('attemptsmode' => $attemptsmode))); // Scorm action bar for report. if ($download === '') { $actionbar = new \mod_scorm\output\actionbar($cm->id, true, $attemptsmode); $renderer = $PAGE->get_renderer('mod_scorm'); echo $renderer->report_actionbar($actionbar); } // Find out current groups mode. $currentgroup = groups_get_activity_group($cm, true); // Detailed report. $mform = new \mod_scorm_report_settings($PAGE->url, compact('currentgroup')); if ($fromform = $mform->get_data()) { $detailedrep = $fromform->detailedrep; $pagesize = $fromform->pagesize; set_user_preference('scorm_report_detailed', $detailedrep); set_user_preference('scorm_report_pagesize', $pagesize); } else { $detailedrep = get_user_preferences('scorm_report_detailed', false); $pagesize = get_user_preferences('scorm_report_pagesize', 0); } if ($pagesize < 1) { $pagesize = SCORM_REPORT_DEFAULT_PAGE_SIZE; } // Select group menu. $displayoptions = array(); $displayoptions['attemptsmode'] = $attemptsmode; if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used. if (!$download) { groups_print_activity_menu($cm, new \moodle_url($PAGE->url, $displayoptions)); } } // We only want to show the checkbox to delete attempts // if the user has permissions and if the report mode is showing attempts. $candelete = has_capability('mod/scorm:deleteresponses', $contextmodule) && ($attemptsmode != SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO); // Select the students. $nostudents = false; list($allowedlistsql, $params) = get_enrolled_sql($contextmodule, 'mod/scorm:savetrack', (int) $currentgroup); if (empty($currentgroup)) { // All users who can attempt scoes. if (!$DB->record_exists_sql($allowedlistsql, $params)) { echo $OUTPUT->notification(get_string('nostudentsyet')); $nostudents = true; } } else { // All users who can attempt scoes and who are in the currently selected group. if (!$DB->record_exists_sql($allowedlistsql, $params)) { echo $OUTPUT->notification(get_string('nostudentsingroup')); $nostudents = true; } } if ( !$nostudents ) { // Now check if asked download of data. $coursecontext = \context_course::instance($course->id); if ($download) { $shortname = format_string($course->shortname, true, array('context' => $coursecontext)); $filename = clean_filename("$shortname ".format_string($scorm->name, true)); } // Define table columns. $columns = array(); $headers = array(); if (!$download && $candelete) { $columns[] = 'checkbox'; $headers[] = $this->generate_master_checkbox(); } if (!$download && $CFG->grade_report_showuserimage) { $columns[] = 'picture'; $headers[] = ''; } $columns[] = 'fullname'; $headers[] = get_string('name'); // TODO Does not support custom user profile fields (MDL-70456). $extrafields = \core_user\fields::get_identity_fields($coursecontext, false); foreach ($extrafields as $field) { $columns[] = $field; $headers[] = \core_user\fields::get_display_name($field); } $columns[] = 'attempt'; $headers[] = get_string('attempt', 'scorm'); $columns[] = 'start'; $headers[] = get_string('started', 'scorm'); $columns[] = 'finish'; $headers[] = get_string('last', 'scorm'); $columns[] = 'score'; $headers[] = get_string('score', 'scorm'); if ($detailedrep && $scoes = $DB->get_records('scorm_scoes', array("scorm" => $scorm->id), 'sortorder, id')) { foreach ($scoes as $sco) { if ($sco->launch != '') { $columns[] = 'scograde'.$sco->id; $headers[] = format_string($sco->title); } } } else { $scoes = null; } if (!$download) { $table = new \flexible_table('mod-scorm-report'); $table->define_columns($columns); $table->define_headers($headers); $table->define_baseurl($PAGE->url); $table->sortable(true); $table->collapsible(true); // This is done to prevent redundant data, when a user has multiple attempts. $table->column_suppress('picture'); $table->column_suppress('fullname'); foreach ($extrafields as $field) { $table->column_suppress($field); } $table->no_sorting('start'); $table->no_sorting('finish'); $table->no_sorting('score'); $table->no_sorting('checkbox'); $table->no_sorting('picture'); if ( $scoes ) { foreach ($scoes as $sco) { if ($sco->launch != '') { $table->no_sorting('scograde'.$sco->id); } } } $table->column_class('picture', 'picture'); $table->column_class('fullname', 'bold'); $table->column_class('score', 'bold'); $table->set_attribute('cellspacing', '0'); $table->set_attribute('id', 'attempts'); $table->set_attribute('class', 'generaltable generalbox'); // Start working -- this is necessary as soon as the niceties are over. $table->setup(); } else if ($download == 'ODS') { require_once("$CFG->libdir/odslib.class.php"); $filename .= ".ods"; // Creating a workbook. $workbook = new \MoodleODSWorkbook("-"); // Sending HTTP headers. $workbook->send($filename); // Creating the first worksheet. $sheettitle = get_string('report', 'scorm'); $myxls = $workbook->add_worksheet($sheettitle); // Format types. $format = $workbook->add_format(); $format->set_bold(0); $formatbc = $workbook->add_format(); $formatbc->set_bold(1); $formatbc->set_align('center'); $formatb = $workbook->add_format(); $formatb->set_bold(1); $formaty = $workbook->add_format(); $formaty->set_bg_color('yellow'); $formatc = $workbook->add_format(); $formatc->set_align('center'); $formatr = $workbook->add_format(); $formatr->set_bold(1); $formatr->set_color('red'); $formatr->set_align('center'); $formatg = $workbook->add_format(); $formatg->set_bold(1); $formatg->set_color('green'); $formatg->set_align('center'); // Here starts workshhet headers. $colnum = 0; foreach ($headers as $item) { $myxls->write(0, $colnum, $item, $formatbc); $colnum++; } $rownum = 1; } else if ($download == 'Excel') { require_once("$CFG->libdir/excellib.class.php"); $filename .= ".xls"; // Creating a workbook. $workbook = new \MoodleExcelWorkbook("-"); // Sending HTTP headers. $workbook->send($filename); // Creating the first worksheet. $sheettitle = get_string('report', 'scorm'); $myxls = $workbook->add_worksheet($sheettitle); // Format types. $format = $workbook->add_format(); $format->set_bold(0); $formatbc = $workbook->add_format(); $formatbc->set_bold(1); $formatbc->set_align('center'); $formatb = $workbook->add_format(); $formatb->set_bold(1); $formaty = $workbook->add_format(); $formaty->set_bg_color('yellow'); $formatc = $workbook->add_format(); $formatc->set_align('center'); $formatr = $workbook->add_format(); $formatr->set_bold(1); $formatr->set_color('red'); $formatr->set_align('center'); $formatg = $workbook->add_format(); $formatg->set_bold(1); $formatg->set_color('green'); $formatg->set_align('center'); $colnum = 0; foreach ($headers as $item) { $myxls->write(0, $colnum, $item, $formatbc); $colnum++; } $rownum = 1; } else if ($download == 'CSV') { $csvexport = new \csv_export_writer("tab"); $csvexport->set_filename($filename, ".txt"); $csvexport->add_data($headers); } // Construct the SQL. $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(sa.attempt, 0)').' AS uniqueid, '; // TODO Does not support custom user profile fields (MDL-70456). $userfields = \core_user\fields::for_identity($coursecontext, false)->with_userpic()->including('idnumber'); $selectfields = $userfields->get_sql('u', false, '', 'userid')->selects; $select .= 'sa.scormid AS scormid, sa.attempt AS attempt ' . $selectfields . ' '; // This part is the same for all cases - join users and user tracking tables. $from = 'FROM {user} u '; $from .= 'LEFT JOIN {scorm_attempt} sa ON sa.userid = u.id AND sa.scormid = '.$scorm->id; switch ($attemptsmode) { case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH: // Show only students with attempts. $where = " WHERE u.id IN ({$allowedlistsql}) AND sa.userid IS NOT NULL"; break; case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO: // Show only students without attempts. $where = " WHERE u.id IN ({$allowedlistsql}) AND sa.userid IS NULL"; break; case SCORM_REPORT_ATTEMPTS_ALL_STUDENTS: // Show all students with or without attempts. $where = " WHERE u.id IN ({$allowedlistsql}) AND (sa.userid IS NOT NULL OR sa.userid IS NULL)"; break; } $countsql = 'SELECT COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(sa.attempt, 0)').')) AS nbresults, '; $countsql .= 'COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'sa.attempt').')) AS nbattempts, '; $countsql .= 'COUNT(DISTINCT(u.id)) AS nbusers '; $countsql .= $from.$where; if (!$download) { $sort = $table->get_sql_sort(); } else { $sort = ''; } // Fix some wired sorting. if (empty($sort)) { $sort = ' ORDER BY uniqueid'; } else { $sort = ' ORDER BY '.$sort; } if (!$download) { // Add extra limits due to initials bar. list($twhere, $tparams) = $table->get_sql_where(); if ($twhere) { $where .= ' AND '.$twhere; // Initial bar. $params = array_merge($params, $tparams); } if (!empty($countsql)) { $count = $DB->get_record_sql($countsql, $params); $totalinitials = $count->nbresults; if ($twhere) { $countsql .= ' AND '.$twhere; } $count = $DB->get_record_sql($countsql, $params); $total = $count->nbresults; } $table->pagesize($pagesize, $total); echo \html_writer::start_div('scormattemptcounts'); if ( $count->nbresults == $count->nbattempts ) { echo get_string('reportcountattempts', 'scorm', $count); } else if ( $count->nbattempts > 0 ) { echo get_string('reportcountallattempts', 'scorm', $count); } else { echo $count->nbusers.' '.get_string('users'); } echo \html_writer::end_div(); } // Fetch the attempts. if (!$download) { $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params, $table->get_page_start(), $table->get_page_size()); echo \html_writer::start_div('', array('id' => 'scormtablecontainer')); if ($candelete) { // Start form. $strreallydel = addslashes_js(get_string('deleteattemptcheck', 'scorm')); echo \html_writer::start_tag('form', array('id' => 'attemptsform', 'method' => 'post', 'action' => $PAGE->url->out(false), 'onsubmit' => 'return confirm("'.$strreallydel.'");')); echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'action', 'value' => 'delete')); echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey())); echo \html_writer::start_div('', array('style' => 'display: none;')); echo \html_writer::input_hidden_params($PAGE->url); echo \html_writer::end_div(); echo \html_writer::start_div(); } $table->initialbars($totalinitials > 20); // Build table rows. } else { $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params); } if ($attempts) { foreach ($attempts as $scouser) { $row = array(); if (!empty($scouser->attempt)) { $timetracks = scorm_get_sco_runtime($scorm->id, false, $scouser->userid, $scouser->attempt); } else { $timetracks = ''; } if (in_array('checkbox', $columns)) { if ($candelete && !empty($timetracks->start)) { $row[] = $this->generate_row_checkbox('attemptid[]', "{$scouser->userid}:{$scouser->attempt}"); } else if ($candelete) { $row[] = ''; } } if (in_array('picture', $columns)) { $user = new \stdClass(); $additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); $user = username_load_fields_from_object($user, $scouser, null, $additionalfields); $user->id = $scouser->userid; $row[] = $OUTPUT->user_picture($user, array('courseid' => $course->id)); } if (!$download) { $url = new \moodle_url('/user/view.php', array('id' => $scouser->userid, 'course' => $course->id)); $row[] = \html_writer::link($url, fullname($scouser)); } else { $row[] = fullname($scouser); } foreach ($extrafields as $field) { $row[] = s($scouser->{$field}); } if (empty($timetracks->start)) { $row[] = '-'; $row[] = '-'; $row[] = '-'; $row[] = '-'; } else { if (!$download) { $url = new \moodle_url('/mod/scorm/report/userreport.php', array('id' => $cm->id, 'user' => $scouser->userid, 'attempt' => $scouser->attempt, 'mode' => 'basic')); $row[] = \html_writer::link($url, $scouser->attempt); } else { $row[] = $scouser->attempt; } if ($download == 'ODS' || $download == 'Excel' ) { $row[] = userdate($timetracks->start, get_string("strftimedatetime", "langconfig")); } else { $row[] = userdate($timetracks->start); } if ($download == 'ODS' || $download == 'Excel' ) { $row[] = userdate($timetracks->finish, get_string('strftimedatetime', 'langconfig')); } else { $row[] = userdate($timetracks->finish); } $row[] = scorm_grade_user_attempt($scorm, $scouser->userid, $scouser->attempt); } // Print out all scores of attempt. if ($scoes) { foreach ($scoes as $sco) { if ($sco->launch != '') { if ($trackdata = scorm_get_tracks($sco->id, $scouser->userid, $scouser->attempt)) { if ($trackdata->status == '') { $trackdata->status = 'notattempted'; } $strstatus = get_string($trackdata->status, 'scorm'); // If raw score exists, print it. if ($trackdata->score_raw != '') { $score = $trackdata->score_raw; // Add max score if it exists. if (scorm_version_check($scorm->version, SCORM_13)) { $maxkey = 'cmi.score.max'; } else { $maxkey = 'cmi.core.score.max'; } if (isset($trackdata->$maxkey)) { $score .= '/'.$trackdata->$maxkey; } // Else print out status. } else { $score = $strstatus; } if (!$download) { $url = new \moodle_url('/mod/scorm/report/userreporttracks.php', array('id' => $cm->id, 'scoid' => $sco->id, 'user' => $scouser->userid, 'attempt' => $scouser->attempt, 'mode' => 'basic')); $row[] = $OUTPUT->pix_icon($trackdata->status, $strstatus, 'scorm') . '
    ' . \html_writer::link($url, $score, array('title' => get_string('details', 'scorm'))); } else { $row[] = $score; } } else { // If we don't have track data, we haven't attempted yet. $strstatus = get_string('notattempted', 'scorm'); if (!$download) { $row[] = $OUTPUT->pix_icon('notattempted', $strstatus, 'scorm') . '
    ' . $strstatus; } else { $row[] = $strstatus; } } } } } if (!$download) { $table->add_data($row); } else if ($download == 'Excel' or $download == 'ODS') { $colnum = 0; foreach ($row as $item) { $myxls->write($rownum, $colnum, $item, $format); $colnum++; } $rownum++; } else if ($download == 'CSV') { $csvexport->add_data($row); } } if (!$download) { $table->finish_output(); if ($candelete) { echo \html_writer::start_tag('table', ['id' => 'commands', 'class' => 'table-reboot']); echo \html_writer::start_tag('tr').\html_writer::start_tag('td'); echo $this->generate_delete_selected_button(); echo \html_writer::end_tag('td').\html_writer::end_tag('tr').\html_writer::end_tag('table'); // Close form. echo \html_writer::end_tag('div'); echo \html_writer::end_tag('form'); } } } else { if ($candelete && !$download) { echo \html_writer::end_div(); echo \html_writer::end_tag('form'); $table->finish_output(); } echo \html_writer::end_div(); } // Show preferences form irrespective of attempts are there to report or not. if (!$download) { $mform->set_data(compact('detailedrep', 'pagesize', 'attemptsmode')); $mform->display(); } if ($download == 'Excel' or $download == 'ODS') { $workbook->close(); exit; } else if ($download == 'CSV') { $csvexport->download_file(); exit; } } else { echo $OUTPUT->notification(get_string('noactivity', 'scorm')); } }// Function ends. } PK5\46//basic/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 the mod scorm basic report === 3.6 === * The following renamed classes have been completely removed: - scorm_basic_report (now: scormreport_basic\report)PK5\CJ %basic/tests/privacy/provider_test.phpnu[. /** * Unit tests for the scormreport_basic implementation of the privacy API. * * @package scormreport_basic * @category test * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace scormreport_basic\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\local\request\writer; use scormreport_basic\privacy\provider; /** * Unit tests for the scormreport_basic implementation of the privacy API. * * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends \core_privacy\tests\provider_testcase { /** * Basic setup for these tests. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Ensure that export_user_preferences returns no data if the user has no data. */ public function test_export_user_preferences_not_defined(): void { $user = \core_user::get_user_by_username('admin'); provider::export_user_preferences($user->id); $writer = writer::with_context(\context_system::instance()); $this->assertFalse($writer->has_any_data()); } /** * Ensure that export_user_preferences returns single preferences. */ public function test_export_user_preferences_single(): void { // Define a user preference. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); set_user_preference('scorm_report_detailed', 1); set_user_preference('scorm_report_pagesize', 50); // Validate exported data. provider::export_user_preferences($user->id); $context = \context_user::instance($user->id); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); $prefs = $writer->get_user_preferences('scormreport_basic'); $this->assertCount(2, (array) $prefs); $this->assertEquals( get_string('privacy:metadata:preference:scorm_report_detailed', 'scormreport_basic'), $prefs->scorm_report_detailed->description ); $this->assertEquals(get_string('yes'), $prefs->scorm_report_detailed->value); $this->assertEquals( get_string('privacy:metadata:preference:scorm_report_pagesize', 'scormreport_basic'), $prefs->scorm_report_pagesize->description ); $this->assertEquals(50, $prefs->scorm_report_pagesize->value); } } PK5\Z reportlib.phpnu[. /** * Returns an array of reports to which are currently readable. * @package mod_scorm * @author Ankit Kumar Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); /* Generates and returns list of available Scorm report sub-plugins * * @param context context level to check caps against * @return array list of valid reports present */ function scorm_report_list($context) { global $CFG; static $reportlist; if (!empty($reportlist)) { return $reportlist; } $installed = core_component::get_plugin_list('scormreport'); foreach ($installed as $reportname => $notused) { // Moodle 2.8+ style of autoloaded classes. $classname = "scormreport_$reportname\\report"; if (class_exists($classname)) { $report = new $classname(); if ($report->canview($context)) { $reportlist[] = $reportname; } continue; } // Legacy style of naming classes. $pluginfile = $CFG->dirroot.'/mod/scorm/report/'.$reportname.'/report.php'; if (is_readable($pluginfile)) { debugging("Please use autoloaded classnames for your plugin. Refer MDL-46469 for details", DEBUG_DEVELOPER); include_once($pluginfile); $reportclassname = "scorm_{$reportname}_report"; if (class_exists($reportclassname)) { $report = new $reportclassname(); if ($report->canview($context)) { $reportlist[] = $reportname; } } } } return $reportlist; } /** * Returns The maximum numbers of Questions associated with an Scorm Pack * * @param int Scorm ID * @return int an integer representing the question count */ function get_scorm_question_count($scormid) { global $DB; $count = 0; $params = array(); $sql = "SELECT DISTINCT e.id, e.element FROM {scorm_element} e JOIN {scorm_scoes_value} v ON e.id = v.elementid JOIN {scorm_attempt} a ON a.id = v.attemptid WHERE a.scormid = ? AND ". $DB->sql_like("element", "?", false) . " ORDER BY e.element"; $params[] = $scormid; $params[] = "cmi.interactions_%.id"; $rs = $DB->get_recordset_sql($sql, $params); $keywords = array("cmi.interactions_", ".id"); if ($rs->valid()) { foreach ($rs as $record) { $num = trim(str_ireplace($keywords, '', $record->element)); if (is_numeric($num) && $num > $count) { $count = $num; } } // Done as interactions start at 0 (do only if we have something to report). $count++; } $rs->close(); // Closing recordset. return $count; } PK5\: YY1interactions/lang/en/scormreport_interactions.phpnu[. /** * Strings for component 'scorm_interactions' report plugin * * @package scormreport * @subpackage interactions * @author Dan Marsden and Ankit Kumar Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $string['pluginname'] = 'Interactions report'; $string['privacy:metadata:preference:scorm_report_interactions_qtext'] = 'Whether to display the summary of questions in the SCORM interactions report'; $string['privacy:metadata:preference:scorm_report_interactions_resp'] = 'Whether to display the summary of responses in the SCORM interactions report'; $string['privacy:metadata:preference:scorm_report_interactions_result'] = 'Whether to display the summary of results in the SCORM interactions report'; $string['privacy:metadata:preference:scorm_report_interactions_right'] = 'Whether to display the summary of right anwers in the SCORM interactions report'; $string['privacy:metadata:preference:scorm_report_pagesize'] = 'Number of users to display in the SCORM reports'; $string['questionx'] = 'Question {$a}'; $string['responsex'] = 'Response {$a}'; $string['rightanswerx'] = 'Right answer {$a}'; $string['resultx'] = 'Result {$a}'; $string['summaryofquestiontext'] = 'Summary of question'; $string['summaryofresponse'] = 'Summary of responses'; $string['summaryofrightanswer'] = 'Summary of right answer'; $string['summaryofresult'] = 'Summary of result'; PK5\interactions/version.phpnu[. /** * Defines the version of scorm_interactions * @package scormreport * @subpackage interactions * @author Dan Marsden and Ankit Kumar Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'scormreport_interactions'; // Full name of the plugin (used for diagnostics). PK5\x= 'interactions/responsessettings_form.phpnu[. /** * Defines the version of scorm_interactions * @package scormreport * @subpackage interactions * @author Dan Marsden and Ankit Kumar Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once("$CFG->libdir/formslib.php"); class mod_scorm_report_interactions_settings extends moodleform { public function definition() { global $COURSE; $mform =& $this->_form; // ------------------------------------------------------------------------------- $mform->addElement('header', 'preferencespage', get_string('preferencespage', 'scorm')); $options = array(); if ($COURSE->id != SITEID) { $options[SCORM_REPORT_ATTEMPTS_ALL_STUDENTS] = get_string('optallstudents', 'scorm'); $options[SCORM_REPORT_ATTEMPTS_STUDENTS_WITH] = get_string('optattemptsonly', 'scorm'); $options[SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO] = get_string('optnoattemptsonly', 'scorm'); } $mform->addElement('select', 'attemptsmode', get_string('show', 'scorm'), $options); $mform->addElement('advcheckbox', 'qtext', '', get_string('summaryofquestiontext', 'scormreport_interactions')); $mform->addElement('advcheckbox', 'resp', '', get_string('summaryofresponse', 'scormreport_interactions')); $mform->addElement('advcheckbox', 'right', '', get_string('summaryofrightanswer', 'scormreport_interactions')); $mform->addElement('advcheckbox', 'result', '', get_string('summaryofresult', 'scormreport_interactions')); // ------------------------------------------------------------------------------- $mform->addElement('header', 'preferencesuser', get_string('preferencesuser', 'scorm')); $mform->addElement('text', 'pagesize', get_string('pagesize', 'scorm')); $mform->setType('pagesize', PARAM_INT); $this->add_action_buttons(false, get_string('savepreferences')); } } PK5\2wQQ)interactions/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for scormreport_interactions. * * @package scormreport_interactions * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace scormreport_interactions\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\transform; use \core_privacy\local\request\writer; /** * Privacy Subsystem for scormreport_interactions. * * @copyright 2018 Sara Arjona * @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\user_preference_provider { /** * Returns meta data about this system. * * @param collection $collection The initialised item collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { // User preferences shared between different scorm reports. $collection->add_user_preference('scorm_report_pagesize', 'privacy:metadata:preference:scorm_report_pagesize'); // User preferences specific for this scorm report. $collection->add_user_preference( 'scorm_report_interactions_qtext', 'privacy:metadata:preference:scorm_report_interactions_qtext' ); $collection->add_user_preference( 'scorm_report_interactions_resp', 'privacy:metadata:preference:scorm_report_interactions_resp' ); $collection->add_user_preference( 'scorm_report_interactions_right', 'privacy:metadata:preference:scorm_report_interactions_right' ); $collection->add_user_preference( 'scorm_report_interactions_result', 'privacy:metadata:preference:scorm_report_interactions_result' ); return $collection; } /** * Store all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { static::get_and_export_user_preference($userid, 'scorm_report_pagesize'); static::get_and_export_user_preference($userid, 'scorm_report_interactions_qtext', true); static::get_and_export_user_preference($userid, 'scorm_report_interactions_resp', true); static::get_and_export_user_preference($userid, 'scorm_report_interactions_right', true); static::get_and_export_user_preference($userid, 'scorm_report_interactions_result', true); } /** * Get and export a user preference. * * @param int $userid The userid of the user whose data is to be exported. * @param string $userpreference The user preference to export. * @param boolean $transform If true, transform value to yesno. */ protected static function get_and_export_user_preference(int $userid, string $userpreference, $transform = false) { $prefvalue = get_user_preferences($userpreference, null, $userid); if ($prefvalue !== null) { if ($transform) { $transformedvalue = transform::yesno($prefvalue); } else { $transformedvalue = $prefvalue; } writer::export_user_preference( 'scormreport_interactions', $userpreference, $transformedvalue, get_string('privacy:metadata:preference:'.$userpreference, 'scormreport_interactions') ); } } } PK5\E1]]uuinteractions/classes/report.phpnu[. /** * Core Report class of basic reporting plugin * @package scormreport * @subpackage interactions * @author Dan Marsden and Ankit Kumar Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace scormreport_interactions; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/mod/scorm/report/interactions/responsessettings_form.php'); class report extends \mod_scorm\report { /** * displays the full report * @param \stdClass $scorm full SCORM object * @param \stdClass $cm - full course_module object * @param \stdClass $course - full course object * @param string $download - type of download being requested */ public function display($scorm, $cm, $course, $download) { global $CFG, $DB, $OUTPUT, $PAGE; $contextmodule = \context_module::instance($cm->id); $attemptsmode = optional_param('attemptsmode', SCORM_REPORT_ATTEMPTS_ALL_STUDENTS, PARAM_INT); $PAGE->set_url(new \moodle_url($PAGE->url, array('attemptsmode' => $attemptsmode))); // Scorm action bar for report. if ($download === '') { $actionbar = new \mod_scorm\output\actionbar($cm->id, true, $attemptsmode); $renderer = $PAGE->get_renderer('mod_scorm'); echo $renderer->report_actionbar($actionbar); } // Find out current groups mode. $currentgroup = groups_get_activity_group($cm, true); // Detailed report. $mform = new \mod_scorm_report_interactions_settings($PAGE->url, compact('currentgroup')); if ($fromform = $mform->get_data()) { $pagesize = $fromform->pagesize; $includeqtext = $fromform->qtext; $includeresp = $fromform->resp; $includeright = $fromform->right; $includeresult = $fromform->result; set_user_preference('scorm_report_pagesize', $pagesize); set_user_preference('scorm_report_interactions_qtext', $includeqtext); set_user_preference('scorm_report_interactions_resp', $includeresp); set_user_preference('scorm_report_interactions_right', $includeright); set_user_preference('scorm_report_interactions_result', $includeresult); } else { $pagesize = get_user_preferences('scorm_report_pagesize', 0); $includeqtext = get_user_preferences('scorm_report_interactions_qtext', 0); $includeresp = get_user_preferences('scorm_report_interactions_resp', 1); $includeright = get_user_preferences('scorm_report_interactions_right', 0); $includeresult = get_user_preferences('scorm_report_interactions_result', 0); } if ($pagesize < 1) { $pagesize = SCORM_REPORT_DEFAULT_PAGE_SIZE; } // Select group menu. $displayoptions = array(); $displayoptions['attemptsmode'] = $attemptsmode; $displayoptions['qtext'] = $includeqtext; $displayoptions['resp'] = $includeresp; $displayoptions['right'] = $includeright; $displayoptions['result'] = $includeresult; $mform->set_data($displayoptions + array('pagesize' => $pagesize)); if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used. if (!$download) { groups_print_activity_menu($cm, new \moodle_url($PAGE->url, $displayoptions)); } } $formattextoptions = array('context' => \context_course::instance($course->id)); // We only want to show the checkbox to delete attempts // if the user has permissions and if the report mode is showing attempts. $candelete = has_capability('mod/scorm:deleteresponses', $contextmodule) && ($attemptsmode != SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO); // Select the students. $nostudents = false; list($allowedlistsql, $params) = get_enrolled_sql($contextmodule, 'mod/scorm:savetrack', (int) $currentgroup); if (empty($currentgroup)) { // All users who can attempt scoes. if (!$DB->record_exists_sql($allowedlistsql, $params)) { echo $OUTPUT->notification(get_string('nostudentsyet')); $nostudents = true; } } else { // All users who can attempt scoes and who are in the currently selected group. if (!$DB->record_exists_sql($allowedlistsql, $params)) { echo $OUTPUT->notification(get_string('nostudentsingroup')); $nostudents = true; } } if ( !$nostudents ) { // Now check if asked download of data. $coursecontext = \context_course::instance($course->id); if ($download) { $filename = clean_filename("$course->shortname ".format_string($scorm->name, true, $formattextoptions)); } // Define table columns. $columns = array(); $headers = array(); if (!$download && $candelete) { $columns[] = 'checkbox'; $headers[] = $this->generate_master_checkbox(); } if (!$download && $CFG->grade_report_showuserimage) { $columns[] = 'picture'; $headers[] = ''; } $columns[] = 'fullname'; $headers[] = get_string('name'); // TODO Does not support custom user profile fields (MDL-70456). $extrafields = \core_user\fields::get_identity_fields($coursecontext, false); foreach ($extrafields as $field) { $columns[] = $field; $headers[] = \core_user\fields::get_display_name($field); } $columns[] = 'attempt'; $headers[] = get_string('attempt', 'scorm'); $columns[] = 'start'; $headers[] = get_string('started', 'scorm'); $columns[] = 'finish'; $headers[] = get_string('last', 'scorm'); $columns[] = 'score'; $headers[] = get_string('score', 'scorm'); $scoes = $DB->get_records('scorm_scoes', array("scorm" => $scorm->id), 'sortorder, id'); foreach ($scoes as $sco) { if ($sco->launch != '') { $columns[] = 'scograde'.$sco->id; $headers[] = format_string($sco->title, '', $formattextoptions); } } // Construct the SQL. $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(sa.attempt, 0)').' AS uniqueid, '; // TODO Does not support custom user profile fields (MDL-70456). $userfields = \core_user\fields::for_identity($coursecontext, false)->with_userpic()->including('idnumber'); $selectfields = $userfields->get_sql('u', false, '', 'userid')->selects; $select .= 'sa.scormid AS scormid, sa.attempt AS attempt ' . $selectfields . ' '; // This part is the same for all cases - join users and user tracking tables. $from = 'FROM {user} u '; $from .= 'LEFT JOIN {scorm_attempt} sa ON sa.userid = u.id AND sa.scormid = '.$scorm->id; switch ($attemptsmode) { case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH: // Show only students with attempts. $where = " WHERE u.id IN ({$allowedlistsql}) AND sa.userid IS NOT NULL"; break; case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO: // Show only students without attempts. $where = " WHERE u.id IN ({$allowedlistsql}) AND sa.userid IS NULL"; break; case SCORM_REPORT_ATTEMPTS_ALL_STUDENTS: // Show all students with or without attempts. $where = " WHERE u.id IN ({$allowedlistsql}) AND (sa.userid IS NOT NULL OR sa.userid IS NULL)"; break; } $countsql = 'SELECT COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(sa.attempt, 0)').')) AS nbresults, '; $countsql .= 'COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'sa.attempt').')) AS nbattempts, '; $countsql .= 'COUNT(DISTINCT(u.id)) AS nbusers '; $countsql .= $from.$where; $questioncount = get_scorm_question_count($scorm->id); $nbmaincolumns = count($columns); for ($id = 0; $id < $questioncount; $id++) { if ($displayoptions['qtext']) { $columns[] = 'question' . $id; $headers[] = get_string('questionx', 'scormreport_interactions', $id); } if ($displayoptions['resp']) { $columns[] = 'response' . $id; $headers[] = get_string('responsex', 'scormreport_interactions', $id); } if ($displayoptions['right']) { $columns[] = 'right' . $id; $headers[] = get_string('rightanswerx', 'scormreport_interactions', $id); } if ($displayoptions['result']) { $columns[] = 'result' . $id; $headers[] = get_string('resultx', 'scormreport_interactions', $id); } } if (!$download) { $table = new \flexible_table('mod-scorm-report'); $table->define_columns($columns); $table->define_headers($headers); $table->define_baseurl($PAGE->url); $table->sortable(true); $table->collapsible(true); // This is done to prevent redundant data, when a user has multiple attempts. $table->column_suppress('picture'); $table->column_suppress('fullname'); foreach ($extrafields as $field) { $table->column_suppress($field); } $table->no_sorting('start'); $table->no_sorting('finish'); $table->no_sorting('score'); $table->no_sorting('checkbox'); $table->no_sorting('picture'); for ($id = 0; $id < $questioncount; $id++) { if ($displayoptions['qtext']) { $table->no_sorting('question'.$id); } if ($displayoptions['resp']) { $table->no_sorting('response'.$id); } if ($displayoptions['right']) { $table->no_sorting('right'.$id); } if ($displayoptions['result']) { $table->no_sorting('result'.$id); } } foreach ($scoes as $sco) { if ($sco->launch != '') { $table->no_sorting('scograde'.$sco->id); } } $table->column_class('picture', 'picture'); $table->column_class('fullname', 'bold'); $table->column_class('score', 'bold'); $table->set_attribute('cellspacing', '0'); $table->set_attribute('id', 'attempts'); $table->set_attribute('class', 'generaltable generalbox'); // Start working -- this is necessary as soon as the niceties are over. $table->setup(); } else if ($download == 'ODS') { require_once("$CFG->libdir/odslib.class.php"); $filename .= ".ods"; // Creating a workbook. $workbook = new \MoodleODSWorkbook("-"); // Sending HTTP headers. $workbook->send($filename); // Creating the first worksheet. $sheettitle = get_string('report', 'scorm'); $myxls = $workbook->add_worksheet($sheettitle); // Format types. $format = $workbook->add_format(); $format->set_bold(0); $formatbc = $workbook->add_format(); $formatbc->set_bold(1); $formatbc->set_align('center'); $formatb = $workbook->add_format(); $formatb->set_bold(1); $formaty = $workbook->add_format(); $formaty->set_bg_color('yellow'); $formatc = $workbook->add_format(); $formatc->set_align('center'); $formatr = $workbook->add_format(); $formatr->set_bold(1); $formatr->set_color('red'); $formatr->set_align('center'); $formatg = $workbook->add_format(); $formatg->set_bold(1); $formatg->set_color('green'); $formatg->set_align('center'); // Here starts workshhet headers. $colnum = 0; foreach ($headers as $item) { $myxls->write(0, $colnum, $item, $formatbc); $colnum++; } $rownum = 1; } else if ($download == 'Excel') { require_once("$CFG->libdir/excellib.class.php"); $filename .= ".xls"; // Creating a workbook. $workbook = new \MoodleExcelWorkbook("-"); // Sending HTTP headers. $workbook->send($filename); // Creating the first worksheet. $sheettitle = get_string('report', 'scorm'); $myxls = $workbook->add_worksheet($sheettitle); // Format types. $format = $workbook->add_format(); $format->set_bold(0); $formatbc = $workbook->add_format(); $formatbc->set_bold(1); $formatbc->set_align('center'); $formatb = $workbook->add_format(); $formatb->set_bold(1); $formaty = $workbook->add_format(); $formaty->set_bg_color('yellow'); $formatc = $workbook->add_format(); $formatc->set_align('center'); $formatr = $workbook->add_format(); $formatr->set_bold(1); $formatr->set_color('red'); $formatr->set_align('center'); $formatg = $workbook->add_format(); $formatg->set_bold(1); $formatg->set_color('green'); $formatg->set_align('center'); $colnum = 0; foreach ($headers as $item) { $myxls->write(0, $colnum, $item, $formatbc); $colnum++; } $rownum = 1; } else if ($download == 'CSV') { $csvexport = new \csv_export_writer("tab"); $csvexport->set_filename($filename, ".txt"); $csvexport->add_data($headers); } if (!$download) { $sort = $table->get_sql_sort(); } else { $sort = ''; } // Fix some wired sorting. if (empty($sort)) { $sort = ' ORDER BY uniqueid'; } else { $sort = ' ORDER BY '.$sort; } if (!$download) { // Add extra limits due to initials bar. list($twhere, $tparams) = $table->get_sql_where(); if ($twhere) { $where .= ' AND '.$twhere; // Initial bar. $params = array_merge($params, $tparams); } if (!empty($countsql)) { $count = $DB->get_record_sql($countsql, $params); $totalinitials = $count->nbresults; if ($twhere) { $countsql .= ' AND '.$twhere; } $count = $DB->get_record_sql($countsql, $params); $total = $count->nbresults; } $table->pagesize($pagesize, $total); echo \html_writer::start_div('scormattemptcounts'); if ( $count->nbresults == $count->nbattempts ) { echo get_string('reportcountattempts', 'scorm', $count); } else if ( $count->nbattempts > 0 ) { echo get_string('reportcountallattempts', 'scorm', $count); } else { echo $count->nbusers.' '.get_string('users'); } echo \html_writer::end_div(); } // Fetch the attempts. if (!$download) { $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params, $table->get_page_start(), $table->get_page_size()); echo \html_writer::start_div('', array('id' => 'scormtablecontainer')); if ($candelete) { // Start form. $strreallydel = addslashes_js(get_string('deleteattemptcheck', 'scorm')); echo \html_writer::start_tag('form', array('id' => 'attemptsform', 'method' => 'post', 'action' => $PAGE->url->out(false), 'onsubmit' => 'return confirm("'.$strreallydel.'");')); echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'action', 'value' => 'delete')); echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey())); echo \html_writer::start_div('', array('style' => 'display: none;')); echo \html_writer::input_hidden_params($PAGE->url); echo \html_writer::end_div(); echo \html_writer::start_div(); } $table->initialbars($totalinitials > 20); // Build table rows. } else { $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params); } if ($attempts) { foreach ($attempts as $scouser) { $row = array(); if (!empty($scouser->attempt)) { $timetracks = scorm_get_sco_runtime($scorm->id, false, $scouser->userid, $scouser->attempt); } else { $timetracks = ''; } if (in_array('checkbox', $columns)) { if ($candelete && !empty($timetracks->start)) { $row[] = $this->generate_row_checkbox('attemptid[]', "{$scouser->userid}:{$scouser->attempt}"); } else if ($candelete) { $row[] = ''; } } if (in_array('picture', $columns)) { $user = new \stdClass(); $additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); $user = username_load_fields_from_object($user, $scouser, null, $additionalfields); $user->id = $scouser->userid; $row[] = $OUTPUT->user_picture($user, array('courseid' => $course->id)); } if (!$download) { $url = new \moodle_url('/user/view.php', array('id' => $scouser->userid, 'course' => $course->id)); $row[] = \html_writer::link($url, fullname($scouser)); } else { $row[] = fullname($scouser); } foreach ($extrafields as $field) { $row[] = s($scouser->{$field}); } if (empty($timetracks->start)) { $row[] = '-'; $row[] = '-'; $row[] = '-'; $row[] = '-'; } else { if (!$download) { $url = new \moodle_url('/mod/scorm/report/userreport.php', array('id' => $cm->id, 'user' => $scouser->userid, 'attempt' => $scouser->attempt, 'mode' => 'interactions')); $row[] = \html_writer::link($url, $scouser->attempt); } else { $row[] = $scouser->attempt; } if ($download == 'ODS' || $download == 'Excel' ) { $row[] = userdate($timetracks->start, get_string("strftimedatetime", "langconfig")); } else { $row[] = userdate($timetracks->start); } if ($download == 'ODS' || $download == 'Excel' ) { $row[] = userdate($timetracks->finish, get_string('strftimedatetime', 'langconfig')); } else { $row[] = userdate($timetracks->finish); } $row[] = scorm_grade_user_attempt($scorm, $scouser->userid, $scouser->attempt); } // Print out all scores of attempt. $emptyrow = $download ? '' : ' '; foreach ($scoes as $sco) { if ($sco->launch != '') { if ($trackdata = scorm_get_tracks($sco->id, $scouser->userid, $scouser->attempt)) { if ($trackdata->status == '') { $trackdata->status = 'notattempted'; } $strstatus = get_string($trackdata->status, 'scorm'); // If raw score exists, print it. if ($trackdata->score_raw != '') { $score = $trackdata->score_raw; // Add max score if it exists. if (isset($trackdata->score_max)) { $score .= '/'.$trackdata->score_max; } } else { // Else print out status. $score = $strstatus; } if (!$download) { $url = new \moodle_url('/mod/scorm/report/userreporttracks.php', array('id' => $cm->id, 'scoid' => $sco->id, 'user' => $scouser->userid, 'attempt' => $scouser->attempt, 'mode' => 'interactions')); $row[] = $OUTPUT->pix_icon($trackdata->status, $strstatus, 'scorm') . '
    ' . \html_writer::link($url, $score, array('title' => get_string('details', 'scorm'))); } else { $row[] = $score; } // Interaction data. for ($i = 0; $i < $questioncount; $i++) { if ($displayoptions['qtext']) { $element = 'cmi.interactions_'.$i.'.id'; if (isset($trackdata->$element)) { $row[] = s($trackdata->$element); } else { $row[] = $emptyrow; } } if ($displayoptions['resp']) { $element = 'cmi.interactions_'.$i.'.student_response'; if (isset($trackdata->$element)) { $row[] = s($trackdata->$element); } else { $row[] = $emptyrow; } } if ($displayoptions['right']) { $j = 0; $element = 'cmi.interactions_'.$i.'.correct_responses_'.$j.'.pattern'; $rightans = ''; if (isset($trackdata->$element)) { while (isset($trackdata->$element)) { if ($j > 0) { $rightans .= ','; } $rightans .= s($trackdata->$element); $j++; $element = 'cmi.interactions_'.$i.'.correct_responses_'.$j.'.pattern'; } $row[] = $rightans; } else { $row[] = $emptyrow; } } if ($displayoptions['result']) { $element = 'cmi.interactions_'.$i.'.result'; if (isset($trackdata->$element)) { $row[] = s($trackdata->$element); } else { $row[] = $emptyrow; } } } // End of interaction data. } else { // If we don't have track data, we haven't attempted yet. $strstatus = get_string('notattempted', 'scorm'); if (!$download) { $row[] = $OUTPUT->pix_icon('notattempted', $strstatus, 'scorm') . '
    ' . $strstatus; } else { $row[] = $strstatus; } // Complete the empty cells. for ($i = 0; $i < count($columns) - $nbmaincolumns; $i++) { $row[] = $emptyrow; } } } } if (!$download) { $table->add_data($row); } else if ($download == 'Excel' or $download == 'ODS') { $colnum = 0; foreach ($row as $item) { $myxls->write($rownum, $colnum, $item, $format); $colnum++; } $rownum++; } else if ($download == 'CSV') { $csvexport->add_data($row); } } if (!$download) { $table->finish_output(); if ($candelete) { echo \html_writer::start_tag('table', ['id' => 'commands', 'class' => 'table-reboot']); echo \html_writer::start_tag('tr').\html_writer::start_tag('td'); echo $this->generate_delete_selected_button(); echo \html_writer::end_tag('td').\html_writer::end_tag('tr').\html_writer::end_tag('table'); // Close form. echo \html_writer::end_tag('div'); echo \html_writer::end_tag('form'); } } } else { if ($candelete && !$download) { echo \html_writer::end_div(); echo \html_writer::end_tag('form'); $table->finish_output(); } echo \html_writer::end_div(); } // Show preferences form irrespective of attempts are there to report or not. if (!$download) { $mform->set_data(compact('pagesize', 'attemptsmode')); $mform->display(); } if ($download == 'Excel' or $download == 'ODS') { $workbook->close(); exit; } else if ($download == 'CSV') { $csvexport->download_file(); exit; } } else { echo $OUTPUT->notification(get_string('noactivity', 'scorm')); } }// Function ends. } PK5\B|5?,interactions/tests/privacy/provider_test.phpnu[. /** * Unit tests for the scormreport_interactions implementation of the privacy API. * * @package scormreport_interactions * @category test * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace scormreport_interactions\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\local\request\writer; use scormreport_interactions\privacy\provider; /** * Unit tests for the scormreport_interactions implementation of the privacy API. * * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends \core_privacy\tests\provider_testcase { /** * Basic setup for these tests. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Ensure that export_user_preferences returns no data if the user has no data. */ public function test_export_user_preferences_not_defined(): void { $user = \core_user::get_user_by_username('admin'); provider::export_user_preferences($user->id); $writer = writer::with_context(\context_system::instance()); $this->assertFalse($writer->has_any_data()); } /** * Ensure that export_user_preferences returns single preferences. */ public function test_export_user_preferences_single(): void { // Define a user preference. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); set_user_preference('scorm_report_pagesize', 50); set_user_preference('scorm_report_interactions_qtext', 1); set_user_preference('scorm_report_interactions_resp', 0); set_user_preference('scorm_report_interactions_right', 1); set_user_preference('scorm_report_interactions_result', 1); // Validate exported data. provider::export_user_preferences($user->id); $context = \context_user::instance($user->id); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); $prefs = $writer->get_user_preferences('scormreport_interactions'); $this->assertCount(5, (array) $prefs); $this->assertEquals( get_string('privacy:metadata:preference:scorm_report_pagesize', 'scormreport_interactions'), $prefs->scorm_report_pagesize->description ); $this->assertEquals(50, $prefs->scorm_report_pagesize->value); $this->assertEquals( get_string('privacy:metadata:preference:scorm_report_interactions_qtext', 'scormreport_interactions'), $prefs->scorm_report_interactions_qtext->description ); $this->assertEquals(get_string('yes'), $prefs->scorm_report_interactions_qtext->value); $this->assertEquals(get_string('no'), $prefs->scorm_report_interactions_resp->value); } } PK5\ڔ-objectives/lang/en/scormreport_objectives.phpnu[. /** * Strings for component 'scorm_objectives' report plugin * * @package scormreport_objectives * @author Dan Marsden * @copyright 2013 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $string['pluginname'] = 'Objectives report'; $string['privacy:metadata:preference:scorm_report_objectives_score'] = 'Whether to display the objective score in the SCORM report'; $string['privacy:metadata:preference:scorm_report_pagesize'] = 'Number of users to display in the SCORM reports'; $string['objectivex'] = 'Objective {$a}'; $string['objectivescore'] = 'Show objective score'; $string['score'] = 'score'; $string['status'] = 'status'; PK5\L|x88objectives/version.phpnu[. /** * Defines the version of scorm_objectives * @package scormreport_objectives * @author Dan Marsden * @copyright 2013 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'scormreport_objectives'; PK5\W?c c %objectives/responsessettings_form.phpnu[. /** * Defines the version of scorm_objectives * @package scormreport_objectives * @author Dan Marsden * @copyright 2013 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); require_once("$CFG->libdir/formslib.php"); /** * A form that displays the objective report settings * * @copyright 2013 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class mod_scorm_report_objectives_settings extends moodleform { /** * Definition of the setting form elements */ protected function definition() { global $COURSE; $mform =& $this->_form; $mform->addElement('header', 'preferencespage', get_string('preferencespage', 'scorm')); $options = array(); if ($COURSE->id != SITEID) { $options[SCORM_REPORT_ATTEMPTS_ALL_STUDENTS] = get_string('optallstudents', 'scorm'); $options[SCORM_REPORT_ATTEMPTS_STUDENTS_WITH] = get_string('optattemptsonly', 'scorm'); $options[SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO] = get_string('optnoattemptsonly', 'scorm'); } $mform->addElement('select', 'attemptsmode', get_string('show', 'scorm'), $options); $mform->addElement('advcheckbox', 'objectivescore', '', get_string('objectivescore', 'scormreport_objectives')); $mform->addElement('header', 'preferencesuser', get_string('preferencesuser', 'scorm')); $mform->addElement('text', 'pagesize', get_string('pagesize', 'scorm')); $mform->setType('pagesize', PARAM_INT); $this->add_action_buttons(false, get_string('savepreferences')); } } PK5\+`'objectives/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for scormreport_objectives. * * @package scormreport_objectives * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace scormreport_objectives\privacy; defined('MOODLE_INTERNAL') || die(); use \core_privacy\local\metadata\collection; use \core_privacy\local\request\transform; use \core_privacy\local\request\writer; /** * Privacy Subsystem for scormreport_objectives. * * @copyright 2018 Sara Arjona * @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\user_preference_provider { /** * Returns meta data about this system. * * @param collection $collection The initialised item collection to add items to. * @return collection A listing of user data stored through this system. */ public static function get_metadata(collection $collection): collection { // User preferences shared between different scorm reports. $collection->add_user_preference('scorm_report_pagesize', 'privacy:metadata:preference:scorm_report_pagesize'); // User preferences specific for this scorm report. $collection->add_user_preference( 'scorm_report_objectives_score', 'privacy:metadata:preference:scorm_report_objectives_score' ); return $collection; } /** * Store all user preferences for the plugin. * * @param int $userid The userid of the user whose data is to be exported. */ public static function export_user_preferences(int $userid) { static::get_and_export_user_preference($userid, 'scorm_report_pagesize'); static::get_and_export_user_preference($userid, 'scorm_report_objectives_score', true); } /** * Get and export a user preference. * * @param int $userid The userid of the user whose data is to be exported. * @param string $userpreference The user preference to export. * @param boolean $transform If true, transform value to yesno. */ protected static function get_and_export_user_preference(int $userid, string $userpreference, $transform = false) { $prefvalue = get_user_preferences($userpreference, null, $userid); if ($prefvalue !== null) { if ($transform) { $transformedvalue = transform::yesno($prefvalue); } else { $transformedvalue = $prefvalue; } writer::export_user_preference( 'scormreport_objectives', $userpreference, $transformedvalue, get_string('privacy:metadata:preference:'.$userpreference, 'scormreport_objectives') ); } } } PK5\q)xxobjectives/classes/report.phpnu[. /** * Core Report class of objectives SCORM report plugin * @package scormreport_objectives * @author Dan Marsden * @copyright 2013 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace scormreport_objectives; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/mod/scorm/report/objectives/responsessettings_form.php'); /** * Objectives report class * * @copyright 2013 Dan Marsden * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report extends \mod_scorm\report { /** * displays the full report * @param \stdClass $scorm full SCORM object * @param \stdClass $cm - full course_module object * @param \stdClass $course - full course object * @param string $download - type of download being requested */ public function display($scorm, $cm, $course, $download) { global $CFG, $DB, $OUTPUT, $PAGE; $contextmodule = \context_module::instance($cm->id); $attemptsmode = optional_param('attemptsmode', SCORM_REPORT_ATTEMPTS_ALL_STUDENTS, PARAM_INT); $PAGE->set_url(new \moodle_url($PAGE->url, array('attemptsmode' => $attemptsmode))); // Scorm action bar for report. if ($download === '') { $actionbar = new \mod_scorm\output\actionbar($cm->id, true, $attemptsmode); $renderer = $PAGE->get_renderer('mod_scorm'); echo $renderer->report_actionbar($actionbar); } // Find out current groups mode. $currentgroup = groups_get_activity_group($cm, true); // Detailed report. $mform = new \mod_scorm_report_objectives_settings($PAGE->url, compact('currentgroup')); if ($fromform = $mform->get_data()) { $pagesize = $fromform->pagesize; $showobjectivescore = $fromform->objectivescore; set_user_preference('scorm_report_pagesize', $pagesize); set_user_preference('scorm_report_objectives_score', $showobjectivescore); } else { $pagesize = get_user_preferences('scorm_report_pagesize', 0); $showobjectivescore = get_user_preferences('scorm_report_objectives_score', 0); } if ($pagesize < 1) { $pagesize = SCORM_REPORT_DEFAULT_PAGE_SIZE; } // Select group menu. $displayoptions = array(); $displayoptions['attemptsmode'] = $attemptsmode; $displayoptions['objectivescore'] = $showobjectivescore; $mform->set_data($displayoptions + array('pagesize' => $pagesize)); if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used. if (!$download) { groups_print_activity_menu($cm, new \moodle_url($PAGE->url, $displayoptions)); } } $formattextoptions = array('context' => \context_course::instance($course->id)); // We only want to show the checkbox to delete attempts // if the user has permissions and if the report mode is showing attempts. $candelete = has_capability('mod/scorm:deleteresponses', $contextmodule) && ($attemptsmode != SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO); // Select the students. $nostudents = false; list($allowedlistsql, $params) = get_enrolled_sql($contextmodule, 'mod/scorm:savetrack', (int) $currentgroup); if (empty($currentgroup)) { // All users who can attempt scoes. if (!$DB->record_exists_sql($allowedlistsql, $params)) { echo $OUTPUT->notification(get_string('nostudentsyet')); $nostudents = true; } } else { // All users who can attempt scoes and who are in the currently selected group. if (!$DB->record_exists_sql($allowedlistsql, $params)) { echo $OUTPUT->notification(get_string('nostudentsingroup')); $nostudents = true; } } if ( !$nostudents ) { // Now check if asked download of data. $coursecontext = \context_course::instance($course->id); if ($download) { $filename = clean_filename("$course->shortname ".format_string($scorm->name, true, $formattextoptions)); } // Define table columns. $columns = array(); $headers = array(); if (!$download && $candelete) { $columns[] = 'checkbox'; $headers[] = $this->generate_master_checkbox(); } if (!$download && $CFG->grade_report_showuserimage) { $columns[] = 'picture'; $headers[] = ''; } $columns[] = 'fullname'; $headers[] = get_string('name'); // TODO Does not support custom user profile fields (MDL-70456). $extrafields = \core_user\fields::get_identity_fields($coursecontext, false); foreach ($extrafields as $field) { $columns[] = $field; $headers[] = \core_user\fields::get_display_name($field); } $columns[] = 'attempt'; $headers[] = get_string('attempt', 'scorm'); $columns[] = 'start'; $headers[] = get_string('started', 'scorm'); $columns[] = 'finish'; $headers[] = get_string('last', 'scorm'); $columns[] = 'score'; $headers[] = get_string('score', 'scorm'); $scoes = $DB->get_records('scorm_scoes', array("scorm" => $scorm->id), 'sortorder, id'); foreach ($scoes as $sco) { if ($sco->launch != '') { $columns[] = 'scograde'.$sco->id; $headers[] = format_string($sco->title, '', $formattextoptions); } } // Construct the SQL. $select = 'SELECT DISTINCT '.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(sa.attempt, 0)').' AS uniqueid, '; // TODO Does not support custom user profile fields (MDL-70456). $userfields = \core_user\fields::for_identity($coursecontext, false)->with_userpic()->including('idnumber'); $selectfields = $userfields->get_sql('u', false, '', 'userid')->selects; $select .= 'sa.scormid AS scormid, sa.attempt AS attempt ' . $selectfields . ' '; // This part is the same for all cases - join users and user tracking tables. $from = 'FROM {user} u '; $from .= 'LEFT JOIN {scorm_attempt} sa ON sa.userid = u.id AND sa.scormid = '.$scorm->id; switch ($attemptsmode) { case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH: // Show only students with attempts. $where = " WHERE u.id IN ({$allowedlistsql}) AND sa.userid IS NOT NULL"; break; case SCORM_REPORT_ATTEMPTS_STUDENTS_WITH_NO: // Show only students without attempts. $where = " WHERE u.id IN ({$allowedlistsql}) AND sa.userid IS NULL"; break; case SCORM_REPORT_ATTEMPTS_ALL_STUDENTS: // Show all students with or without attempts. $where = " WHERE u.id IN ({$allowedlistsql}) AND (sa.userid IS NOT NULL OR sa.userid IS NULL)"; break; } $countsql = 'SELECT COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'COALESCE(sa.attempt, 0)').')) AS nbresults, '; $countsql .= 'COUNT(DISTINCT('.$DB->sql_concat('u.id', '\'#\'', 'sa.attempt').')) AS nbattempts, '; $countsql .= 'COUNT(DISTINCT(u.id)) AS nbusers '; $countsql .= $from.$where; $nbmaincolumns = count($columns); // Get number of main columns used. $objectives = get_scorm_objectives($scorm->id); $nosort = array(); foreach ($objectives as $scoid => $sco) { foreach ($sco as $id => $objectivename) { $colid = $scoid.'objectivestatus' . $id; $columns[] = $colid; $nosort[] = $colid; if (!$displayoptions['objectivescore']) { // Display the objective name only. $headers[] = $objectivename; } else { // Display the objective status header with a "status" suffix to avoid confusion. $headers[] = $objectivename. ' '. get_string('status', 'scormreport_objectives'); // Now print objective score headers. $colid = $scoid.'objectivescore' . $id; $columns[] = $colid; $nosort[] = $colid; $headers[] = $objectivename. ' '. get_string('score', 'scormreport_objectives'); } } } $emptycell = ''; // Used when an empty cell is being printed - in html we add a space. if (!$download) { $emptycell = ' '; $table = new \flexible_table('mod-scorm-report'); $table->define_columns($columns); $table->define_headers($headers); $table->define_baseurl($PAGE->url); $table->sortable(true); $table->collapsible(true); // This is done to prevent redundant data, when a user has multiple attempts. $table->column_suppress('picture'); $table->column_suppress('fullname'); foreach ($extrafields as $field) { $table->column_suppress($field); } foreach ($nosort as $field) { $table->no_sorting($field); } $table->no_sorting('start'); $table->no_sorting('finish'); $table->no_sorting('score'); $table->no_sorting('checkbox'); $table->no_sorting('picture'); foreach ($scoes as $sco) { if ($sco->launch != '') { $table->no_sorting('scograde'.$sco->id); } } $table->column_class('picture', 'picture'); $table->column_class('fullname', 'bold'); $table->column_class('score', 'bold'); $table->set_attribute('cellspacing', '0'); $table->set_attribute('id', 'attempts'); $table->set_attribute('class', 'generaltable generalbox'); // Start working -- this is necessary as soon as the niceties are over. $table->setup(); } else if ($download == 'ODS') { require_once("$CFG->libdir/odslib.class.php"); $filename .= ".ods"; // Creating a workbook. $workbook = new \MoodleODSWorkbook("-"); // Sending HTTP headers. $workbook->send($filename); // Creating the first worksheet. $sheettitle = get_string('report', 'scorm'); $myxls = $workbook->add_worksheet($sheettitle); // Format types. $format = $workbook->add_format(); $format->set_bold(0); $formatbc = $workbook->add_format(); $formatbc->set_bold(1); $formatbc->set_align('center'); $formatb = $workbook->add_format(); $formatb->set_bold(1); $formaty = $workbook->add_format(); $formaty->set_bg_color('yellow'); $formatc = $workbook->add_format(); $formatc->set_align('center'); $formatr = $workbook->add_format(); $formatr->set_bold(1); $formatr->set_color('red'); $formatr->set_align('center'); $formatg = $workbook->add_format(); $formatg->set_bold(1); $formatg->set_color('green'); $formatg->set_align('center'); // Here starts workshhet headers. $colnum = 0; foreach ($headers as $item) { $myxls->write(0, $colnum, $item, $formatbc); $colnum++; } $rownum = 1; } else if ($download == 'Excel') { require_once("$CFG->libdir/excellib.class.php"); $filename .= ".xls"; // Creating a workbook. $workbook = new \MoodleExcelWorkbook("-"); // Sending HTTP headers. $workbook->send($filename); // Creating the first worksheet. $sheettitle = get_string('report', 'scorm'); $myxls = $workbook->add_worksheet($sheettitle); // Format types. $format = $workbook->add_format(); $format->set_bold(0); $formatbc = $workbook->add_format(); $formatbc->set_bold(1); $formatbc->set_align('center'); $formatb = $workbook->add_format(); $formatb->set_bold(1); $formaty = $workbook->add_format(); $formaty->set_bg_color('yellow'); $formatc = $workbook->add_format(); $formatc->set_align('center'); $formatr = $workbook->add_format(); $formatr->set_bold(1); $formatr->set_color('red'); $formatr->set_align('center'); $formatg = $workbook->add_format(); $formatg->set_bold(1); $formatg->set_color('green'); $formatg->set_align('center'); $colnum = 0; foreach ($headers as $item) { $myxls->write(0, $colnum, $item, $formatbc); $colnum++; } $rownum = 1; } else if ($download == 'CSV') { $csvexport = new \csv_export_writer("tab"); $csvexport->set_filename($filename, ".txt"); $csvexport->add_data($headers); } if (!$download) { $sort = $table->get_sql_sort(); } else { $sort = ''; } // Fix some wired sorting. if (empty($sort)) { $sort = ' ORDER BY uniqueid'; } else { $sort = ' ORDER BY '.$sort; } if (!$download) { // Add extra limits due to initials bar. list($twhere, $tparams) = $table->get_sql_where(); if ($twhere) { $where .= ' AND '.$twhere; // Initial bar. $params = array_merge($params, $tparams); } if (!empty($countsql)) { $count = $DB->get_record_sql($countsql, $params); $totalinitials = $count->nbresults; if ($twhere) { $countsql .= ' AND '.$twhere; } $count = $DB->get_record_sql($countsql, $params); $total = $count->nbresults; } $table->pagesize($pagesize, $total); echo \html_writer::start_div('scormattemptcounts'); if ( $count->nbresults == $count->nbattempts ) { echo get_string('reportcountattempts', 'scorm', $count); } else if ( $count->nbattempts > 0 ) { echo get_string('reportcountallattempts', 'scorm', $count); } else { echo $count->nbusers.' '.get_string('users'); } echo \html_writer::end_div(); } // Fetch the attempts. if (!$download) { $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params, $table->get_page_start(), $table->get_page_size()); echo \html_writer::start_div('', array('id' => 'scormtablecontainer')); if ($candelete) { // Start form. $strreallydel = addslashes_js(get_string('deleteattemptcheck', 'scorm')); echo \html_writer::start_tag('form', array('id' => 'attemptsform', 'method' => 'post', 'action' => $PAGE->url->out(false), 'onsubmit' => 'return confirm("'.$strreallydel.'");')); echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'action', 'value' => 'delete')); echo \html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey())); echo \html_writer::start_div('', array('style' => 'display: none;')); echo \html_writer::input_hidden_params($PAGE->url); echo \html_writer::end_div(); echo \html_writer::start_div(); } $table->initialbars($totalinitials > 20); // Build table rows. } else { $attempts = $DB->get_records_sql($select.$from.$where.$sort, $params); } if ($attempts) { foreach ($attempts as $scouser) { $row = array(); if (!empty($scouser->attempt)) { $timetracks = scorm_get_sco_runtime($scorm->id, false, $scouser->userid, $scouser->attempt); } else { $timetracks = ''; } if (in_array('checkbox', $columns)) { if ($candelete && !empty($timetracks->start)) { $row[] = $this->generate_row_checkbox('attemptid[]', "{$scouser->userid}:{$scouser->attempt}"); } else if ($candelete) { $row[] = ''; } } if (in_array('picture', $columns)) { $user = new \stdClass(); $additionalfields = explode(',', implode(',', \core_user\fields::get_picture_fields())); $user = username_load_fields_from_object($user, $scouser, null, $additionalfields); $user->id = $scouser->userid; $row[] = $OUTPUT->user_picture($user, array('courseid' => $course->id)); } if (!$download) { $url = new \moodle_url('/user/view.php', array('id' => $scouser->userid, 'course' => $course->id)); $row[] = \html_writer::link($url, fullname($scouser)); } else { $row[] = fullname($scouser); } foreach ($extrafields as $field) { $row[] = s($scouser->{$field}); } if (empty($timetracks->start)) { $row[] = '-'; $row[] = '-'; $row[] = '-'; $row[] = '-'; } else { if (!$download) { $url = new \moodle_url('/mod/scorm/report/userreport.php', array('id' => $cm->id, 'user' => $scouser->userid, 'attempt' => $scouser->attempt, 'mode' => 'objectives')); $row[] = \html_writer::link($url, $scouser->attempt); } else { $row[] = $scouser->attempt; } if ($download == 'ODS' || $download == 'Excel' ) { $row[] = userdate($timetracks->start, get_string("strftimedatetime", "langconfig")); } else { $row[] = userdate($timetracks->start); } if ($download == 'ODS' || $download == 'Excel' ) { $row[] = userdate($timetracks->finish, get_string('strftimedatetime', 'langconfig')); } else { $row[] = userdate($timetracks->finish); } $row[] = scorm_grade_user_attempt($scorm, $scouser->userid, $scouser->attempt); } // Print out all scores of attempt. foreach ($scoes as $sco) { if ($sco->launch != '') { if ($trackdata = scorm_get_tracks($sco->id, $scouser->userid, $scouser->attempt)) { if ($trackdata->status == '') { $trackdata->status = 'notattempted'; } $strstatus = get_string($trackdata->status, 'scorm'); if ($trackdata->score_raw != '') { // If raw score exists, print it. $score = $trackdata->score_raw; // Add max score if it exists. if (isset($trackdata->score_max)) { $score .= '/'.$trackdata->score_max; } } else { // ...else print out status. $score = $strstatus; } if (!$download) { $url = new \moodle_url('/mod/scorm/report/userreporttracks.php', array('id' => $cm->id, 'scoid' => $sco->id, 'user' => $scouser->userid, 'attempt' => $scouser->attempt, 'mode' => 'objectives')); $row[] = $OUTPUT->pix_icon($trackdata->status, $strstatus, 'scorm') . '
    ' . \html_writer::link($url, $score, array('title' => get_string('details', 'scorm'))); } else { $row[] = $score; } // Iterate over tracks and match objective id against values. $scorm2004 = false; if (scorm_version_check($scorm->version, SCORM_13)) { $scorm2004 = true; $objectiveprefix = "cmi.objectives."; } else { $objectiveprefix = "cmi.objectives_"; } $keywords = array(".id", $objectiveprefix); $objectivestatus = array(); $objectivescore = array(); foreach ($trackdata as $name => $value) { if (strpos($name, $objectiveprefix) === 0 && strrpos($name, '.id') !== false) { $num = trim(str_ireplace($keywords, '', $name)); if (is_numeric($num)) { if ($scorm2004) { $element = $objectiveprefix.$num.'.completion_status'; } else { $element = $objectiveprefix.$num.'.status'; } if (isset($trackdata->$element)) { $objectivestatus[$value] = $trackdata->$element; } else { $objectivestatus[$value] = ''; } if ($displayoptions['objectivescore']) { $element = $objectiveprefix.$num.'.score.raw'; if (isset($trackdata->$element)) { $objectivescore[$value] = $trackdata->$element; } else { $objectivescore[$value] = ''; } } } } } // Interaction data. if (!empty($objectives[$trackdata->scoid])) { foreach ($objectives[$trackdata->scoid] as $name) { if (isset($objectivestatus[$name])) { $row[] = s($objectivestatus[$name]); } else { $row[] = $emptycell; } if ($displayoptions['objectivescore']) { if (isset($objectivescore[$name])) { $row[] = s($objectivescore[$name]); } else { $row[] = $emptycell; } } } } // End of interaction data. } else { // If we don't have track data, we haven't attempted yet. $strstatus = get_string('notattempted', 'scorm'); if (!$download) { $row[] = $OUTPUT->pix_icon('notattempted', $strstatus, 'scorm') . '
    ' . $strstatus; } else { $row[] = $strstatus; } // Complete the empty cells. for ($i = 0; $i < count($columns) - $nbmaincolumns; $i++) { $row[] = $emptycell; } } } } if (!$download) { $table->add_data($row); } else if ($download == 'Excel' or $download == 'ODS') { $colnum = 0; foreach ($row as $item) { $myxls->write($rownum, $colnum, $item, $format); $colnum++; } $rownum++; } else if ($download == 'CSV') { $csvexport->add_data($row); } } if (!$download) { $table->finish_output(); if ($candelete) { echo \html_writer::start_tag('table', ['id' => 'commands', 'class' => 'table-reboot']); echo \html_writer::start_tag('tr').\html_writer::start_tag('td'); echo $this->generate_delete_selected_button(); echo \html_writer::end_tag('td').\html_writer::end_tag('tr').\html_writer::end_tag('table'); // Close form. echo \html_writer::end_tag('div'); echo \html_writer::end_tag('form'); } } } else { if ($candelete && !$download) { echo \html_writer::end_div(); echo \html_writer::end_tag('form'); $table->finish_output(); } echo \html_writer::end_div(); } // Show preferences form irrespective of attempts are there to report or not. if (!$download) { $mform->set_data(compact('pagesize', 'attemptsmode')); $mform->display(); } if ($download == 'Excel' or $download == 'ODS') { $workbook->close(); exit; } else if ($download == 'CSV') { $csvexport->download_file(); exit; } } else { echo $OUTPUT->notification(get_string('noactivity', 'scorm')); } }// Function ends. } /** * Returns The maximum numbers of Objectives associated with an Scorm Pack * * @param int $scormid Scorm instance id * @return array an array of possible objectives. */ function get_scorm_objectives($scormid) { global $DB; $objectives = []; $params = ['scormid' => $scormid, 'search' => 'cmi.objectives%.id']; $value = $DB->sql_compare_text('v.value'); $sql = "SELECT DISTINCT $value as value, ss.id FROM {scorm_scoes_value} v JOIN {scorm_scoes} ss ON ss.id = v.scoid AND ss.scorm = :scormid JOIN {scorm_element} e ON v.elementid = e.id WHERE ".$DB->sql_like("element", ":search", false); $rs = $DB->get_records_sql($sql, $params); foreach ($rs as $record) { $objectives[$record->scoid][] = $record->value; } // Now naturally sort the sco arrays. foreach ($objectives as $scoid => $sco) { natsort($objectives[$scoid]); } return $objectives; } PK5\TT T *objectives/tests/privacy/provider_test.phpnu[. /** * Unit tests for the scormreport_objectives implementation of the privacy API. * * @package scormreport_objectives * @category test * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace scormreport_objectives\privacy; defined('MOODLE_INTERNAL') || die(); use core_privacy\local\request\writer; use scormreport_objectives\privacy\provider; /** * Unit tests for the scormreport_objectives implementation of the privacy API. * * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class provider_test extends \core_privacy\tests\provider_testcase { /** * Basic setup for these tests. */ public function setUp(): void { parent::setUp(); $this->resetAfterTest(true); } /** * Ensure that export_user_preferences returns no data if the user has no data. */ public function test_export_user_preferences_not_defined(): void { $user = \core_user::get_user_by_username('admin'); provider::export_user_preferences($user->id); $writer = writer::with_context(\context_system::instance()); $this->assertFalse($writer->has_any_data()); } /** * Ensure that export_user_preferences returns single preferences. */ public function test_export_user_preferences_single(): void { // Define a user preference. $user = $this->getDataGenerator()->create_user(); $this->setUser($user); set_user_preference('scorm_report_pagesize', 50); set_user_preference('scorm_report_objectives_score', 1); // Validate exported data. provider::export_user_preferences($user->id); $context = \context_user::instance($user->id); /** @var \core_privacy\tests\request\content_writer $writer */ $writer = writer::with_context($context); $this->assertTrue($writer->has_any_data()); $prefs = $writer->get_user_preferences('scormreport_objectives'); $this->assertCount(2, (array) $prefs); $this->assertEquals( get_string('privacy:metadata:preference:scorm_report_pagesize', 'scormreport_objectives'), $prefs->scorm_report_pagesize->description ); $this->assertEquals(50, $prefs->scorm_report_pagesize->value); $this->assertEquals( get_string('privacy:metadata:preference:scorm_report_objectives_score', 'scormreport_objectives'), $prefs->scorm_report_objectives_score->description ); $this->assertEquals(get_string('yes'), $prefs->scorm_report_objectives_score->value); } } PK5\ vvuserreport.phpnu[. /** * This page displays the user data from a single attempt * * @package mod_scorm * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../../../config.php"); require_once($CFG->dirroot.'/mod/scorm/locallib.php'); $id = required_param('id', PARAM_INT); // Course Module ID. $userid = required_param('user', PARAM_INT); // User ID. $attempt = optional_param('attempt', 1, PARAM_INT); // attempt number. $mode = optional_param('mode', '', PARAM_ALPHA); // Scorm mode from which reached here. // Building the url to use for links.+ data details buildup. $url = new moodle_url('/mod/scorm/report/userreport.php', array('id' => $id, 'user' => $userid, 'attempt' => $attempt)); $tracksurl = new moodle_url('/mod/scorm/report/userreporttracks.php', array('id' => $id, 'user' => $userid, 'attempt' => $attempt, 'mode' => $mode)); $cm = get_coursemodule_from_id('scorm', $id, 0, false, MUST_EXIST); $course = get_course($cm->course); $scorm = $DB->get_record('scorm', array('id' => $cm->instance), '*', MUST_EXIST); $user = $DB->get_record('user', array('id' => $userid), implode(',', \core_user\fields::get_picture_fields()), MUST_EXIST); // Get list of attempts this user has made. $attemptids = scorm_get_all_attempts($scorm->id, $userid); $PAGE->set_url($url); $PAGE->set_secondary_active_tab('scormreport'); // END of url setting + data buildup. // Checking login +logging +getting context. require_login($course, false, $cm); $contextmodule = context_module::instance($cm->id); require_capability('mod/scorm:viewreport', $contextmodule); // Check user has group access. if (!groups_user_groups_visible($course, $userid, $cm)) { throw new moodle_exception('nopermissiontoshow'); } // Trigger a user report viewed event. $event = \mod_scorm\event\user_report_viewed::create(array( 'context' => $contextmodule, 'relateduserid' => $userid, 'other' => array('attemptid' => $attempt, 'instanceid' => $scorm->id) )); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('scorm', $scorm); $event->trigger(); // Print the page header. $strreport = get_string('report', 'scorm'); $strattempt = get_string('attempt', 'scorm'); $PAGE->set_title("$course->shortname: ".format_string($scorm->name)); $PAGE->set_heading($course->fullname); $PAGE->navbar->add($strreport, new moodle_url('/mod/scorm/report.php', array('id' => $cm->id))); $PAGE->navbar->add(fullname($user). " - $strattempt $attempt"); $PAGE->activityheader->set_attrs([ 'hidecompletion' => true, 'description' => '' ]); echo $OUTPUT->header(); // End of Print the page header. $currenttab = 'scoes'; $renderer = $PAGE->get_renderer('mod_scorm'); $useractionreport = new \mod_scorm\output\userreportsactionbar($id, $userid, $attempt, 'learning', $mode); echo $renderer->user_report_actionbar($useractionreport); // Printing user details. $output = $PAGE->get_renderer('mod_scorm'); echo $output->view_user_heading($user, $course, $PAGE->url, $attempt, $attemptids); if ($scoes = $DB->get_records('scorm_scoes', array('scorm' => $scorm->id), 'sortorder, id')) { // Print general score data. $table = new html_table(); $table->head = array( get_string('title', 'scorm'), get_string('status', 'scorm'), get_string('time', 'scorm'), get_string('score', 'scorm'), ''); $table->align = array('left', 'center', 'center', 'right', 'left'); $table->wrap = array('nowrap', 'nowrap', 'nowrap', 'nowrap', 'nowrap'); $table->width = '80%'; $table->size = array('*', '*', '*', '*', '*'); foreach ($scoes as $sco) { if ($sco->launch != '') { $row = array(); $score = ' '; if ($trackdata = scorm_get_tracks($sco->id, $userid, $attempt)) { if ($trackdata->score_raw != '') { $score = $trackdata->score_raw; } if ($trackdata->status == '') { if (!empty($trackdata->progress)) { $trackdata->status = $trackdata->progress; } else { $trackdata->status = 'notattempted'; } } $tracksurl->param('scoid', $sco->id); $detailslink = html_writer::link($tracksurl, get_string('details', 'scorm')); } else { $trackdata = new stdClass(); $trackdata->status = 'notattempted'; $trackdata->total_time = ' '; $detailslink = ' '; } $strstatus = get_string($trackdata->status, 'scorm'); $row[] = $OUTPUT->pix_icon($trackdata->status, $strstatus, 'scorm') . ' '.format_string($sco->title); $row[] = get_string($trackdata->status, 'scorm'); $row[] = scorm_format_duration($trackdata->total_time); $row[] = $score; $row[] = $detailslink; } else { $row = array(format_string($sco->title), ' ', ' ', ' ', ' '); } $table->data[] = $row; } echo html_writer::table($table); } // Print footer. echo $OUTPUT->footer(); PK5\userreportinteractions.phpnu[. /** * This page displays the user data from a single attempt * * @package mod_scorm * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../../../config.php"); require_once($CFG->dirroot.'/mod/scorm/locallib.php'); require_once($CFG->dirroot.'/mod/scorm/report/reportlib.php'); require_once($CFG->libdir . '/tablelib.php'); $id = required_param('id', PARAM_INT); // Course Module ID. $userid = required_param('user', PARAM_INT); // User ID. $attempt = optional_param('attempt', 1, PARAM_INT); // attempt number. $download = optional_param('download', '', PARAM_ALPHA); $mode = optional_param('mode', '', PARAM_ALPHA); // Scorm mode from which reached here. // Building the url to use for links.+ data details buildup. $url = new moodle_url('/mod/scorm/report/userreportinteractions.php', array('id' => $id, 'user' => $userid, 'attempt' => $attempt)); $cm = get_coursemodule_from_id('scorm', $id, 0, false, MUST_EXIST); $course = get_course($cm->course); $scorm = $DB->get_record('scorm', array('id' => $cm->instance), '*', MUST_EXIST); $user = $DB->get_record('user', array('id' => $userid), implode(',', \core_user\fields::get_picture_fields()), MUST_EXIST); // Get list of attempts this user has made. $attemptids = scorm_get_all_attempts($scorm->id, $userid); $PAGE->set_url($url); $PAGE->set_secondary_active_tab('scormreport'); // END of url setting + data buildup. // Checking login +logging +getting context. require_login($course, false, $cm); $contextmodule = context_module::instance($cm->id); require_capability('mod/scorm:viewreport', $contextmodule); // Check user has group access. if (!groups_user_groups_visible($course, $userid, $cm)) { throw new moodle_exception('nopermissiontoshow'); } // Trigger a user interactions viewed event. $event = \mod_scorm\event\interactions_viewed::create(array( 'context' => $contextmodule, 'relateduserid' => $userid, 'other' => array('attemptid' => $attempt, 'instanceid' => $scorm->id) )); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('scorm', $scorm); $event->trigger(); $sql = "SELECT a.id, a.userid, a.scormid, v.scoid, a.attempt, v.value, v.timemodified, e.element FROM {scorm_attempt} a JOIN {scorm_scoes_value} v ON v.attemptid = a.id JOIN {scorm_element} e ON e.id = v.elementid WHERE a.userid = :userid AND a.scormid = :scormid AND a.attempt = :attempt"; $trackdata = $DB->get_records_sql($sql, ['userid' => $userid, 'scormid' => $scorm->id, 'attempt' => $attempt]); $usertrack = scorm_format_interactions($trackdata); $questioncount = get_scorm_question_count($scorm->id); $courseshortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id))); $exportfilename = $courseshortname . '-' . format_string($scorm->name, true) . '-' . get_string('interactions', 'scorm'); // Set up the table. $table = new flexible_table('mod-scorm-userreport-interactions'); if (!$table->is_downloading($download, $exportfilename)) { // Print the page header. $strattempt = get_string('attempt', 'scorm'); $strreport = get_string('report', 'scorm'); $PAGE->set_title("$course->shortname: ".format_string($scorm->name)); $PAGE->set_heading($course->fullname); $PAGE->navbar->add($strreport, new moodle_url('/mod/scorm/report.php', array('id' => $cm->id))); $PAGE->navbar->add(fullname($user). " - $strattempt $attempt"); $PAGE->activityheader->set_attrs([ 'hidecompletion' => true, 'description' => '' ]); echo $OUTPUT->header(); // End of Print the page header. $currenttab = 'interactions'; $renderer = $PAGE->get_renderer('mod_scorm'); $useractionreport = new \mod_scorm\output\userreportsactionbar($id, $userid, $attempt, 'interact', $mode); echo $renderer->user_report_actionbar($useractionreport); // Printing user details. $output = $PAGE->get_renderer('mod_scorm'); echo $output->view_user_heading($user, $course, $PAGE->url, $attempt, $attemptids); } $table->define_baseurl($PAGE->url); $table->define_columns(array('id', 'studentanswer', 'correctanswer', 'result', 'calcweight')); $table->define_headers(array(get_string('trackid', 'scorm'), get_string('response', 'scorm'), get_string('rightanswer', 'scorm'), get_string('result', 'scorm'), get_string('calculatedweight', 'scorm'))); $table->set_attribute('class', 'generaltable generalbox boxaligncenter boxwidthwide'); $table->show_download_buttons_at(array(TABLE_P_BOTTOM)); $table->setup(); for ($i = 0; $i < $questioncount; $i++) { $row = array(); $element = 'cmi.interactions_'.$i.'.id'; if (isset($usertrack->$element)) { $row[] = s($usertrack->$element); $element = 'cmi.interactions_'.$i.'.student_response'; if (isset($usertrack->$element)) { $row[] = s($usertrack->$element); } else { $row[] = ' '; } $j = 0; $element = 'cmi.interactions_'.$i.'.correct_responses_'.$j.'.pattern'; $rightans = ''; if (isset($usertrack->$element)) { while (isset($usertrack->$element)) { if ($j > 0) { $rightans .= ','; } $rightans .= s($usertrack->$element); $j++; $element = 'cmi.interactions_'.$i.'.correct_responses_'.$j.'.pattern'; } $row[] = $rightans; } else { $row[] = ' '; } $element = 'cmi.interactions_'.$i.'.result'; $weighting = 'cmi.interactions_'.$i.'.weighting'; if (isset($usertrack->$element)) { $row[] = s($usertrack->$element); if ($usertrack->$element == 'correct' && isset($usertrack->$weighting)) { $row[] = s($usertrack->$weighting); } else { $row[] = '0'; } } else { $row[] = ' '; } $table->add_data($row); } } $table->finish_output(); if (!$table->is_downloading()) { echo $OUTPUT->footer(); } PK5\4%graphs/lang/en/scormreport_graphs.phpnu[. /** * Strings for component 'scorm_basic' report plugin * * @package scormreport_graphs * @copyright 2012 Ankit Kumar Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['invaliddata'] = 'Not enough data'; $string['participants'] = 'Number of participants'; $string['percent'] = 'Percent(%) secured'; $string['pluginname'] = 'Graph report'; $string['privacy:metadata'] = 'The Graph report only shows data stored in other locations.'; PK5\graphs/version.phpnu[. /** * Defines the version of scormreport_graphs * * @package scormreport_graphs * @copyright 2012 Ankit Kumar Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2025040800; // Requires this Moodle version. $plugin->component = 'scormreport_graphs'; // Full name of the plugin (used for diagnostics). PK5\? #graphs/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for scormreport_graphs. * * @package scormreport_graphs * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace scormreport_graphs\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem for scormreport_graphs implementing null_provider. * * @copyright 2018 Sara Arjona * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PK5\Koographs/classes/report.phpnu[. /** * Core Report class of graphs reporting plugin * * @package scormreport_graphs * @copyright 2012 Ankit Kumar Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace scormreport_graphs; defined('MOODLE_INTERNAL') || die(); use context_module; use core\chart_bar; use core\chart_series; use moodle_url; /** * Main class to control the graphs reporting * * @package scormreport_graphs * @copyright 2012 Ankit Kumar Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report extends \mod_scorm\report { /** Number of bars. */ const BANDS = 11; /** Range of each bar. */ const BANDWIDTH = 10; /** * Get the data for the report. * * @param int $scoid The sco ID. * @param array $allowedlist The SQL and params to get the userlist. * @return array of data indexed per bar. */ protected function get_data($scoid, $allowedlistsql) { global $DB; $data = array_fill(0, self::BANDS, 0); list($allowedlist, $params) = $allowedlistsql; $params = array_merge($params, ['scoid' => $scoid]); // Construct the SQL. $sql = "SELECT DISTINCT " . $DB->sql_concat('a.userid', '\'#\'', 'COALESCE(a.attempt, 0)') . " AS uniqueid, a.userid AS userid, a.scormid AS scormid, a.attempt AS attempt, v.scoid AS scoid FROM {scorm_attempt} a JOIN {scorm_scoes_value} v ON v.attemptid = a.id WHERE a.userid IN ({$allowedlist}) AND v.scoid = :scoid"; $attempts = $DB->get_records_sql($sql, $params); $usergrades = []; foreach ($attempts as $attempt) { if ($trackdata = scorm_get_tracks($scoid, $attempt->userid, $attempt->attempt)) { if (isset($trackdata->score_raw)) { $score = (int) $trackdata->score_raw; if (empty($trackdata->score_min)) { $minmark = 0; } else { $minmark = $trackdata->score_min; } // TODO MDL-55004: Get this value from elsewhere? if (empty($trackdata->score_max)) { $maxmark = 100; } else { $maxmark = $trackdata->score_max; } $range = ($maxmark - $minmark); if (empty($range)) { continue; } $percent = round((($score * 100) / $range), 2); if (empty($usergrades[$attempt->userid]) || !isset($usergrades[$attempt->userid]) || ($percent > $usergrades[$attempt->userid]) || ($usergrades[$attempt->userid] === '*')) { $usergrades[$attempt->userid] = $percent; } unset($percent); } else { // User has made an attempt but either SCO was not able to record the score or something else is broken in SCO. if (!isset($usergrades[$attempt->userid])) { $usergrades[$attempt->userid] = '*'; } } } } // Recording all users who attempted the SCO, but resulting data was invalid. foreach ($usergrades as $userpercent) { if ($userpercent === '*') { $data[0]++; } else { $gradeband = floor($userpercent / self::BANDWIDTH); if ($gradeband != (self::BANDS - 1)) { $gradeband++; } $data[$gradeband]++; } } return $data; } /** * Displays the full report. * * @param \stdClass $scorm full SCORM object * @param \stdClass $cm - full course_module object * @param \stdClass $course - full course object * @param string $download - type of download being requested * @return void */ public function display($scorm, $cm, $course, $download) { global $DB, $OUTPUT, $PAGE; $contextmodule = context_module::instance($cm->id); $actionbar = new \mod_scorm\output\actionbar($cm->id, false, 0); $renderer = $PAGE->get_renderer('mod_scorm'); echo $renderer->report_actionbar($actionbar); if ($groupmode = groups_get_activity_groupmode($cm)) { // Groups are being used. groups_print_activity_menu($cm, new moodle_url($PAGE->url)); } // Find out current restriction. $group = groups_get_activity_group($cm, true); $allowedlistsql = get_enrolled_sql($contextmodule, 'mod/scorm:savetrack', (int) $group); // Labels. $labels = [get_string('invaliddata', 'scormreport_graphs')]; for ($i = 1; $i <= self::BANDS - 1; $i++) { $labels[] = ($i - 1) * self::BANDWIDTH . ' - ' . $i * self::BANDWIDTH; } if ($scoes = $DB->get_records('scorm_scoes', array("scorm" => $scorm->id), 'sortorder, id')) { foreach ($scoes as $sco) { if ($sco->launch != '') { $data = $this->get_data($sco->id, $allowedlistsql); $series = new chart_series($sco->title, $data); $chart = new chart_bar(); $chart->set_labels($labels); $chart->add_series($series); $chart->get_xaxis(0, true)->set_label(get_string('percent', 'scormreport_graphs')); $yaxis = $chart->get_yaxis(0, true); $yaxis->set_label(get_string('participants', 'scormreport_graphs')); $yaxis->set_stepsize(max(1, round(max($data) / 10))); echo $OUTPUT->heading($sco->title, 3); echo $OUTPUT->render($chart); } } } } } PK5\Ĉgraphs/graph.phpnu[. /** * Core Report class of graphs reporting plugin * * @package scormreport_graphs * @copyright 2012 Ankit Kumar Agarwal * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @deprecated since Moodle 3.2 */ define('NO_DEBUG_DISPLAY', true); require_once(__DIR__ . '/../../../../config.php'); require_once($CFG->libdir . '/filelib.php'); debugging('This way of generating the chart is deprecated, refer to \\scormreport_graphs\\report::display().', DEBUG_DEVELOPER); send_file_not_found(); PK5\userreporttracks.phpnu[. /** * This page displays the user data from a single attempt * * @package mod_scorm * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../../../config.php"); require_once($CFG->dirroot.'/mod/scorm/locallib.php'); require_once($CFG->libdir.'/tablelib.php'); $id = required_param('id', PARAM_INT); // Course Module ID. $userid = required_param('user', PARAM_INT); // User ID. $scoid = required_param('scoid', PARAM_INT); // SCO ID. $mode = required_param('mode', PARAM_ALPHA); // Scorm mode. $attempt = optional_param('attempt', 1, PARAM_INT); // attempt number. $download = optional_param('download', '', PARAM_ALPHA); // Building the url to use for links.+ data details buildup. $url = new moodle_url('/mod/scorm/report/userreporttracks.php', array('id' => $id, 'user' => $userid, 'attempt' => $attempt, 'scoid' => $scoid, 'mode' => $mode)); $cm = get_coursemodule_from_id('scorm', $id, 0, false, MUST_EXIST); $course = get_course($cm->course); $scorm = $DB->get_record('scorm', array('id' => $cm->instance), '*', MUST_EXIST); $user = $DB->get_record('user', array('id' => $userid), implode(',', \core_user\fields::get_picture_fields()), MUST_EXIST); $selsco = $DB->get_record('scorm_scoes', array('id' => $scoid), '*', MUST_EXIST); $PAGE->set_url($url); // END of url setting + data buildup. // Checking login +logging +getting context. require_login($course, false, $cm); $contextmodule = context_module::instance($cm->id); require_capability('mod/scorm:viewreport', $contextmodule); // Check user has group access. if (!groups_user_groups_visible($course, $userid, $cm)) { throw new moodle_exception('nopermissiontoshow'); } // Trigger a tracks viewed event. $event = \mod_scorm\event\tracks_viewed::create(array( 'context' => $contextmodule, 'relateduserid' => $userid, 'other' => array('attemptid' => $attempt, 'instanceid' => $scorm->id, 'scoid' => $scoid, 'mode' => $mode) )); $event->add_record_snapshot('course_modules', $cm); $event->add_record_snapshot('scorm', $scorm); $event->trigger(); // Print the page header. $strreport = get_string('report', 'scorm'); $strattempt = get_string('attempt', 'scorm'); $PAGE->set_title("$course->shortname: ".format_string($scorm->name)); $PAGE->set_heading($course->fullname); $PAGE->navbar->add($strreport, new moodle_url('/mod/scorm/report.php', array('id' => $cm->id))); $PAGE->navbar->add("$strattempt $attempt - ".fullname($user), new moodle_url('/mod/scorm/report/userreport.php', array('id' => $id, 'user' => $userid, 'attempt' => $attempt))); $PAGE->navbar->add($selsco->title . ' - '. get_string('details', 'scorm')); $PAGE->set_secondary_active_tab('scormreport'); if ($trackdata = scorm_get_tracks($selsco->id, $userid, $attempt)) { if ($trackdata->status == '') { $trackdata->status = 'notattempted'; } } else { $trackdata = new stdClass(); $trackdata->status = 'notattempted'; $trackdata->total_time = ''; } $courseshortname = format_string($course->shortname, true, array('context' => context_course::instance($course->id))); $exportfilename = $courseshortname . '-' . format_string($scorm->name, true) . '-' . get_string('details', 'scorm'); $table = new flexible_table('mod_scorm-userreporttracks'); if (!$table->is_downloading($download, $exportfilename)) { $PAGE->activityheader->set_attrs([ 'hidecompletion' => true, 'description' => '' ]); echo $OUTPUT->header(); $currenttab = ''; $renderer = $PAGE->get_renderer('mod_scorm'); $useractionreport = new \mod_scorm\output\userreportsactionbar($id, $userid, $attempt, 'attempt', $mode, $scoid); echo $renderer->user_report_actionbar($useractionreport); echo $OUTPUT->box_start('generalbox boxaligncenter'); echo $OUTPUT->heading("$strattempt $attempt - ". fullname($user).': '. format_string($selsco->title). ' - '. get_string('details', 'scorm'), 3); } $table->define_baseurl($PAGE->url); $table->define_columns(array('element', 'value')); $table->define_headers(array(get_string('element', 'scorm'), get_string('value', 'scorm'))); $table->set_attribute('class', 'generaltable generalbox boxaligncenter scormtrackreport'); $table->show_download_buttons_at(array(TABLE_P_BOTTOM)); $table->setup(); foreach ($trackdata as $element => $value) { if (substr($element, 0, 3) == 'cmi') { $existelements = true; $row = array(); $string = false; if (stristr($element, '.id') !== false) { $string = "trackid"; } else if (stristr($element, '.result') !== false) { $string = "trackresult"; } else if (stristr($element, '.student_response') !== false or // SCORM 1.2 value. stristr($element, '.learner_response') !== false) { // SCORM 2004 value. $string = "trackresponse"; } else if (stristr($element, '.type') !== false) { $string = "tracktype"; } else if (stristr($element, '.weighting') !== false) { $string = "trackweight"; } else if (stristr($element, '.time') !== false) { $string = "tracktime"; } else if (stristr($element, '.correct_responses._count') !== false) { $string = "trackcorrectcount"; } else if (stristr($element, '.score.min') !== false) { $string = "trackscoremin"; } else if (stristr($element, '.score.max') !== false) { $string = "trackscoremax"; } else if (stristr($element, '.score.raw') !== false) { $string = "trackscoreraw"; } else if (stristr($element, '.latency') !== false) { $string = "tracklatency"; } else if (stristr($element, '.pattern') !== false) { $string = "trackpattern"; } else if (stristr($element, '.suspend_data') !== false) { $string = "tracksuspenddata"; } if (empty($string) || $table->is_downloading()) { $row[] = s($element); } else { $row[] = s($element) . $OUTPUT->help_icon($string, 'scorm'); } if (strpos($element, '_time') === false) { $row[] = s($value); } else { $row[] = s(scorm_format_duration($value)); } $table->add_data($row); } } $table->finish_output(); if (!$table->is_downloading()) { echo $OUTPUT->box_end(); echo $OUTPUT->footer(); } PKW\J J summary/renderer.phpnu[. /** * Provides rendering functionality for the forum summary report subplugin. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); use forumreport_summary\summary_table; /** * Renderer for the forum summary report. * * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class forumreport_summary_renderer extends plugin_renderer_base { /** * Render the filters available for the forum summary report. * * @param stdClass $course The course object. * @param array $cms Array of course module objects. * @param moodle_url $actionurl The form action URL. * @param array $filters Optional array of currently applied filter values. * @return string The filter form HTML. */ public function render_filters_form(stdClass $course, array $cms, moodle_url $actionurl, array $filters = []): string { $renderable = new \forumreport_summary\output\filters($course, $cms, $actionurl, $filters); $templatecontext = $renderable->export_for_template($this); return $this->render_from_template('forumreport_summary/filters', $templatecontext); } /** * Render the summary report table. * * @param summary_table $table The summary table to be rendered. * @return string The report table HTML. */ public function render_summary_table(summary_table $table): string { // Buffer so calling script can output the report as required. ob_start(); // Render table. $table->out($table->get_perpage(), false); $tablehtml = ob_get_contents(); ob_end_clean(); return $this->render_from_template('forumreport_summary/report', ['tablehtml' => $tablehtml]); } } PKW\SSC  summary/db/access.phpnu[. /** * Forum summary report subplugin capabilities. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $capabilities = [ 'forumreport/summary:view' => [ 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ], ], 'forumreport/summary:viewall' => [ 'riskbitmask' => RISK_PERSONAL, 'captype' => 'read', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => [ 'teacher' => CAP_ALLOW, 'editingteacher' => CAP_ALLOW, 'manager' => CAP_ALLOW ], ], ]; PKW\fǢ9x x 'summary/lang/en/forumreport_summary.phpnu[. /** * Strings for the Forum report subplugin. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ $string['attachmentcount'] = 'Number of attachments'; $string['charcount'] = 'Character count'; $string['viewcount'] = 'Number of views'; $string['earliestpost'] = 'Earliest post'; $string['eventreportdownloaded'] = 'Forum summary report downloaded'; $string['eventreportviewed'] = 'Forum summary report viewed'; $string['filter:datesbuttonlabel'] = 'Open the dates filter'; $string['filter:datesname'] = 'Dates'; $string['filter:datesfrom'] = 'From {$a}'; $string['filter:datesfromto'] = '{$a->datefrom} - {$a->dateto}'; $string['filter:datesorderwarning'] = 'The from date can\'t be earlier than the to date.'; $string['filter:datesto'] = 'To {$a}'; $string['filter:groupsbuttonlabel'] = 'Open the groups filter'; $string['filter:groupsname'] = 'Groups'; $string['filter:groupscountall'] = 'Groups (all)'; $string['filter:groupscountnumber'] = 'Groups ({$a})'; $string['forumselectlabel'] = 'Forum selected'; $string['forumselectcourseoption'] = 'All forums in course'; $string['latestpost'] = 'Most recent post'; $string['exportposts'] = 'Export posts'; $string['exportpostslabel'] = 'Export posts for {$a}'; $string['nodetitle'] = 'Forum summary report'; $string['pluginname'] = 'Forum summary report'; $string['postcount'] = 'Number of discussions posted'; $string['privacy:metadata'] = 'The Forum summary report plugin does not store any personal data.'; $string['replycount'] = 'Number of replies posted'; $string['summary:viewall'] = 'Access summary report data for each user within a given forum or forums'; $string['summary:view'] = 'Access summary report within a given forum or forums'; $string['summarytitle'] = 'Forum summary report - {$a}'; $string['viewsdisclaimer'] = 'Number of views column is not filtered by group'; $string['wordcount'] = 'Word count'; PKW\V.++summary/version.phpnu[. /** * Defines the version of the forum report subplugin. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); $plugin->version = 2025041400; $plugin->requires = 2025040800; $plugin->component = 'forumreport_summary'; PKW\EE*summary/classes/form/dates_filter_form.phpnu[. /** * The mform used by the forum summary report dates filter. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary\form; defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot.'/lib/formslib.php'); /** * The mform class for creating the forum summary report dates filter. * * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class dates_filter_form extends \moodleform { /** * The form definition. * */ public function definition() { $attributes = [ 'class' => 'align-items-center', ]; // From date field. $this->_form->addElement('date_selector', 'filterdatefrompopover', get_string('fromdate'), ['optional' => true], $attributes); // To date field. $this->_form->addElement('date_selector', 'filterdatetopopover', get_string('todate'), ['optional' => true], $attributes); } } PKW\::$summary/classes/privacy/provider.phpnu[. /** * Privacy Subsystem implementation for forumreport_summary subplugin. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary\privacy; defined('MOODLE_INTERNAL') || die(); /** * Privacy Subsystem implementation for forumreport_summary subplugin, implementing null_provider. * * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class provider implements \core_privacy\local\metadata\null_provider { /** * Get the language string identifier with the component's language * file to explain why this plugin stores no data. * * @return string */ public static function get_reason(): string { return 'privacy:metadata'; } } PKW\BEE!summary/classes/summary_table.phpnu[. /** * The class for displaying the forum report table. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary; defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/tablelib.php'); use coding_exception; use table_sql; /** * The class for displaying the forum report table. * * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class summary_table extends table_sql { /** Forum filter type */ const FILTER_FORUM = 1; /** Groups filter type */ const FILTER_GROUPS = 2; /** Dates filter type */ const FILTER_DATES = 3; /** Table to store summary data extracted from the log table */ const LOG_SUMMARY_TEMP_TABLE = 'forum_report_summary_counts'; /** Default number of rows to display per page */ const DEFAULT_PER_PAGE = 50; /** @var \stdClass The various SQL segments that will be combined to form queries to fetch various information. */ public $sql; /** @var int The number of rows to be displayed per page. */ protected $perpage = self::DEFAULT_PER_PAGE; /** @var array The values available for pagination size per page. */ protected $perpageoptions = [50, 100, 200]; /** @var int The course ID containing the forum(s) being reported on. */ protected $courseid; /** @var bool True if reporting on all forums in course user has access to, false if reporting on a single forum */ protected $iscoursereport = false; /** @var bool True if user has access to all forums in the course (and is running course report), otherwise false. */ protected $accessallforums = false; /** @var \stdClass The course module object(s) of the forum(s) being reported on. */ protected $cms = []; /** * @var int The user ID if only one user's summary will be generated. * This will apply to users without permission to view others' summaries. */ protected $userid; /** * @var \core\log\sql_reader|null */ protected $logreader = null; /** * @var array of \context objects for the forums included in the report. */ protected $forumcontexts = []; /** * @var context_course|context_module The context where the report is being run (either a specific forum or the course). */ protected $userfieldscontext = null; /** @var bool Whether the user has the capability/capabilities to perform bulk operations. */ protected $allowbulkoperations = false; /** * @var bool */ private $showwordcharcounts = null; /** * @var bool Whether the user can see all private replies or not. */ protected $canseeprivatereplies; /** * @var array Validated filter data, for use in GET parameters by export links. */ protected $exportfilterdata = []; /** * Forum report table constructor. * * @param int $courseid The ID of the course the forum(s) exist within. * @param array $filters Report filters in the format 'type' => [values]. * @param bool $allowbulkoperations Is the user allowed to perform bulk operations? * @param bool $canseeprivatereplies Whether the user can see all private replies or not. * @param int $perpage The number of rows to display per page. * @param bool $canexport Is the user allowed to export records? * @param bool $iscoursereport Whether the user is running a course level report * @param bool $accessallforums If user is running a course level report, do they have access to all forums in the course? */ public function __construct(int $courseid, array $filters, bool $allowbulkoperations, bool $canseeprivatereplies, int $perpage, bool $canexport, bool $iscoursereport, bool $accessallforums) { global $OUTPUT; $uniqueid = $courseid . ($iscoursereport ? '' : '_' . $filters['forums'][0]); parent::__construct("summaryreport_{$uniqueid}"); $this->courseid = $courseid; $this->iscoursereport = $iscoursereport; $this->accessallforums = $accessallforums; $this->allowbulkoperations = $allowbulkoperations; $this->canseeprivatereplies = $canseeprivatereplies; $this->perpage = $perpage; $this->set_forum_properties($filters['forums']); $columnheaders = []; if ($allowbulkoperations) { $mastercheckbox = new \core\output\checkbox_toggleall('summaryreport-table', true, [ 'id' => 'select-all-users', 'name' => 'select-all-users', 'label' => get_string('selectall'), 'labelclasses' => 'visually-hidden', 'classes' => 'm-1', 'checked' => false ]); $columnheaders['select'] = $OUTPUT->render($mastercheckbox); } $columnheaders += [ 'fullname' => get_string('fullnameuser'), 'postcount' => get_string('postcount', 'forumreport_summary'), 'replycount' => get_string('replycount', 'forumreport_summary'), 'attachmentcount' => get_string('attachmentcount', 'forumreport_summary'), ]; $this->logreader = $this->get_internal_log_reader(); if ($this->logreader) { $columnheaders['viewcount'] = get_string('viewcount', 'forumreport_summary'); } if ($this->show_word_char_counts()) { $columnheaders['wordcount'] = get_string('wordcount', 'forumreport_summary'); $columnheaders['charcount'] = get_string('charcount', 'forumreport_summary'); } $columnheaders['earliestpost'] = get_string('earliestpost', 'forumreport_summary'); $columnheaders['latestpost'] = get_string('latestpost', 'forumreport_summary'); if ($canexport) { $columnheaders['export'] = get_string('exportposts', 'forumreport_summary'); } $this->define_columns(array_keys($columnheaders)); $this->define_headers(array_values($columnheaders)); // Define configs. $this->define_table_configs(); // Apply relevant filters. $this->define_base_filter_sql(); $this->apply_filters($filters); // Define the basic SQL data and object format. $this->define_base_sql(); } /** * Sets properties that are determined by forum filter values. * * @param array $forumids The forum IDs passed in by the filter. * @return void */ protected function set_forum_properties(array $forumids): void { global $USER; // Course context if reporting on all forums in the course the user has access to. if ($this->iscoursereport) { $this->userfieldscontext = \context_course::instance($this->courseid); } foreach ($forumids as $forumid) { $cm = get_coursemodule_from_instance('forum', $forumid, $this->courseid); $this->cms[] = $cm; $this->forumcontexts[$cm->id] = \context_module::instance($cm->id); // Set forum context if not reporting on course. if (!isset($this->userfieldscontext)) { $this->userfieldscontext = $this->forumcontexts[$cm->id]; } // Only show own summary unless they have permission to view all in every forum being reported. if (empty($this->userid) && !has_capability('forumreport/summary:viewall', $this->forumcontexts[$cm->id])) { $this->userid = $USER->id; } } } /** * Provides the string name of each filter type, to be used by errors. * Note: This does not use language strings as the value is injected into error strings. * * @param int $filtertype Type of filter * @return string Name of the filter */ protected function get_filter_name(int $filtertype): string { $filternames = [ self::FILTER_FORUM => 'Forum', self::FILTER_GROUPS => 'Groups', self::FILTER_DATES => 'Dates', ]; return $filternames[$filtertype]; } /** * Generate the select column. * * @param \stdClass $data * @return string */ public function col_select($data) { global $OUTPUT; $checkbox = new \core\output\checkbox_toggleall('summaryreport-table', false, [ 'classes' => 'usercheckbox m-1', 'id' => 'user' . $data->userid, 'name' => 'user' . $data->userid, 'checked' => false, 'label' => get_string('selectitem', 'moodle', fullname($data)), 'labelclasses' => 'accesshide', ]); return $OUTPUT->render($checkbox); } /** * Generate the fullname column. * * @param \stdClass $data The row data. * @return string User's full name. */ public function col_fullname($data): string { if ($this->is_downloading()) { return fullname($data); } global $OUTPUT; return $OUTPUT->user_picture($data, array('courseid' => $this->courseid, 'includefullname' => true)); } /** * Generate the postcount column. * * @param \stdClass $data The row data. * @return int number of discussion posts made by user. */ public function col_postcount(\stdClass $data): int { return $data->postcount; } /** * Generate the replycount column. * * @param \stdClass $data The row data. * @return int number of replies made by user. */ public function col_replycount(\stdClass $data): int { return $data->replycount; } /** * Generate the attachmentcount column. * * @param \stdClass $data The row data. * @return int number of files attached to posts by user. */ public function col_attachmentcount(\stdClass $data): int { return $data->attachmentcount; } /** * Generate the earliestpost column. * * @param \stdClass $data The row data. * @return string Timestamp of user's earliest post, or a dash if no posts exist. */ public function col_earliestpost(\stdClass $data): string { global $USER; return empty($data->earliestpost) ? '-' : userdate($data->earliestpost, "", \core_date::get_user_timezone($USER)); } /** * Generate the latestpost column. * * @param \stdClass $data The row data. * @return string Timestamp of user's most recent post, or a dash if no posts exist. */ public function col_latestpost(\stdClass $data): string { global $USER; return empty($data->latestpost) ? '-' : userdate($data->latestpost, "", \core_date::get_user_timezone($USER)); } /** * Generate the export column. * * @param \stdClass $data The row data. * @return string The link to export content belonging to the row. */ public function col_export(\stdClass $data): string { global $OUTPUT; // If no posts, nothing to export. if (empty($data->earliestpost)) { return ''; } $params = [ 'id' => $this->cms[0]->instance, // Forum id. 'userids[]' => $data->userid, // User id. ]; // Add relevant filter params. foreach ($this->exportfilterdata as $name => $filterdata) { if (is_array($filterdata)) { foreach ($filterdata as $key => $value) { $params["{$name}[{$key}]"] = $value; } } else { $params[$name] = $filterdata; } } $buttoncontext = [ 'url' => new \moodle_url('/mod/forum/export.php', $params), 'label' => get_string('exportpostslabel', 'forumreport_summary', fullname($data)), ]; return $OUTPUT->render_from_template('forumreport_summary/export_link_button', $buttoncontext); } /** * Override the default implementation to set a notification. * * @return void. */ public function print_nothing_to_display(): void { global $OUTPUT; echo $OUTPUT->notification(get_string('nothingtodisplay'), 'info', false); } /** * Query the db. Store results in the table object for use by build_table. * * @param int $pagesize Size of page for paginated displayed table. * @param bool $useinitialsbar Overridden but unused. * @return void */ public function query_db($pagesize, $useinitialsbar = false): void { global $DB; // Set up pagination if not downloading the whole report. if (!$this->is_downloading()) { $totalsql = $this->get_full_sql(false); // Set up pagination. $totalrows = $DB->count_records_sql($totalsql, $this->sql->params); $this->pagesize($pagesize, $totalrows); } // Fetch the data. $sql = $this->get_full_sql(); // Only paginate when not downloading. if (!$this->is_downloading()) { $this->rawdata = $DB->get_records_sql($sql, $this->sql->params, $this->get_page_start(), $this->get_page_size()); } else { $this->rawdata = $DB->get_records_sql($sql, $this->sql->params); } } /** * Adds the relevant SQL to apply a filter to the report. * * @param int $filtertype Filter type as defined by class constants. * @param array $values Optional array of values passed into the filter type. * @return void * @throws coding_exception */ public function add_filter(int $filtertype, array $values = []): void { global $DB; $paramcounterror = false; switch($filtertype) { case self::FILTER_FORUM: // Requires at least one forum ID. if (empty($values)) { $paramcounterror = true; } else { // No select fields required - displayed in title. // No extra joins required, forum is already joined. list($forumidin, $forumidparams) = $DB->get_in_or_equal($values, SQL_PARAMS_NAMED); $this->sql->filterwhere .= " AND f.id {$forumidin}"; $this->sql->params += $forumidparams; } break; case self::FILTER_GROUPS: // Filter data to only include content within specified groups (and/or no groups). // Additionally, only display users who can post within the selected option(s). // Only filter by groups the user has access to. $groups = $this->get_filter_groups($values); // Skip adding filter if not applied, or all valid options are selected. if (!empty($groups)) { list($groupidin, $groupidparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); // Posts within selected groups and/or not in any groups (group ID -1) are included. // No user filtering as anyone enrolled can potentially post to unrestricted discussions. if (array_search(-1, $groups) !== false) { $this->sql->filterwhere .= " AND d.groupid {$groupidin}"; $this->sql->params += $groupidparams; } else { // Only posts and users within selected groups are included. list($groupusersin, $groupusersparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED); // No joins required (handled by where to prevent data duplication). $this->sql->filterwhere .= " AND u.id IN ( SELECT gm.userid FROM {groups_members} gm WHERE gm.groupid {$groupusersin} ) AND d.groupid {$groupidin}"; $this->sql->params += $groupusersparams + $groupidparams; } } break; case self::FILTER_DATES: if (!isset($values['from']['enabled']) || !isset($values['to']['enabled']) || ($values['from']['enabled'] && !isset($values['from']['timestamp'])) || ($values['to']['enabled'] && !isset($values['to']['timestamp']))) { $paramcounterror = true; } else { $this->sql->filterbase['dates'] = ''; $this->sql->filterbase['dateslog'] = ''; $this->sql->filterbase['dateslogparams'] = []; // From date. if ($values['from']['enabled']) { // If the filter was enabled, include the date restriction. // Needs to form part of the base join to posts, so will be injected by define_base_sql(). $this->sql->filterbase['dates'] .= " AND p.created >= :fromdate"; $this->sql->params['fromdate'] = $values['from']['timestamp']; $this->sql->filterbase['dateslog'] .= ' AND timecreated >= :fromdate'; $this->sql->filterbase['dateslogparams']['fromdate'] = $values['from']['timestamp']; $this->exportfilterdata['timestampfrom'] = $values['from']['timestamp']; } // To date. if ($values['to']['enabled']) { // If the filter was enabled, include the date restriction. // Needs to form part of the base join to posts, so will be injected by define_base_sql(). $this->sql->filterbase['dates'] .= " AND p.created <= :todate"; $this->sql->params['todate'] = $values['to']['timestamp']; $this->sql->filterbase['dateslog'] .= ' AND timecreated <= :todate'; $this->sql->filterbase['dateslogparams']['todate'] = $values['to']['timestamp']; $this->exportfilterdata['timestampto'] = $values['to']['timestamp']; } } break; default: throw new coding_exception("Report filter type '{$filtertype}' not found."); break; } if ($paramcounterror) { $filtername = $this->get_filter_name($filtertype); throw new coding_exception("An invalid number of values have been passed for the '{$filtername}' filter."); } } /** * Define various table config options. * * @return void. */ protected function define_table_configs(): void { $this->collapsible(false); $this->sortable(true, 'firstname', SORT_ASC); $this->pageable(true); $this->is_downloadable(true); $this->no_sorting('select'); $this->no_sorting('export'); $this->set_attribute('id', 'forumreport_summary_table'); $this->sql = new \stdClass(); $this->sql->params = []; } /** * Define the object to store all for the table SQL and initialises the base SQL required. * * @return void. */ protected function define_base_sql(): void { global $USER; // TODO Does not support custom user profile fields (MDL-70456). $userfieldsapi = \core_user\fields::for_identity($this->userfieldscontext, false)->with_userpic(); $userfieldssql = $userfieldsapi->get_sql('u', false, '', '', false)->selects; // Define base SQL query format. $this->sql->basefields = ' u.id AS userid, d.course AS courseid, SUM(CASE WHEN p.parent = 0 THEN 1 ELSE 0 END) AS postcount, SUM(CASE WHEN p.parent != 0 THEN 1 ELSE 0 END) AS replycount, ' . $userfieldssql . ', SUM(CASE WHEN att.attcount IS NULL THEN 0 ELSE att.attcount END) AS attachmentcount, MIN(p.created) AS earliestpost, MAX(p.created) AS latestpost'; // Handle private replies. $privaterepliessql = ''; $privaterepliesparams = []; if (!$this->canseeprivatereplies) { $privaterepliessql = ' AND (p.privatereplyto = :privatereplyto OR p.userid = :privatereplyfrom OR p.privatereplyto = 0)'; $privaterepliesparams['privatereplyto'] = $USER->id; $privaterepliesparams['privatereplyfrom'] = $USER->id; } if ($this->iscoursereport) { $course = get_course($this->courseid); $groupmode = groups_get_course_groupmode($course); } else { $cm = \cm_info::create($this->cms[0]); $groupmode = $cm->effectivegroupmode; } if ($groupmode == SEPARATEGROUPS && !has_capability('moodle/site:accessallgroups', $this->get_context())) { $groups = groups_get_all_groups($this->courseid, $USER->id, 0, 'g.id'); $groupids = array_column($groups, 'id'); } else { $groupids = []; } [$enrolleduserssql, $enrolledusersparams] = get_enrolled_sql($this->get_context(), '', $groupids); $this->sql->params += $enrolledusersparams; $queryattachments = 'SELECT COUNT(fi.id) AS attcount, fi.itemid AS postid, fi.userid FROM {files} fi WHERE fi.component = :component AND fi.filesize > 0 GROUP BY fi.itemid, fi.userid'; $this->sql->basefromjoins = ' {user} u JOIN (' . $enrolleduserssql . ') enrolledusers ON enrolledusers.id = u.id JOIN {forum} f ON f.course = :forumcourseid JOIN {forum_discussions} d ON d.forum = f.id LEFT JOIN {forum_posts} p ON p.discussion = d.id AND p.userid = u.id ' . $privaterepliessql . $this->sql->filterbase['dates'] . ' LEFT JOIN (' . $queryattachments . ') att ON att.postid = p.id AND att.userid = u.id'; $this->sql->basewhere = '1 = 1'; $this->sql->basegroupby = "$userfieldssql, d.course"; if ($this->logreader) { $this->fill_log_summary_temp_table(); $this->sql->basefields .= ', CASE WHEN tmp.viewcount IS NOT NULL THEN tmp.viewcount ELSE 0 END AS viewcount'; $this->sql->basefromjoins .= ' LEFT JOIN {' . self::LOG_SUMMARY_TEMP_TABLE . '} tmp ON tmp.userid = u.id '; $this->sql->basegroupby .= ', tmp.viewcount'; } if ($this->show_word_char_counts()) { // All p.wordcount values should be NOT NULL, this CASE WHEN is an extra just-in-case. $this->sql->basefields .= ', SUM(CASE WHEN p.wordcount IS NOT NULL THEN p.wordcount ELSE 0 END) AS wordcount'; $this->sql->basefields .= ', SUM(CASE WHEN p.charcount IS NOT NULL THEN p.charcount ELSE 0 END) AS charcount'; } $this->sql->params += [ 'component' => 'mod_forum', 'forumcourseid' => $this->courseid, ] + $privaterepliesparams; // Handle if a user is limited to viewing their own summary. if (!empty($this->userid)) { $this->sql->basewhere .= ' AND u.id = :userid'; $this->sql->params['userid'] = $this->userid; } } /** * Instantiate the properties to store filter values. * * @return void. */ protected function define_base_filter_sql(): void { // Filter values will be populated separately where required. $this->sql->filterfields = ''; $this->sql->filterfromjoins = ''; $this->sql->filterwhere = ''; $this->sql->filtergroupby = ''; } /** * Overriding the parent method because it should not be used here. * Filters are applied, so the structure of $this->sql is now different to the way this is set up in the parent. * * @param string $fields Unused. * @param string $from Unused. * @param string $where Unused. * @param array $params Unused. * @return void. * * @throws coding_exception */ public function set_sql($fields, $from, $where, array $params = []) { throw new coding_exception('The set_sql method should not be used by the summary_table class.'); } /** * Convenience method to call a number of methods for you to display the table. * Overrides the parent so SQL for filters is handled. * * @param int $pagesize Number of rows to fetch. * @param bool $useinitialsbar Whether to include the initials bar with the table. * @param string $downloadhelpbutton Unused. * * @return void. */ public function out($pagesize, $useinitialsbar, $downloadhelpbutton = ''): void { global $DB; if (!$this->columns) { $sql = $this->get_full_sql(); $onerow = $DB->get_record_sql($sql, $this->sql->params, IGNORE_MULTIPLE); // If columns is not set, define columns as the keys of the rows returned from the db. $this->define_columns(array_keys((array)$onerow)); $this->define_headers(array_keys((array)$onerow)); } $this->setup(); $this->query_db($pagesize, $useinitialsbar); $this->build_table(); $this->close_recordset(); $this->finish_output(); // Drop the temp table when necessary. if ($this->logreader) { $this->drop_log_summary_temp_table(); } } /** * Apply the relevant filters to the report. * * @param array $filters Report filters in the format 'type' => [values]. * @return void. */ protected function apply_filters(array $filters): void { // Apply the forums filter if not reporting on every forum in a course. if (!$this->accessallforums) { $this->add_filter(self::FILTER_FORUM, $filters['forums']); } // Apply groups filter. $this->add_filter(self::FILTER_GROUPS, $filters['groups']); // Apply dates filter. $datevalues = [ 'from' => $filters['datefrom'], 'to' => $filters['dateto'], ]; $this->add_filter(self::FILTER_DATES, $datevalues); } /** * Prepares a complete SQL statement from the base query and any filters defined. * * @param bool $fullselect Whether to select all relevant columns. * False selects a count only (used to calculate pagination). * @return string The complete SQL statement. */ protected function get_full_sql(bool $fullselect = true): string { $groupby = ''; $orderby = ''; if ($fullselect) { $selectfields = "{$this->sql->basefields} {$this->sql->filterfields}"; $groupby = ' GROUP BY ' . $this->sql->basegroupby . $this->sql->filtergroupby; if ($sort = $this->get_sql_sort()) { $orderby = " ORDER BY {$sort}"; } } else { $selectfields = 'COUNT(DISTINCT u.id)'; } $sql = "SELECT {$selectfields} FROM {$this->sql->basefromjoins} {$this->sql->filterfromjoins} WHERE {$this->sql->basewhere} {$this->sql->filterwhere} {$groupby} {$orderby}"; return $sql; } /** * Returns an internal and enabled log reader. * * @return \core\log\sql_reader|false */ protected function get_internal_log_reader(): ?\core\log\sql_reader { global $DB; $readers = get_log_manager()->get_readers('core\log\sql_reader'); foreach ($readers as $reader) { // If reader is not a sql_internal_table_reader and not legacy store then return. if (!($reader instanceof \core\log\sql_internal_table_reader)) { continue; } $logreader = $reader; } if (empty($logreader)) { return null; } return $logreader; } /** * Fills the log summary temp table. * * @return null */ protected function fill_log_summary_temp_table() { global $DB; $this->create_log_summary_temp_table(); $logtable = $this->logreader->get_internal_log_table_name(); $nonanonymous = 'AND anonymous = 0'; // Apply dates filter if applied. $datewhere = $this->sql->filterbase['dateslog'] ?? ''; $dateparams = $this->sql->filterbase['dateslogparams'] ?? []; $contextids = []; foreach ($this->forumcontexts as $forumcontext) { $contextids[] = $forumcontext->id; } list($contextidin, $contextidparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED); $params = $contextidparams + $dateparams; $sql = "INSERT INTO {" . self::LOG_SUMMARY_TEMP_TABLE . "} (userid, viewcount) SELECT userid, COUNT(*) AS viewcount FROM {" . $logtable . "} WHERE contextid {$contextidin} $datewhere $nonanonymous GROUP BY userid"; $DB->execute($sql, $params); } /** * Creates a temp table to store summary data from the log table for this request. * * @return null */ protected function create_log_summary_temp_table() { global $DB; $dbman = $DB->get_manager(); $temptablename = self::LOG_SUMMARY_TEMP_TABLE; $xmldbtable = new \xmldb_table($temptablename); $xmldbtable->add_field('userid', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null); $xmldbtable->add_field('viewcount', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, null, null); $xmldbtable->add_key('primary', XMLDB_KEY_PRIMARY, array('userid')); $dbman->create_temp_table($xmldbtable); } /** * Drops the temp table. * * This should be called once the processing for the summary table has been done. */ protected function drop_log_summary_temp_table(): void { global $DB; // Drop the temp table if it exists. $temptable = new \xmldb_table(self::LOG_SUMMARY_TEMP_TABLE); $dbman = $DB->get_manager(); if ($dbman->table_exists($temptable)) { $dbman->drop_table($temptable); } } /** * Get the final list of groups to filter by, based on the groups submitted, * and those the user has access to. * * * @param array $groups The group IDs submitted. * @return array Group objects of groups to use in groups filter. * If no filtering required (all groups selected), returns []. */ protected function get_filter_groups(array $groups): array { global $USER; $usergroups = groups_get_all_groups($this->courseid, $USER->id); $coursegroupsobj = groups_get_all_groups($this->courseid); $allgroups = false; $allowedgroupsobj = []; $allowedgroups = []; $filtergroups = []; foreach ($this->cms as $cm) { // Only need to check for all groups access if not confirmed by a previous check. if (!$allgroups) { $groupmode = groups_get_activity_groupmode($cm); // If no groups mode enabled on the forum, nothing to prepare. if (!in_array($groupmode, [VISIBLEGROUPS, SEPARATEGROUPS])) { continue; } $aag = has_capability('moodle/site:accessallgroups', $this->forumcontexts[$cm->id]); if ($groupmode == VISIBLEGROUPS || $aag) { $allgroups = true; // All groups in course fetched, no need to continue checking for others. break; } } } if ($allgroups) { $nogroups = new \stdClass(); $nogroups->id = -1; $nogroups->name = get_string('groupsnone'); // Any groups and no groups. $allowedgroupsobj = $coursegroupsobj + [$nogroups]; } else { $allowedgroupsobj = $usergroups; } foreach ($allowedgroupsobj as $group) { $allowedgroups[] = $group->id; } // If not all groups in course are selected, filter by allowed groups submitted. if (!empty($groups)) { if (!empty(array_diff($allowedgroups, $groups))) { $filtergroups = array_intersect($groups, $allowedgroups); } else { $coursegroups = []; foreach ($coursegroupsobj as $group) { $coursegroups[] = $group->id; } // If user's 'all groups' is a subset of the course groups, filter by all groups available to them. if (!empty(array_diff($coursegroups, $allowedgroups))) { $filtergroups = $allowedgroups; } } } return $filtergroups; } /** * Download the summary report in the selected format. * * @param string $format The format to download the report. */ public function download($format) { $filename = 'summary_report_' . userdate(time(), get_string('backupnameformat', 'langconfig'), 99, false); $this->is_downloading($format, $filename); $this->out($this->perpage, false); } /* * Should the word / char counts be displayed? * * We don't want to show word/char columns if there is any null value because this means * that they have not been calculated yet. * @return bool */ protected function show_word_char_counts(): bool { global $DB; if (is_null($this->showwordcharcounts)) { $forumids = []; foreach ($this->cms as $cm) { $forumids[] = $cm->instance; } list($forumidin, $forumidparams) = $DB->get_in_or_equal($forumids, SQL_PARAMS_NAMED); // This should be really fast. $sql = "SELECT 'x' FROM {forum_posts} fp JOIN {forum_discussions} fd ON fd.id = fp.discussion WHERE fd.forum {$forumidin} AND (fp.wordcount IS NULL OR fp.charcount IS NULL)"; if ($DB->record_exists_sql($sql, $forumidparams)) { $this->showwordcharcounts = false; } else { $this->showwordcharcounts = true; } } return $this->showwordcharcounts; } /** * Fetch the number of items to be displayed per page. * * @return int */ public function get_perpage(): int { return $this->perpage; } /** * Overriding method to render the bulk actions and items per page pagination options directly below the table. * * @return void */ public function wrap_html_finish(): void { global $OUTPUT; $data = new \stdClass(); $data->showbulkactions = $this->allowbulkoperations; if ($data->showbulkactions) { $data->id = 'formactionid'; $data->attributes = [ [ 'name' => 'data-action', 'value' => 'toggle' ], [ 'name' => 'data-togglegroup', 'value' => 'summaryreport-table' ], [ 'name' => 'data-toggle', 'value' => 'action' ], [ 'name' => 'disabled', 'value' => true ] ]; $data->actions = [ [ 'value' => '#messageselect', 'name' => get_string('messageselectadd') ] ]; } // Include the pagination size selector. $perpageoptions = array_combine($this->perpageoptions, $this->perpageoptions); $selected = in_array($this->perpage, $this->perpageoptions) ? $this->perpage : $this->perpageoptions[0]; $perpageselect = new \single_select(new \moodle_url(''), 'perpage', $perpageoptions, $selected, null, 'selectperpage'); $perpageselect->set_label(get_string('perpage', 'moodle')); $data->perpage = $perpageselect->export_for_template($OUTPUT); echo $OUTPUT->render_from_template('forumreport_summary/bulk_action_menu', $data); } } PKW\L%k4343"summary/classes/output/filters.phpnu[. /** * Forum summary report filters renderable. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary\output; use context_course; use moodle_url; use renderable; use renderer_base; use stdClass; use templatable; use forumreport_summary; /** * Forum summary report filters renderable. * * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class filters implements renderable, templatable { /** * Course modules the report relates to. * Array of stdClass objects * * @var array $cms */ protected $cms; /** * Course ID where the report is being generated. * * @var int $courseid */ protected $courseid; /** * Moodle URL used as the form action on the generate button. * * @var moodle_url $actionurl */ protected $actionurl; /** * Details of groups available for filtering. * Stored in the format groupid => groupname. * * @var array $groupsavailable */ protected $groupsavailable = []; /** * IDs of groups selected for filtering. * * @var array $groupsselected */ protected $groupsselected = []; /** * IDs of discussions required for export links. * If a subset of groups available are selected, this will include the discussion IDs * within that group in the forum. * If all groups are selected, or no groups mode is enabled, this will be empty as * no discussion filtering is required in the export. * * @var array $discussionids */ protected $discussionids = []; /** * HTML for dates filter. * * @var array $datesdata */ protected $datesdata = []; /** * Text to display on the dates filter button. * * @var string $datesbuttontext */ protected $datesbuttontext; /** * Builds renderable filter data. * * @param stdClass $course The course object. * @param array $cms Array of course module objects. * @param moodle_url $actionurl The form action URL. * @param array $filterdata (optional) Associative array of data that has been set on available filters, if any, * in the format filtertype => [values] */ public function __construct(stdClass $course, array $cms, moodle_url $actionurl, array $filterdata = []) { $this->cms = $cms; $this->courseid = $course->id; $this->actionurl = $actionurl; // Prepare groups filter data. $groupsdata = $filterdata['groups'] ?? []; $this->prepare_groups_data($groupsdata); // Prepare dates filter data. $datefromdata = $filterdata['datefrom'] ?? []; $datetodata = $filterdata['dateto'] ?? []; $this->prepare_dates_data($datefromdata, $datetodata); } /** * Prepares groups data and sets relevant property values. * * @param array $groupsdata Groups selected for filtering. * @return void. */ protected function prepare_groups_data(array $groupsdata): void { global $DB, $USER; $groupsavailable = []; $allowedgroupsobj = []; $usergroups = groups_get_all_groups($this->courseid, $USER->id); $coursegroups = groups_get_all_groups($this->courseid); $forumids = []; $allgroups = false; $hasgroups = false; // Check if any forum gives the user access to all groups and no groups. foreach ($this->cms as $cm) { $forumids[] = $cm->instance; // Only need to check for all groups access if not confirmed by a previous check. if (!$allgroups) { $groupmode = groups_get_activity_groupmode($cm); // If no groups mode enabled on the forum, nothing to prepare. if (!in_array($groupmode, [VISIBLEGROUPS, SEPARATEGROUPS])) { continue; } $hasgroups = true; // Fetch for the current cm's forum. $context = \context_module::instance($cm->id); $aag = has_capability('moodle/site:accessallgroups', $context); if ($groupmode == VISIBLEGROUPS || $aag) { $allgroups = true; } } } // If no groups mode enabled, nothing to prepare. if (!$hasgroups) { return; } // Any groups, and no groups. if ($allgroups) { $nogroups = new stdClass(); $nogroups->id = -1; $nogroups->name = get_string('groupsnone'); $allowedgroupsobj = $coursegroups + [$nogroups]; } else { $allowedgroupsobj = $usergroups; } $contextcourse = context_course::instance($this->courseid); foreach ($allowedgroupsobj as $group) { $groupsavailable[$group->id] = format_string($group->name, true, ['context' => $contextcourse]); } // Set valid groups selected. $groupsselected = array_intersect($groupsdata, array_keys($groupsavailable)); // Overwrite groups properties. $this->groupsavailable = $groupsavailable; $this->groupsselected = $groupsselected; $groupsselectedcount = count($groupsselected); if ($groupsselectedcount > 0 && $groupsselectedcount < count($groupsavailable)) { list($forumidin, $forumidparams) = $DB->get_in_or_equal($forumids, SQL_PARAMS_NAMED); list($groupidin, $groupidparams) = $DB->get_in_or_equal($groupsselected, SQL_PARAMS_NAMED); $discussionswhere = "course = :courseid AND forum {$forumidin} AND groupid {$groupidin}"; $discussionsparams = ['courseid' => $this->courseid]; $discussionsparams += $forumidparams + $groupidparams; $discussionids = $DB->get_fieldset_select('forum_discussions', 'DISTINCT id', $discussionswhere, $discussionsparams); foreach ($discussionids as $discussionid) { $this->discussionids[] = ['discid' => $discussionid]; } } } /** * Prepares from date, to date and button text. * Empty data will default to a disabled filter with today's date. * * @param array $datefromdata From date selected for filtering, and whether the filter is enabled. * @param array $datetodata To date selected for filtering, and whether the filter is enabled. * @return void. */ private function prepare_dates_data(array $datefromdata, array $datetodata): void { $timezone = \core_date::get_user_timezone_object(); $calendartype = \core_calendar\type_factory::get_calendar_instance(); $timestamptoday = time(); $datetoday = $calendartype->timestamp_to_date_array($timestamptoday, $timezone); // Prepare date/enabled data. if (empty($datefromdata['enabled'])) { $fromdate = $datetoday; $fromtimestamp = $timestamptoday; $fromenabled = false; } else { $fromdate = $calendartype->timestamp_to_date_array($datefromdata['timestamp'], $timezone); $fromtimestamp = $datefromdata['timestamp']; $fromenabled = true; } if (empty($datetodata['enabled'])) { $todate = $datetoday; $totimestamp = $timestamptoday; $toenabled = false; } else { $todate = $calendartype->timestamp_to_date_array($datetodata['timestamp'], $timezone); $totimestamp = $datetodata['timestamp']; $toenabled = true; } $this->datesdata = [ 'from' => [ 'day' => $fromdate['mday'], 'month' => $fromdate['mon'], 'year' => $fromdate['year'], 'timestamp' => $fromtimestamp, 'enabled' => $fromenabled, ], 'to' => [ 'day' => $todate['mday'], 'month' => $todate['mon'], 'year' => $todate['year'], 'timestamp' => $totimestamp, 'enabled' => $toenabled, ], ]; // Prepare button string data. $displayformat = get_string('strftimedatemonthabbr', 'langconfig'); $fromdatestring = $calendartype->timestamp_to_date_string($fromtimestamp, $displayformat, $timezone, true, true); $todatestring = $calendartype->timestamp_to_date_string($totimestamp, $displayformat, $timezone, true, true); if ($fromenabled && $toenabled) { $datestrings = [ 'datefrom' => $fromdatestring, 'dateto' => $todatestring, ]; $this->datesbuttontext = get_string('filter:datesfromto', 'forumreport_summary', $datestrings); } else if ($fromenabled) { $this->datesbuttontext = get_string('filter:datesfrom', 'forumreport_summary', $fromdatestring); } else if ($toenabled) { $this->datesbuttontext = get_string('filter:datesto', 'forumreport_summary', $todatestring); } else { $this->datesbuttontext = get_string('filter:datesname', 'forumreport_summary'); } } /** * Export data for use as the context of a mustache template. * * @param renderer_base $renderer The renderer to be used to display report filters. * @return array Data in a format compatible with a mustache template. */ public function export_for_template(renderer_base $renderer): stdClass { $output = new stdClass(); // Set formaction URL. $output->actionurl = $this->actionurl->out(false); // Set groups filter data. if (!empty($this->groupsavailable)) { $output->hasgroups = true; $groupscount = count($this->groupsselected); if (count($this->groupsavailable) <= $groupscount) { $output->filtergroupsname = get_string('filter:groupscountall', 'forumreport_summary'); } else if (!empty($this->groupsselected)) { $output->filtergroupsname = get_string('filter:groupscountnumber', 'forumreport_summary', $groupscount); } else { $output->filtergroupsname = get_string('filter:groupsname', 'forumreport_summary'); } // Set groups filter. $groupsdata = []; foreach ($this->groupsavailable as $groupid => $groupname) { $groupsdata[] = [ 'groupid' => $groupid, 'groupname' => $groupname, 'checked' => in_array($groupid, $this->groupsselected), ]; } $output->filtergroups = $groupsdata; } else { $output->hasgroups = false; } // Set discussion IDs for use by export links (always included, as it will be empty if not required). $output->discussionids = $this->discussionids; // Set date button and generate dates popover mform. $datesformdata = []; if ($this->datesdata['from']['enabled']) { $datesformdata['filterdatefrompopover'] = $this->datesdata['from']; } if ($this->datesdata['to']['enabled']) { $datesformdata['filterdatetopopover'] = $this->datesdata['to']; } $output->filterdatesname = $this->datesbuttontext; $datesform = new forumreport_summary\form\dates_filter_form(); $datesform->set_data($datesformdata); $output->filterdatesform = $datesform->render(); // Set dates filter data within filters form. $disableddate = [ 'day' => '', 'month' => '', 'year' => '', 'enabled' => '0', ]; $datefromdata = ['type' => 'from'] + ($this->datesdata['from']['enabled'] ? $this->datesdata['from'] : $disableddate); $datetodata = ['type' => 'to'] + ($this->datesdata['to']['enabled'] ? $this->datesdata['to'] : $disableddate); $output->filterdatesdata = [$datefromdata, $datetodata]; return $output; } } PKW\_ +summary/classes/event/report_downloaded.phpnu[. /** * The forum summary report downloaded event. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary\event; defined('MOODLE_INTERNAL') || die(); /** * The forum summary report downloaded event class. * * @package forumreport_summary * @since Moodle 3.8 * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_downloaded extends \core\event\base { /** * Set basic properties for the event. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventreportdownloaded', 'forumreport_summary'); } /** * Returns non-localised event description with ids for admin use only. * * @return string */ public function get_description() { $whose = $this->other['hasviewall'] ? 'the' : 'their own'; $description = "The user with id '{$this->userid}' downloaded {$whose} summary report for "; if ($this->other['forumid']) { $description .= "the forum with course module id '{$this->contextinstanceid}'."; } else { $description .= "the course with id '{$this->contextinstanceid}'."; } return $description; } /** * Returns relevant URL. * @return \moodle_url */ public function get_url() { $params = ['courseid' => $this->courseid]; if (!empty($this->other['forumid'])) { $params['forumid'] = $this->other['forumid']; } return new \moodle_url('/mod/forum/report/summary/index.php', $params); } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['hasviewall'])) { throw new \coding_exception('The \'hasviewall\' value must be set'); } } } PKW\e{Ճ 'summary/classes/event/report_viewed.phpnu[. /** * The forum summary report viewed event. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary\event; defined('MOODLE_INTERNAL') || die(); /** * The forum summary report viewed event class. * * @package forumreport_summary * @since Moodle 3.8 * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class report_viewed extends \core\event\base { /** * Set basic properties for the event. */ protected function init() { $this->data['crud'] = 'r'; $this->data['edulevel'] = self::LEVEL_TEACHING; } /** * Returns localised general event name. * * @return string */ public static function get_name() { return get_string('eventreportviewed', 'forumreport_summary'); } /** * Returns non-localised event description with ids for admin use only. * * @return string */ public function get_description() { $whose = $this->other['hasviewall'] ? 'the' : 'their own'; $description = "The user with id '{$this->userid}' viewed {$whose} summary report for "; if ($this->other['forumid']) { $description .= "the forum with course module id '{$this->contextinstanceid}'."; } else { $description .= "the course with id '{$this->contextinstanceid}'."; } return $description; } /** * Returns relevant URL. * * @return \moodle_url */ public function get_url() { $params = ['courseid' => $this->courseid]; if (!empty($this->other['forumid'])) { $params['forumid'] = $this->other['forumid']; } return new \moodle_url('/mod/forum/report/summary/index.php', $params); } /** * Custom validation. * * @throws \coding_exception */ protected function validate_data() { parent::validate_data(); if (!isset($this->other['hasviewall'])) { throw new \coding_exception('The \'hasviewall\' value must be set'); } } } PKW\ p  +summary/templates/bulk_action_menu.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 forumreport_summary/bulk_action_menu Summary report filters. Example context (json): { "showbulkactions": true, "id": "formactionid", "attributes": [ { "name": "data-action", "value": "toggle" }, { "name": "data-togglegroup", "value": "summaryreport-table" }, { "name": "data-toggle", "value": "action" } ], "actions": [ { "name": "Choose...", "value": "" }, { "name": "Send a message", "value": "#messageselect" } ], "perpage": { "name": "perpage", "method": "get", "action": "#", "classes": "singleselect", "label": "Per page", "formid": "randomid", "id": "selectperpage", "options":[ {"value": 50, "name": "50", "selected": true}, {"value": 100, "name": "100", "selected":false}, {"value": 200, "name": "200", "selected": false} ] } } }}
    {{#showbulkactions}}
    {{/showbulkactions}}
    {{#perpage}} {{> core/single_select}} {{/perpage}}
    {{#js}} {{#id}} require(['jquery', 'core_message/message_send_bulk', 'core/custom_interaction_events'], function($, BulkSender, CustomEvents) { CustomEvents.define('#{{id}}', [CustomEvents.events.accessibleChange]); $('#{{id}}').on(CustomEvents.events.accessibleChange, function(e) { var action = $(e.target).val(); if (action.indexOf('#') !== -1) { e.preventDefault(); var ids = $('#summaryreport input.usercheckbox:checked').map(function(index, element) { return element.name.replace('user', ''); }).get(); if (action == '#messageselect') { BulkSender.showModal(ids, function() { $('#{{id}}').focus(); }); } $('#{{id}} option[value=""]').prop('selected', 'selected'); } }); }); {{/id}} {{/js}} PKW\U??/summary/templates/filter_dates_popover.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 forumreport_summary/filter_dates_popover Summary report dates filter. Example context (json): { "filterdatesform": "
    HTML for date filters
    " } }} PKW\_XHH-summary/templates/export_link_button.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 forumreport_summary/export_link_button Summary report filters. Example context (json): { "url": "wwwroot/moodle/mod/forum/export.php?userids[]=12", "label": "Export posts for Joey Jojo" } }} PKW\SY Y (summary/templates/filter_groups.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 forumreport_summary/filter_groups Summary report groups filter. Example context (json): { "hasgroups": true, "filtergroupsname" : "Groups (all)", "filtergroups": [ { "groupid": "1", "groupname": "Group A", "checked": true }, { "groupid": "3", "groupname": "Group C", "checked": false } ] } }} {{#hasgroups}} {{! Groups filter values to submit }} {{! Groups filter popover }} {{/hasgroups}} PKW\ghx "summary/templates/filters.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 forumreport_summary/filters Summary report filters. Example context (json): { "actionurl": "https://mymoodlesite.com/mod/forum/report/summary/index.php?courseid=2&forumid=2&perpage=50", "hasgroups": true, "filtergroupsname" : "Groups (all)", "filtergroups": [ { "groupid": "1", "groupname": "Group A", "checked": true }, { "groupid": "3", "groupname": "Group C", "checked": false } ], "filterdatesname": "Dates", "filterdatesform": "
    HTML for date filters
    ", "filterdatesdata": [ { "type": "from", "timestamp": "510969600", "enabled": "1" }, { "type": "to", "timestamp": "725673600", "enabled": "1" } ], "discussionids": [ { "discid": 123 }, { "discid": 456 } ] } }}
    {{> forumreport_summary/filter_dates}} {{> forumreport_summary/filter_groups}}
    {{#discussionids}} {{/discussionids}}
    {{! Dates filter popover - mform must exist outside of the filtersform }} {{> forumreport_summary/filter_dates_popover}}
    {{#js}} require(['forumreport_summary/filters'], function(Filters) { Filters.init(document.querySelector("[data-report-id='{{uniqid}}']")); }); {{/js}} PKW\rH!summary/templates/report.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 forumreport_summary/report Summary report filters. Example context (json): { "tablehtml": "TableSQL format HTML for the table" } }}
    {{> core/loading }}
    PKW\6v~~'summary/templates/filter_dates.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 forumreport_summary/filter_dates Summary report dates filter. Example context (json): { "filterdatesname": "Dates", "filterdatesdata": [ { "type": "from", "timestamp": "1571377510", "enabled": "1" }, { "type": "to", "timestamp": "1571377510", "enabled": "1" } ] } }} {{! Hidden dates fields to populate from visible mform in popover. }} {{#filterdatesdata}} {{/filterdatesdata}} PKW\\Gsummary/index.phpnu[. /** * This script displays the forum summary report for the given parameters, within a user's capabilities. * * @package forumreport_summary * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ require_once("../../../../config.php"); if (isguestuser()) { throw new \moodle_exception('noguest'); } $courseid = required_param('courseid', PARAM_INT); $forumid = optional_param('forumid', 0, PARAM_INT); $perpage = optional_param('perpage', \forumreport_summary\summary_table::DEFAULT_PER_PAGE, PARAM_INT); $download = optional_param('download', '', PARAM_ALPHA); $filters = []; $pageurlparams = [ 'courseid' => $courseid, 'perpage' => $perpage, ]; // Establish filter values. $filters['groups'] = optional_param_array('filtergroups', [], PARAM_INT); $filters['datefrom'] = optional_param_array('datefrom', ['enabled' => 0], PARAM_INT); $filters['dateto'] = optional_param_array('dateto', ['enabled' => 0], PARAM_INT); $modinfo = get_fast_modinfo($courseid); $course = $modinfo->get_course(); $courseforums = $modinfo->instances['forum']; $cms = []; // Determine which forums the user has access to in the course. $accessallforums = false; $allforumidsincourse = array_keys($courseforums); $forumsvisibletouser = []; $forumselectoptions = [0 => get_string('forumselectcourseoption', 'forumreport_summary')]; foreach ($courseforums as $courseforumid => $courseforum) { if ($courseforum->uservisible) { $forumsvisibletouser[$courseforumid] = $courseforum; $forumselectoptions[$courseforumid] = $courseforum->get_formatted_name(); } } if ($forumid) { if (!isset($forumsvisibletouser[$forumid])) { throw new \moodle_exception('A valid forum ID is required to generate a summary report.'); } $filters['forums'] = [$forumid]; $title = $forumsvisibletouser[$forumid]->get_formatted_name(); $forumcm = $forumsvisibletouser[$forumid]; $cms[] = $forumcm; require_login($courseid, false, $forumcm); $context = $forumcm->context; $canexport = !$download && has_capability('mod/forum:exportforum', $context); $redirecturl = new moodle_url('/mod/forum/view.php', ['id' => $forumid]); $numforums = 1; $pageurlparams['forumid'] = $forumid; $iscoursereport = false; } else { // Course level report. require_login($courseid, false); $filters['forums'] = array_keys($forumsvisibletouser); // Fetch the forum CMs for the course. foreach ($forumsvisibletouser as $visibleforum) { $cms[] = $visibleforum; } $context = \context_course::instance($courseid); $title = $course->fullname; // Export currently only supports single forum exports. $canexport = false; $redirecturl = new moodle_url('/course/view.php', ['id' => $courseid]); $numforums = count($forumsvisibletouser); $iscoursereport = true; // Specify whether user has access to all forums in the course. $accessallforums = empty(array_diff($allforumidsincourse, $filters['forums'])); } $pageurl = new moodle_url('/mod/forum/report/summary/index.php', $pageurlparams); $PAGE->set_url($pageurl); $PAGE->set_pagelayout('report'); $PAGE->set_title($title); $PAGE->set_heading($course->fullname); $PAGE->activityheader->disable(); $PAGE->navbar->add(get_string('nodetitle', 'forumreport_summary')); // Activate the secondary nav tab. navigation_node::override_active_url(new moodle_url('/mod/forum/report/summary/index.php', ['courseid' => $courseid, 'forumid' => $forumid])); $allowbulkoperations = !$download && !empty($CFG->messaging) && has_capability('moodle/course:bulkmessaging', $context); $canseeprivatereplies = false; $hasviewall = false; $privatereplycapcount = 0; $viewallcount = 0; $canview = false; foreach ($cms as $cm) { $forumcontext = $cm->context; // This capability is required in at least one of the given contexts to view any version of the report. if (has_capability('forumreport/summary:view', $forumcontext)) { $canview = true; } if (has_capability('mod/forum:readprivatereplies', $forumcontext)) { $privatereplycapcount++; } if (has_capability('forumreport/summary:viewall', $forumcontext)) { $viewallcount++; } } if (!$canview) { redirect($redirecturl); } // Only use private replies if user has that cap in all forums in the report. if ($numforums === $privatereplycapcount) { $canseeprivatereplies = true; } // Will only show all users if user has the cap for all forums in the report. if ($numforums === $viewallcount) { $hasviewall = true; } // Prepare and display the report. $table = new \forumreport_summary\summary_table($courseid, $filters, $allowbulkoperations, $canseeprivatereplies, $perpage, $canexport, $iscoursereport, $accessallforums); $table->baseurl = $pageurl; $eventparams = [ 'context' => $context, 'other' => [ 'forumid' => $forumid, 'hasviewall' => $hasviewall, ], ]; if ($download) { \forumreport_summary\event\report_downloaded::create($eventparams)->trigger(); $table->download($download); } else { \forumreport_summary\event\report_viewed::create($eventparams)->trigger(); echo $OUTPUT->header(); if (!empty($filters['groups'])) { \core\notification::info(get_string('viewsdisclaimer', 'forumreport_summary')); } // Allow switching to course report (or other forum user has access to). $reporturl = new moodle_url('/mod/forum/report/summary/index.php', ['courseid' => $courseid]); $forumselect = new single_select($reporturl, 'forumid', $forumselectoptions, $forumid, ''); $forumselect->set_label(get_string('forumselectlabel', 'forumreport_summary')); echo $OUTPUT->render($forumselect); echo $OUTPUT->heading(get_string('nodetitle', 'forumreport_summary'), 2, 'pb-5 mt-3'); // Render the report filters form. $renderer = $PAGE->get_renderer('forumreport_summary'); unset($filters['forums']); echo $renderer->render_filters_form($course, $cms, $pageurl, $filters); $table->show_download_buttons_at(array(TABLE_P_BOTTOM)); echo $renderer->render_summary_table($table); echo $OUTPUT->footer(); } PKW\&1??summary/amd/src/filters.jsnu[// 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 . /** * Module responsible for handling forum summary report filters. * * @module forumreport_summary/filters * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ import $ from 'jquery'; import {createPopper} from 'core/popper2'; import CustomEvents from 'core/custom_interaction_events'; import Selectors from 'forumreport_summary/selectors'; import Ajax from 'core/ajax'; import KeyCodes from 'core/key_codes'; import * as FormChangeChecker from 'core_form/changechecker'; export const init = (root) => { let jqRoot = $(root); // Hide loading spinner and show report once page is ready. // This ensures filters can be applied when sorting by columns. $(document).ready(function() { $('.loading-icon').hide(); $('#summaryreport').removeClass('hidden'); }); // Generic filter handlers. // Called to override click event to trigger a proper generate request with filtering. const generateWithFilters = (event, getparams) => { let currentLink = document.forms.filtersform.action, newLink; if (event) { event.preventDefault(); let currentSplit = currentLink.split('?'), currentstring = currentSplit[1], newparamsarray = getparams.split('&'), paramsstring = '', paramkeys = [], paramvalues = []; // Separate out the existing action GET param string. currentstring.split('&').forEach(function(param) { let splitparam = param.split('='); paramkeys.push(splitparam[0]); paramvalues.push(splitparam[1]); }); newparamsarray.forEach(function(paramstring) { let newparam = paramstring.split('='), existingkey = paramkeys.indexOf(newparam[0]); // Overwrite value if existing, otherwise add new param. if (existingkey > -1) { paramvalues[existingkey] = newparam[1]; } else { paramkeys.push(newparam[0]); paramvalues.push(newparam[1]); } }); // Build URL. paramkeys.forEach(function(name, key) { paramsstring += `&${name}=${paramvalues[key]}`; }); newLink = currentSplit[0] + '?' + paramsstring.substr(1); } else { newLink = currentLink; } document.forms.filtersform.action = newLink; document.forms.filtersform.submit(); }; // Override 'reset table preferences' so it generates with filters. $('.resettable').on("click", "a", function(event) { generateWithFilters(event, event.target.search.substr(1)); }); // Override table heading sort links so they generate with filters. $('thead').on("click", "a", function(event) { generateWithFilters(event, event.target.search.substr(1)); }); // Override pagination page links so they generate with filters. $('.pagination').on("click", "a", function(event) { generateWithFilters(event, event.target.search.substr(1)); }); // Override rows per page submission so it generates with filters. if (document.forms.selectperpage) { document.forms.selectperpage.onsubmit = (event) => { let getparam = 'perpage=' + document.forms.selectperpage.elements.perpage.value; generateWithFilters(event, getparam); }; } // Override download link so the file is generated with filters. const downloadForm = document.getElementById('summaryreport').querySelector('form.dataformatselector'); if (downloadForm) { downloadForm.onsubmit = (event) => { const downloadType = downloadForm.querySelector('#downloadtype_download').value; const getParams = `download=${downloadType}`; const prevAction = document.forms.filtersform.action; generateWithFilters(event, getParams); // Revert action, so re-submitting the form via filter does not trigger a further download. document.forms.filtersform.action = prevAction; }; } // Submit report via filter const submitWithFilter = (containerelement) => { // Disable the dates filter mform checker to prevent any changes triggering a warning to the user. FormChangeChecker.unWatchForm(document.querySelector(containerelement).querySelector('form')); // Close the container (eg popover). $(containerelement).addClass('hidden'); // Submit the filter values and re-generate report. generateWithFilters(false); }; // Use popper to override date mform calendar position. const updateCalendarPosition = (referenceid) => { let referenceElement = document.querySelector(referenceid), popperContent = document.querySelector(Selectors.filters.date.calendar); popperContent.style.removeProperty("z-index"); createPopper(referenceElement, popperContent, {placement: 'bottom-end'}); }; // Close the relevant filter. const closeOpenFilters = (openFilterButton, openFilter) => { openFilter.classList.add('hidden'); openFilter.setAttribute('data-openfilter', 'false'); openFilterButton.classList.add('btn-primary'); openFilterButton.classList.remove('btn-outline-primary'); openFilterButton.setAttribute('aria-expanded', false); }; // Groups filter specific handlers. // Event handler for clicking select all groups. jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.selectall, function() { let deselected = root.querySelectorAll(Selectors.filters.group.checkbox + ':not(:checked)'); deselected.forEach(function(checkbox) { checkbox.checked = true; }); }); // Event handler for clearing filter by clicking option. jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.clear, function() { // Clear checkboxes. let selected = root.querySelectorAll(Selectors.filters.group.checkbox + ':checked'); selected.forEach(function(checkbox) { checkbox.checked = false; }); }); // Event handler for showing groups filter popover. jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.trigger, function() { // Create popover. let referenceElement = root.querySelector(Selectors.filters.group.trigger), popperContent = root.querySelector(Selectors.filters.group.popover); createPopper(referenceElement, popperContent, {placement: 'bottom-end'}); // Show popover. popperContent.classList.remove('hidden'); popperContent.setAttribute('data-openfilter', 'true'); // Change to outlined button. referenceElement.classList.add('btn-outline-primary'); referenceElement.classList.remove('btn-primary'); // Let screen readers know that it's now expanded. referenceElement.setAttribute('aria-expanded', true); // Add listeners to handle closing filter. const closeListener = e => { if (e.target.id !== referenceElement.id && popperContent !== e.target.closest('[data-openfilter="true"]') && (typeof e.keyCode === 'undefined' || e.keyCode === KeyCodes.enter || e.keyCode === KeyCodes.space)) { closeOpenFilters(referenceElement, popperContent); document.removeEventListener('click', closeListener); document.removeEventListener('keyup', closeListener); document.removeEventListener('keyup', escCloseListener); } }; document.addEventListener('click', closeListener); document.addEventListener('keyup', closeListener); const escCloseListener = e => { if (e.keyCode === KeyCodes.escape) { closeOpenFilters(referenceElement, popperContent); document.removeEventListener('keyup', escCloseListener); document.removeEventListener('click', closeListener); } }; document.addEventListener('keyup', escCloseListener); }); // Event handler to click save groups filter. jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.save, function() { // Copy the saved values into the form before submitting. let popcheckboxes = root.querySelectorAll(Selectors.filters.group.checkbox); popcheckboxes.forEach(function(popcheckbox) { let filtersform = document.forms.filtersform, saveid = popcheckbox.getAttribute('data-saveid'); filtersform.querySelector(`#${saveid}`).checked = popcheckbox.checked; }); submitWithFilter('#filter-groups-popover'); }); // Listeners for export buttons. // These allow fetching of the relevant export URL, before submitting the request with // any POST data that is common to all of the export links. This allows filters to be // applied that contain potentially a lot of data (eg discussion IDs for groups filtering). document.querySelectorAll(Selectors.filters.exportlink.link).forEach(function(exportbutton) { exportbutton.addEventListener('click', function(event) { document.forms.exportlinkform.action = event.target.dataset.url; document.forms.exportlinkform.submit(); }); }); // Dates filter specific handlers. // Event handler for showing dates filter popover. jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.trigger, function() { // Create popover. let referenceElement = root.querySelector(Selectors.filters.date.trigger), popperContent = root.querySelector(Selectors.filters.date.popover); createPopper(referenceElement, popperContent, {placement: 'bottom-end'}); // Show popover and move focus. popperContent.classList.remove('hidden'); popperContent.setAttribute('data-openfilter', 'true'); popperContent.querySelector('[name="filterdatefrompopover[enabled]"]').focus(); // Change to outlined button. referenceElement.classList.add('btn-outline-primary'); referenceElement.classList.remove('btn-primary'); // Let screen readers know that it's now expanded. referenceElement.setAttribute('aria-expanded', true); // Add listener to handle closing filter. const closeListener = e => { if (e.target.id !== referenceElement.id && popperContent !== e.target.closest('[data-openfilter="true"]') && (typeof e.keyCode === 'undefined' || e.keyCode === KeyCodes.enter || e.keyCode === KeyCodes.space)) { closeOpenFilters(referenceElement, popperContent); document.removeEventListener('click', closeListener); document.removeEventListener('keyup', closeListener); document.removeEventListener('keyup', escCloseListener); } }; document.addEventListener('click', closeListener); document.addEventListener('keyup', closeListener); const escCloseListener = e => { if (e.keyCode === KeyCodes.escape) { closeOpenFilters(referenceElement, popperContent); document.removeEventListener('keyup', escCloseListener); document.removeEventListener('click', closeListener); } }; document.addEventListener('keyup', escCloseListener); }); // Event handler to save dates filter. jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.save, function() { // Populate the hidden form inputs to submit the data. let filtersForm = document.forms.filtersform; const datesPopover = root.querySelector(Selectors.filters.date.popover); const fromEnabled = datesPopover.querySelector('[name="filterdatefrompopover[enabled]"]').checked ? 1 : 0; const toEnabled = datesPopover.querySelector('[name="filterdatetopopover[enabled]"]').checked ? 1 : 0; if (!fromEnabled && !toEnabled) { // Update the elements in the filter form. filtersForm.elements['datefrom[timestamp]'].value = 0; filtersForm.elements['datefrom[enabled]'].value = fromEnabled; filtersForm.elements['dateto[timestamp]'].value = 0; filtersForm.elements['dateto[enabled]'].value = toEnabled; // Submit the filter values and re-generate report. submitWithFilter('#filter-dates-popover'); } else { let args = {data: []}; if (fromEnabled) { args.data.push({ 'key': 'from', 'year': datesPopover.querySelector('[name="filterdatefrompopover[year]"]').value, 'month': datesPopover.querySelector('[name="filterdatefrompopover[month]"]').value, 'day': datesPopover.querySelector('[name="filterdatefrompopover[day]"]').value, 'hour': 0, 'minute': 0 }); } if (toEnabled) { args.data.push({ 'key': 'to', 'year': datesPopover.querySelector('[name="filterdatetopopover[year]"]').value, 'month': datesPopover.querySelector('[name="filterdatetopopover[month]"]').value, 'day': datesPopover.querySelector('[name="filterdatetopopover[day]"]').value, 'hour': 23, 'minute': 59 }); } const request = { methodname: 'core_calendar_get_timestamps', args: args }; Ajax.call([request])[0].done(function(result) { let fromTimestamp = 0, toTimestamp = 0; result.timestamps.forEach(function(data) { if (data.key === 'from') { fromTimestamp = data.timestamp; } else if (data.key === 'to') { toTimestamp = data.timestamp; } }); // Display an error if the from date is later than the do date. if (toTimestamp > 0 && fromTimestamp > toTimestamp) { const warningdiv = document.getElementById('dates-filter-warning'); warningdiv.classList.remove('hidden'); warningdiv.classList.add('d-block'); } else { filtersForm.elements['datefrom[timestamp]'].value = fromTimestamp; filtersForm.elements['datefrom[enabled]'].value = fromEnabled; filtersForm.elements['dateto[timestamp]'].value = toTimestamp; filtersForm.elements['dateto[enabled]'].value = toEnabled; // Submit the filter values and re-generate report. submitWithFilter('#filter-dates-popover'); } }); } }); jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconfrom, function() { updateCalendarPosition(Selectors.filters.date.calendariconfrom); }); jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconto, function() { updateCalendarPosition(Selectors.filters.date.calendariconto); }); }; PKW\*%%summary/amd/src/selectors.jsnu[// 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 . /** * Module containing the selectors for the forum summary report. * * @module forumreport_summary/selectors * @copyright 2019 Jun Pataleta * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ export default { filters: { group: { checkbox: '[data-region="filter-groups"] input[type="checkbox"]', clear: '[data-region="filter-groups"] .filter-clear', popover: '#filter-groups-popover', save: '[data-region="filter-groups"] .filter-save', selectall: '[data-region="filter-groups"] .select-all', trigger: '#filter-groups-button', }, date: { calendar: '#dateselector-calendar-panel', calendariconfrom: '#id_filterdatefrompopover_calendar', calendariconto: '#id_filterdatetopopover_calendar', popover: '#filter-dates-popover', save: '[data-region="filter-dates"] .filter-save', trigger: '#filter-dates-button', }, exportlink: { link: '#summaryreport #forumreport_summary_table button.export-link' } } }; PKW\lG<,, summary/amd/build/filters.min.jsnu[define("forumreport_summary/filters",["exports","jquery","core/popper2","core/custom_interaction_events","forumreport_summary/selectors","core/ajax","core/key_codes","core_form/changechecker"],(function(_exports,_jquery,_popper,_custom_interaction_events,_selectors,_ajax,_key_codes,FormChangeChecker){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} /** * Module responsible for handling forum summary report filters. * * @module forumreport_summary/filters * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=_interopRequireDefault(_jquery),_custom_interaction_events=_interopRequireDefault(_custom_interaction_events),_selectors=_interopRequireDefault(_selectors),_ajax=_interopRequireDefault(_ajax),_key_codes=_interopRequireDefault(_key_codes),FormChangeChecker=function(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}(FormChangeChecker);_exports.init=root=>{let jqRoot=(0,_jquery.default)(root);(0,_jquery.default)(document).ready((function(){(0,_jquery.default)(".loading-icon").hide(),(0,_jquery.default)("#summaryreport").removeClass("hidden")}));const generateWithFilters=(event,getparams)=>{let newLink,currentLink=document.forms.filtersform.action;if(event){event.preventDefault();let currentSplit=currentLink.split("?"),currentstring=currentSplit[1],newparamsarray=getparams.split("&"),paramsstring="",paramkeys=[],paramvalues=[];currentstring.split("&").forEach((function(param){let splitparam=param.split("=");paramkeys.push(splitparam[0]),paramvalues.push(splitparam[1])})),newparamsarray.forEach((function(paramstring){let newparam=paramstring.split("="),existingkey=paramkeys.indexOf(newparam[0]);existingkey>-1?paramvalues[existingkey]=newparam[1]:(paramkeys.push(newparam[0]),paramvalues.push(newparam[1]))})),paramkeys.forEach((function(name,key){paramsstring+="&".concat(name,"=").concat(paramvalues[key])})),newLink=currentSplit[0]+"?"+paramsstring.substr(1)}else newLink=currentLink;document.forms.filtersform.action=newLink,document.forms.filtersform.submit()};(0,_jquery.default)(".resettable").on("click","a",(function(event){generateWithFilters(event,event.target.search.substr(1))})),(0,_jquery.default)("thead").on("click","a",(function(event){generateWithFilters(event,event.target.search.substr(1))})),(0,_jquery.default)(".pagination").on("click","a",(function(event){generateWithFilters(event,event.target.search.substr(1))})),document.forms.selectperpage&&(document.forms.selectperpage.onsubmit=event=>{let getparam="perpage="+document.forms.selectperpage.elements.perpage.value;generateWithFilters(event,getparam)});const downloadForm=document.getElementById("summaryreport").querySelector("form.dataformatselector");downloadForm&&(downloadForm.onsubmit=event=>{const downloadType=downloadForm.querySelector("#downloadtype_download").value,getParams="download=".concat(downloadType),prevAction=document.forms.filtersform.action;generateWithFilters(event,getParams),document.forms.filtersform.action=prevAction});const submitWithFilter=containerelement=>{FormChangeChecker.unWatchForm(document.querySelector(containerelement).querySelector("form")),(0,_jquery.default)(containerelement).addClass("hidden"),generateWithFilters(!1)},updateCalendarPosition=referenceid=>{let referenceElement=document.querySelector(referenceid),popperContent=document.querySelector(_selectors.default.filters.date.calendar);popperContent.style.removeProperty("z-index"),(0,_popper.createPopper)(referenceElement,popperContent,{placement:"bottom-end"})},closeOpenFilters=(openFilterButton,openFilter)=>{openFilter.classList.add("hidden"),openFilter.setAttribute("data-openfilter","false"),openFilterButton.classList.add("btn-primary"),openFilterButton.classList.remove("btn-outline-primary"),openFilterButton.setAttribute("aria-expanded",!1)};jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.group.selectall,(function(){root.querySelectorAll(_selectors.default.filters.group.checkbox+":not(:checked)").forEach((function(checkbox){checkbox.checked=!0}))})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.group.clear,(function(){root.querySelectorAll(_selectors.default.filters.group.checkbox+":checked").forEach((function(checkbox){checkbox.checked=!1}))})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.group.trigger,(function(){let referenceElement=root.querySelector(_selectors.default.filters.group.trigger),popperContent=root.querySelector(_selectors.default.filters.group.popover);(0,_popper.createPopper)(referenceElement,popperContent,{placement:"bottom-end"}),popperContent.classList.remove("hidden"),popperContent.setAttribute("data-openfilter","true"),referenceElement.classList.add("btn-outline-primary"),referenceElement.classList.remove("btn-primary"),referenceElement.setAttribute("aria-expanded",!0);const closeListener=e=>{e.target.id===referenceElement.id||popperContent===e.target.closest('[data-openfilter="true"]')||void 0!==e.keyCode&&e.keyCode!==_key_codes.default.enter&&e.keyCode!==_key_codes.default.space||(closeOpenFilters(referenceElement,popperContent),document.removeEventListener("click",closeListener),document.removeEventListener("keyup",closeListener),document.removeEventListener("keyup",escCloseListener))};document.addEventListener("click",closeListener),document.addEventListener("keyup",closeListener);const escCloseListener=e=>{e.keyCode===_key_codes.default.escape&&(closeOpenFilters(referenceElement,popperContent),document.removeEventListener("keyup",escCloseListener),document.removeEventListener("click",closeListener))};document.addEventListener("keyup",escCloseListener)})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.group.save,(function(){root.querySelectorAll(_selectors.default.filters.group.checkbox).forEach((function(popcheckbox){let filtersform=document.forms.filtersform,saveid=popcheckbox.getAttribute("data-saveid");filtersform.querySelector("#".concat(saveid)).checked=popcheckbox.checked})),submitWithFilter("#filter-groups-popover")})),document.querySelectorAll(_selectors.default.filters.exportlink.link).forEach((function(exportbutton){exportbutton.addEventListener("click",(function(event){document.forms.exportlinkform.action=event.target.dataset.url,document.forms.exportlinkform.submit()}))})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.date.trigger,(function(){let referenceElement=root.querySelector(_selectors.default.filters.date.trigger),popperContent=root.querySelector(_selectors.default.filters.date.popover);(0,_popper.createPopper)(referenceElement,popperContent,{placement:"bottom-end"}),popperContent.classList.remove("hidden"),popperContent.setAttribute("data-openfilter","true"),popperContent.querySelector('[name="filterdatefrompopover[enabled]"]').focus(),referenceElement.classList.add("btn-outline-primary"),referenceElement.classList.remove("btn-primary"),referenceElement.setAttribute("aria-expanded",!0);const closeListener=e=>{e.target.id===referenceElement.id||popperContent===e.target.closest('[data-openfilter="true"]')||void 0!==e.keyCode&&e.keyCode!==_key_codes.default.enter&&e.keyCode!==_key_codes.default.space||(closeOpenFilters(referenceElement,popperContent),document.removeEventListener("click",closeListener),document.removeEventListener("keyup",closeListener),document.removeEventListener("keyup",escCloseListener))};document.addEventListener("click",closeListener),document.addEventListener("keyup",closeListener);const escCloseListener=e=>{e.keyCode===_key_codes.default.escape&&(closeOpenFilters(referenceElement,popperContent),document.removeEventListener("keyup",escCloseListener),document.removeEventListener("click",closeListener))};document.addEventListener("keyup",escCloseListener)})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.date.save,(function(){let filtersForm=document.forms.filtersform;const datesPopover=root.querySelector(_selectors.default.filters.date.popover),fromEnabled=datesPopover.querySelector('[name="filterdatefrompopover[enabled]"]').checked?1:0,toEnabled=datesPopover.querySelector('[name="filterdatetopopover[enabled]"]').checked?1:0;if(fromEnabled||toEnabled){let args={data:[]};fromEnabled&&args.data.push({key:"from",year:datesPopover.querySelector('[name="filterdatefrompopover[year]"]').value,month:datesPopover.querySelector('[name="filterdatefrompopover[month]"]').value,day:datesPopover.querySelector('[name="filterdatefrompopover[day]"]').value,hour:0,minute:0}),toEnabled&&args.data.push({key:"to",year:datesPopover.querySelector('[name="filterdatetopopover[year]"]').value,month:datesPopover.querySelector('[name="filterdatetopopover[month]"]').value,day:datesPopover.querySelector('[name="filterdatetopopover[day]"]').value,hour:23,minute:59});const request={methodname:"core_calendar_get_timestamps",args:args};_ajax.default.call([request])[0].done((function(result){let fromTimestamp=0,toTimestamp=0;if(result.timestamps.forEach((function(data){"from"===data.key?fromTimestamp=data.timestamp:"to"===data.key&&(toTimestamp=data.timestamp)})),toTimestamp>0&&fromTimestamp>toTimestamp){const warningdiv=document.getElementById("dates-filter-warning");warningdiv.classList.remove("hidden"),warningdiv.classList.add("d-block")}else filtersForm.elements["datefrom[timestamp]"].value=fromTimestamp,filtersForm.elements["datefrom[enabled]"].value=fromEnabled,filtersForm.elements["dateto[timestamp]"].value=toTimestamp,filtersForm.elements["dateto[enabled]"].value=toEnabled,submitWithFilter("#filter-dates-popover")}))}else filtersForm.elements["datefrom[timestamp]"].value=0,filtersForm.elements["datefrom[enabled]"].value=fromEnabled,filtersForm.elements["dateto[timestamp]"].value=0,filtersForm.elements["dateto[enabled]"].value=toEnabled,submitWithFilter("#filter-dates-popover")})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.date.calendariconfrom,(function(){updateCalendarPosition(_selectors.default.filters.date.calendariconfrom)})),jqRoot.on(_custom_interaction_events.default.events.activate,_selectors.default.filters.date.calendariconto,(function(){updateCalendarPosition(_selectors.default.filters.date.calendariconto)}))}})); //# sourceMappingURL=filters.min.js.mapPKW\y8> > &summary/amd/build/selectors.min.js.mapnu[{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module containing the selectors for the forum summary report.\n *\n * @module forumreport_summary/selectors\n * @copyright 2019 Jun Pataleta\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n filters: {\n group: {\n checkbox: '[data-region=\"filter-groups\"] input[type=\"checkbox\"]',\n clear: '[data-region=\"filter-groups\"] .filter-clear',\n popover: '#filter-groups-popover',\n save: '[data-region=\"filter-groups\"] .filter-save',\n selectall: '[data-region=\"filter-groups\"] .select-all',\n trigger: '#filter-groups-button',\n },\n date: {\n calendar: '#dateselector-calendar-panel',\n calendariconfrom: '#id_filterdatefrompopover_calendar',\n calendariconto: '#id_filterdatetopopover_calendar',\n popover: '#filter-dates-popover',\n save: '[data-region=\"filter-dates\"] .filter-save',\n trigger: '#filter-dates-button',\n },\n exportlink: {\n link: '#summaryreport #forumreport_summary_table button.export-link'\n }\n }\n};\n"],"names":["filters","group","checkbox","clear","popover","save","selectall","trigger","date","calendar","calendariconfrom","calendariconto","exportlink","link"],"mappings":"+KAuBe,CACXA,QAAS,CACLC,MAAO,CACHC,SAAU,uDACVC,MAAO,8CACPC,QAAS,yBACTC,KAAM,6CACNC,UAAW,4CACXC,QAAS,yBAEbC,KAAM,CACFC,SAAU,+BACVC,iBAAkB,qCAClBC,eAAgB,mCAChBP,QAAS,wBACTC,KAAM,4CACNE,QAAS,wBAEbK,WAAY,CACRC,KAAM"}PKW\;Nww"summary/amd/build/selectors.min.jsnu[define("forumreport_summary/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={filters:{group:{checkbox:'[data-region="filter-groups"] input[type="checkbox"]',clear:'[data-region="filter-groups"] .filter-clear',popover:"#filter-groups-popover",save:'[data-region="filter-groups"] .filter-save',selectall:'[data-region="filter-groups"] .select-all',trigger:"#filter-groups-button"},date:{calendar:"#dateselector-calendar-panel",calendariconfrom:"#id_filterdatefrompopover_calendar",calendariconto:"#id_filterdatetopopover_calendar",popover:"#filter-dates-popover",save:'[data-region="filter-dates"] .filter-save',trigger:"#filter-dates-button"},exportlink:{link:"#summaryreport #forumreport_summary_table button.export-link"}}},_exports.default})); //# sourceMappingURL=selectors.min.js.mapPKW\&]]$summary/amd/build/filters.min.js.mapnu[{"version":3,"file":"filters.min.js","sources":["../src/filters.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Module responsible for handling forum summary report filters.\n *\n * @module forumreport_summary/filters\n * @copyright 2019 Michael Hawkins \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport $ from 'jquery';\nimport {createPopper} from 'core/popper2';\nimport CustomEvents from 'core/custom_interaction_events';\nimport Selectors from 'forumreport_summary/selectors';\nimport Ajax from 'core/ajax';\nimport KeyCodes from 'core/key_codes';\nimport * as FormChangeChecker from 'core_form/changechecker';\n\nexport const init = (root) => {\n let jqRoot = $(root);\n\n // Hide loading spinner and show report once page is ready.\n // This ensures filters can be applied when sorting by columns.\n $(document).ready(function() {\n $('.loading-icon').hide();\n $('#summaryreport').removeClass('hidden');\n });\n\n // Generic filter handlers.\n\n // Called to override click event to trigger a proper generate request with filtering.\n const generateWithFilters = (event, getparams) => {\n let currentLink = document.forms.filtersform.action,\n newLink;\n\n if (event) {\n event.preventDefault();\n\n let currentSplit = currentLink.split('?'),\n currentstring = currentSplit[1],\n newparamsarray = getparams.split('&'),\n paramsstring = '',\n paramkeys = [],\n paramvalues = [];\n\n // Separate out the existing action GET param string.\n currentstring.split('&').forEach(function(param) {\n let splitparam = param.split('=');\n paramkeys.push(splitparam[0]);\n paramvalues.push(splitparam[1]);\n });\n\n newparamsarray.forEach(function(paramstring) {\n let newparam = paramstring.split('='),\n existingkey = paramkeys.indexOf(newparam[0]);\n\n // Overwrite value if existing, otherwise add new param.\n if (existingkey > -1) {\n paramvalues[existingkey] = newparam[1];\n } else {\n paramkeys.push(newparam[0]);\n paramvalues.push(newparam[1]);\n }\n });\n\n // Build URL.\n paramkeys.forEach(function(name, key) {\n paramsstring += `&${name}=${paramvalues[key]}`;\n });\n\n newLink = currentSplit[0] + '?' + paramsstring.substr(1);\n } else {\n newLink = currentLink;\n }\n\n document.forms.filtersform.action = newLink;\n document.forms.filtersform.submit();\n };\n\n // Override 'reset table preferences' so it generates with filters.\n $('.resettable').on(\"click\", \"a\", function(event) {\n generateWithFilters(event, event.target.search.substr(1));\n });\n\n // Override table heading sort links so they generate with filters.\n $('thead').on(\"click\", \"a\", function(event) {\n generateWithFilters(event, event.target.search.substr(1));\n });\n\n // Override pagination page links so they generate with filters.\n $('.pagination').on(\"click\", \"a\", function(event) {\n generateWithFilters(event, event.target.search.substr(1));\n });\n\n // Override rows per page submission so it generates with filters.\n if (document.forms.selectperpage) {\n document.forms.selectperpage.onsubmit = (event) => {\n let getparam = 'perpage=' + document.forms.selectperpage.elements.perpage.value;\n generateWithFilters(event, getparam);\n };\n }\n\n // Override download link so the file is generated with filters.\n const downloadForm = document.getElementById('summaryreport').querySelector('form.dataformatselector');\n if (downloadForm) {\n downloadForm.onsubmit = (event) => {\n const downloadType = downloadForm.querySelector('#downloadtype_download').value;\n const getParams = `download=${downloadType}`;\n const prevAction = document.forms.filtersform.action;\n\n generateWithFilters(event, getParams);\n\n // Revert action, so re-submitting the form via filter does not trigger a further download.\n document.forms.filtersform.action = prevAction;\n };\n }\n\n // Submit report via filter\n const submitWithFilter = (containerelement) => {\n // Disable the dates filter mform checker to prevent any changes triggering a warning to the user.\n FormChangeChecker.unWatchForm(document.querySelector(containerelement).querySelector('form'));\n // Close the container (eg popover).\n $(containerelement).addClass('hidden');\n\n // Submit the filter values and re-generate report.\n generateWithFilters(false);\n };\n\n // Use popper to override date mform calendar position.\n const updateCalendarPosition = (referenceid) => {\n let referenceElement = document.querySelector(referenceid),\n popperContent = document.querySelector(Selectors.filters.date.calendar);\n\n popperContent.style.removeProperty(\"z-index\");\n createPopper(referenceElement, popperContent, {placement: 'bottom-end'});\n };\n\n // Close the relevant filter.\n const closeOpenFilters = (openFilterButton, openFilter) => {\n openFilter.classList.add('hidden');\n openFilter.setAttribute('data-openfilter', 'false');\n\n openFilterButton.classList.add('btn-primary');\n openFilterButton.classList.remove('btn-outline-primary');\n openFilterButton.setAttribute('aria-expanded', false);\n };\n\n // Groups filter specific handlers.\n\n // Event handler for clicking select all groups.\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.selectall, function() {\n let deselected = root.querySelectorAll(Selectors.filters.group.checkbox + ':not(:checked)');\n deselected.forEach(function(checkbox) {\n checkbox.checked = true;\n });\n });\n\n // Event handler for clearing filter by clicking option.\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.clear, function() {\n // Clear checkboxes.\n let selected = root.querySelectorAll(Selectors.filters.group.checkbox + ':checked');\n selected.forEach(function(checkbox) {\n checkbox.checked = false;\n });\n });\n\n // Event handler for showing groups filter popover.\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.trigger, function() {\n // Create popover.\n let referenceElement = root.querySelector(Selectors.filters.group.trigger),\n popperContent = root.querySelector(Selectors.filters.group.popover);\n\n createPopper(referenceElement, popperContent, {placement: 'bottom-end'});\n\n // Show popover.\n popperContent.classList.remove('hidden');\n popperContent.setAttribute('data-openfilter', 'true');\n\n // Change to outlined button.\n referenceElement.classList.add('btn-outline-primary');\n referenceElement.classList.remove('btn-primary');\n\n // Let screen readers know that it's now expanded.\n referenceElement.setAttribute('aria-expanded', true);\n\n // Add listeners to handle closing filter.\n const closeListener = e => {\n if (e.target.id !== referenceElement.id && popperContent !== e.target.closest('[data-openfilter=\"true\"]') &&\n (typeof e.keyCode === 'undefined' || e.keyCode === KeyCodes.enter || e.keyCode === KeyCodes.space)) {\n closeOpenFilters(referenceElement, popperContent);\n document.removeEventListener('click', closeListener);\n document.removeEventListener('keyup', closeListener);\n document.removeEventListener('keyup', escCloseListener);\n }\n };\n\n document.addEventListener('click', closeListener);\n document.addEventListener('keyup', closeListener);\n\n const escCloseListener = e => {\n if (e.keyCode === KeyCodes.escape) {\n closeOpenFilters(referenceElement, popperContent);\n document.removeEventListener('keyup', escCloseListener);\n document.removeEventListener('click', closeListener);\n }\n };\n\n document.addEventListener('keyup', escCloseListener);\n });\n\n // Event handler to click save groups filter.\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.group.save, function() {\n // Copy the saved values into the form before submitting.\n let popcheckboxes = root.querySelectorAll(Selectors.filters.group.checkbox);\n\n popcheckboxes.forEach(function(popcheckbox) {\n let filtersform = document.forms.filtersform,\n saveid = popcheckbox.getAttribute('data-saveid');\n\n filtersform.querySelector(`#${saveid}`).checked = popcheckbox.checked;\n });\n\n submitWithFilter('#filter-groups-popover');\n });\n\n // Listeners for export buttons.\n // These allow fetching of the relevant export URL, before submitting the request with\n // any POST data that is common to all of the export links. This allows filters to be\n // applied that contain potentially a lot of data (eg discussion IDs for groups filtering).\n document.querySelectorAll(Selectors.filters.exportlink.link).forEach(function(exportbutton) {\n exportbutton.addEventListener('click', function(event) {\n document.forms.exportlinkform.action = event.target.dataset.url;\n document.forms.exportlinkform.submit();\n });\n });\n\n // Dates filter specific handlers.\n\n // Event handler for showing dates filter popover.\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.trigger, function() {\n\n // Create popover.\n let referenceElement = root.querySelector(Selectors.filters.date.trigger),\n popperContent = root.querySelector(Selectors.filters.date.popover);\n\n createPopper(referenceElement, popperContent, {placement: 'bottom-end'});\n\n // Show popover and move focus.\n popperContent.classList.remove('hidden');\n popperContent.setAttribute('data-openfilter', 'true');\n popperContent.querySelector('[name=\"filterdatefrompopover[enabled]\"]').focus();\n\n // Change to outlined button.\n referenceElement.classList.add('btn-outline-primary');\n referenceElement.classList.remove('btn-primary');\n\n // Let screen readers know that it's now expanded.\n referenceElement.setAttribute('aria-expanded', true);\n\n // Add listener to handle closing filter.\n const closeListener = e => {\n if (e.target.id !== referenceElement.id && popperContent !== e.target.closest('[data-openfilter=\"true\"]') &&\n (typeof e.keyCode === 'undefined' || e.keyCode === KeyCodes.enter || e.keyCode === KeyCodes.space)) {\n closeOpenFilters(referenceElement, popperContent);\n document.removeEventListener('click', closeListener);\n document.removeEventListener('keyup', closeListener);\n document.removeEventListener('keyup', escCloseListener);\n }\n };\n\n document.addEventListener('click', closeListener);\n document.addEventListener('keyup', closeListener);\n\n const escCloseListener = e => {\n if (e.keyCode === KeyCodes.escape) {\n closeOpenFilters(referenceElement, popperContent);\n document.removeEventListener('keyup', escCloseListener);\n document.removeEventListener('click', closeListener);\n }\n };\n\n document.addEventListener('keyup', escCloseListener);\n });\n\n // Event handler to save dates filter.\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.save, function() {\n // Populate the hidden form inputs to submit the data.\n let filtersForm = document.forms.filtersform;\n const datesPopover = root.querySelector(Selectors.filters.date.popover);\n const fromEnabled = datesPopover.querySelector('[name=\"filterdatefrompopover[enabled]\"]').checked ? 1 : 0;\n const toEnabled = datesPopover.querySelector('[name=\"filterdatetopopover[enabled]\"]').checked ? 1 : 0;\n\n if (!fromEnabled && !toEnabled) {\n // Update the elements in the filter form.\n filtersForm.elements['datefrom[timestamp]'].value = 0;\n filtersForm.elements['datefrom[enabled]'].value = fromEnabled;\n filtersForm.elements['dateto[timestamp]'].value = 0;\n filtersForm.elements['dateto[enabled]'].value = toEnabled;\n\n // Submit the filter values and re-generate report.\n submitWithFilter('#filter-dates-popover');\n } else {\n let args = {data: []};\n\n if (fromEnabled) {\n args.data.push({\n 'key': 'from',\n 'year': datesPopover.querySelector('[name=\"filterdatefrompopover[year]\"]').value,\n 'month': datesPopover.querySelector('[name=\"filterdatefrompopover[month]\"]').value,\n 'day': datesPopover.querySelector('[name=\"filterdatefrompopover[day]\"]').value,\n 'hour': 0,\n 'minute': 0\n });\n }\n\n if (toEnabled) {\n args.data.push({\n 'key': 'to',\n 'year': datesPopover.querySelector('[name=\"filterdatetopopover[year]\"]').value,\n 'month': datesPopover.querySelector('[name=\"filterdatetopopover[month]\"]').value,\n 'day': datesPopover.querySelector('[name=\"filterdatetopopover[day]\"]').value,\n 'hour': 23,\n 'minute': 59\n });\n }\n\n const request = {\n methodname: 'core_calendar_get_timestamps',\n args: args\n };\n\n Ajax.call([request])[0].done(function(result) {\n let fromTimestamp = 0,\n toTimestamp = 0;\n\n result.timestamps.forEach(function(data) {\n if (data.key === 'from') {\n fromTimestamp = data.timestamp;\n } else if (data.key === 'to') {\n toTimestamp = data.timestamp;\n }\n });\n\n // Display an error if the from date is later than the do date.\n if (toTimestamp > 0 && fromTimestamp > toTimestamp) {\n const warningdiv = document.getElementById('dates-filter-warning');\n warningdiv.classList.remove('hidden');\n warningdiv.classList.add('d-block');\n } else {\n filtersForm.elements['datefrom[timestamp]'].value = fromTimestamp;\n filtersForm.elements['datefrom[enabled]'].value = fromEnabled;\n filtersForm.elements['dateto[timestamp]'].value = toTimestamp;\n filtersForm.elements['dateto[enabled]'].value = toEnabled;\n\n // Submit the filter values and re-generate report.\n submitWithFilter('#filter-dates-popover');\n }\n });\n }\n });\n\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconfrom, function() {\n updateCalendarPosition(Selectors.filters.date.calendariconfrom);\n });\n\n jqRoot.on(CustomEvents.events.activate, Selectors.filters.date.calendariconto, function() {\n updateCalendarPosition(Selectors.filters.date.calendariconto);\n });\n};\n"],"names":["root","jqRoot","document","ready","hide","removeClass","generateWithFilters","event","getparams","newLink","currentLink","forms","filtersform","action","preventDefault","currentSplit","split","currentstring","newparamsarray","paramsstring","paramkeys","paramvalues","forEach","param","splitparam","push","paramstring","newparam","existingkey","indexOf","name","key","substr","submit","on","target","search","selectperpage","onsubmit","getparam","elements","perpage","value","downloadForm","getElementById","querySelector","downloadType","getParams","prevAction","submitWithFilter","containerelement","FormChangeChecker","unWatchForm","addClass","updateCalendarPosition","referenceid","referenceElement","popperContent","Selectors","filters","date","calendar","style","removeProperty","placement","closeOpenFilters","openFilterButton","openFilter","classList","add","setAttribute","remove","CustomEvents","events","activate","group","selectall","querySelectorAll","checkbox","checked","clear","trigger","popover","closeListener","e","id","closest","keyCode","KeyCodes","enter","space","removeEventListener","escCloseListener","addEventListener","escape","save","popcheckbox","saveid","getAttribute","exportlink","link","exportbutton","exportlinkform","dataset","url","focus","filtersForm","datesPopover","fromEnabled","toEnabled","args","data","request","methodname","call","done","result","fromTimestamp","toTimestamp","timestamps","timestamp","warningdiv","calendariconfrom","calendariconto"],"mappings":";;;;;;;wgCA+BqBA,WACbC,QAAS,mBAAED,0BAIbE,UAAUC,OAAM,+BACZ,iBAAiBC,2BACjB,kBAAkBC,YAAY,mBAM9BC,oBAAsB,CAACC,MAAOC,iBAE5BC,QADAC,YAAcR,SAASS,MAAMC,YAAYC,UAGzCN,MAAO,CACPA,MAAMO,qBAEHC,aAAeL,YAAYM,MAAM,KACjCC,cAAgBF,aAAa,GAC7BG,eAAiBV,UAAUQ,MAAM,KACjCG,aAAe,GACfC,UAAY,GACZC,YAAc,GAGjBJ,cAAcD,MAAM,KAAKM,SAAQ,SAASC,WAClCC,WAAaD,MAAMP,MAAM,KAC7BI,UAAUK,KAAKD,WAAW,IAC1BH,YAAYI,KAAKD,WAAW,OAGhCN,eAAeI,SAAQ,SAASI,iBACxBC,SAAWD,YAAYV,MAAM,KAC7BY,YAAcR,UAAUS,QAAQF,SAAS,IAGzCC,aAAe,EACfP,YAAYO,aAAeD,SAAS,IAEpCP,UAAUK,KAAKE,SAAS,IACxBN,YAAYI,KAAKE,SAAS,QAKlCP,UAAUE,SAAQ,SAASQ,KAAMC,KAC7BZ,yBAAoBW,iBAAQT,YAAYU,SAG5CtB,QAAUM,aAAa,GAAK,IAAMI,aAAaa,OAAO,QAEtDvB,QAAUC,YAGdR,SAASS,MAAMC,YAAYC,OAASJ,QACpCP,SAASS,MAAMC,YAAYqB,8BAI7B,eAAeC,GAAG,QAAS,KAAK,SAAS3B,OACvCD,oBAAoBC,MAAOA,MAAM4B,OAAOC,OAAOJ,OAAO,2BAIxD,SAASE,GAAG,QAAS,KAAK,SAAS3B,OACjCD,oBAAoBC,MAAOA,MAAM4B,OAAOC,OAAOJ,OAAO,2BAIxD,eAAeE,GAAG,QAAS,KAAK,SAAS3B,OACvCD,oBAAoBC,MAAOA,MAAM4B,OAAOC,OAAOJ,OAAO,OAItD9B,SAASS,MAAM0B,gBACfnC,SAASS,MAAM0B,cAAcC,SAAY/B,YACjCgC,SAAW,WAAarC,SAASS,MAAM0B,cAAcG,SAASC,QAAQC,MAC1EpC,oBAAoBC,MAAOgC,kBAK7BI,aAAezC,SAAS0C,eAAe,iBAAiBC,cAAc,2BACxEF,eACAA,aAAaL,SAAY/B,cACfuC,aAAeH,aAAaE,cAAc,0BAA0BH,MACpEK,6BAAwBD,cACxBE,WAAa9C,SAASS,MAAMC,YAAYC,OAE9CP,oBAAoBC,MAAOwC,WAG3B7C,SAASS,MAAMC,YAAYC,OAASmC,mBAKtCC,iBAAoBC,mBAEtBC,kBAAkBC,YAAYlD,SAAS2C,cAAcK,kBAAkBL,cAAc,6BAEnFK,kBAAkBG,SAAS,UAG7B/C,qBAAoB,IAIlBgD,uBAA0BC,kBACxBC,iBAAmBtD,SAAS2C,cAAcU,aAC1CE,cAAgBvD,SAAS2C,cAAca,mBAAUC,QAAQC,KAAKC,UAElEJ,cAAcK,MAAMC,eAAe,oCACtBP,iBAAkBC,cAAe,CAACO,UAAW,gBAIxDC,iBAAmB,CAACC,iBAAkBC,cACxCA,WAAWC,UAAUC,IAAI,UACzBF,WAAWG,aAAa,kBAAmB,SAE3CJ,iBAAiBE,UAAUC,IAAI,eAC/BH,iBAAiBE,UAAUG,OAAO,uBAClCL,iBAAiBI,aAAa,iBAAiB,IAMnDrE,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQgB,MAAMC,WAAW,WACtD5E,KAAK6E,iBAAiBnB,mBAAUC,QAAQgB,MAAMG,SAAW,kBAC/DxD,SAAQ,SAASwD,UACxBA,SAASC,SAAU,QAK3B9E,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQgB,MAAMK,OAAO,WAEpDhF,KAAK6E,iBAAiBnB,mBAAUC,QAAQgB,MAAMG,SAAW,YAC/DxD,SAAQ,SAASwD,UACtBA,SAASC,SAAU,QAK3B9E,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQgB,MAAMM,SAAS,eAEjEzB,iBAAmBxD,KAAK6C,cAAca,mBAAUC,QAAQgB,MAAMM,SAC9DxB,cAAgBzD,KAAK6C,cAAca,mBAAUC,QAAQgB,MAAMO,kCAElD1B,iBAAkBC,cAAe,CAACO,UAAW,eAG1DP,cAAcW,UAAUG,OAAO,UAC/Bd,cAAca,aAAa,kBAAmB,QAG9Cd,iBAAiBY,UAAUC,IAAI,uBAC/Bb,iBAAiBY,UAAUG,OAAO,eAGlCf,iBAAiBc,aAAa,iBAAiB,SAGzCa,cAAgBC,IACdA,EAAEjD,OAAOkD,KAAO7B,iBAAiB6B,IAAM5B,gBAAkB2B,EAAEjD,OAAOmD,QAAQ,kCAChD,IAAdF,EAAEG,SAA2BH,EAAEG,UAAYC,mBAASC,OAASL,EAAEG,UAAYC,mBAASE,QAChGzB,iBAAiBT,iBAAkBC,eACnCvD,SAASyF,oBAAoB,QAASR,eACtCjF,SAASyF,oBAAoB,QAASR,eACtCjF,SAASyF,oBAAoB,QAASC,oBAI9C1F,SAAS2F,iBAAiB,QAASV,eACnCjF,SAAS2F,iBAAiB,QAASV,qBAE7BS,iBAAmBR,IACjBA,EAAEG,UAAYC,mBAASM,SACvB7B,iBAAiBT,iBAAkBC,eACnCvD,SAASyF,oBAAoB,QAASC,kBACtC1F,SAASyF,oBAAoB,QAASR,iBAI9CjF,SAAS2F,iBAAiB,QAASD,qBAIvC3F,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQgB,MAAMoB,MAAM,WAE9C/F,KAAK6E,iBAAiBnB,mBAAUC,QAAQgB,MAAMG,UAEpDxD,SAAQ,SAAS0E,iBACvBpF,YAAcV,SAASS,MAAMC,YAC7BqF,OAASD,YAAYE,aAAa,eAEtCtF,YAAYiC,yBAAkBoD,SAAUlB,QAAUiB,YAAYjB,WAGlE9B,iBAAiB,6BAOrB/C,SAAS2E,iBAAiBnB,mBAAUC,QAAQwC,WAAWC,MAAM9E,SAAQ,SAAS+E,cAC1EA,aAAaR,iBAAiB,SAAS,SAAStF,OAC5CL,SAASS,MAAM2F,eAAezF,OAASN,MAAM4B,OAAOoE,QAAQC,IAC5DtG,SAASS,MAAM2F,eAAerE,eAOtChC,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQC,KAAKqB,SAAS,eAGhEzB,iBAAmBxD,KAAK6C,cAAca,mBAAUC,QAAQC,KAAKqB,SAC7DxB,cAAgBzD,KAAK6C,cAAca,mBAAUC,QAAQC,KAAKsB,kCAEjD1B,iBAAkBC,cAAe,CAACO,UAAW,eAG1DP,cAAcW,UAAUG,OAAO,UAC/Bd,cAAca,aAAa,kBAAmB,QAC9Cb,cAAcZ,cAAc,2CAA2C4D,QAGvEjD,iBAAiBY,UAAUC,IAAI,uBAC/Bb,iBAAiBY,UAAUG,OAAO,eAGlCf,iBAAiBc,aAAa,iBAAiB,SAGzCa,cAAgBC,IACdA,EAAEjD,OAAOkD,KAAO7B,iBAAiB6B,IAAM5B,gBAAkB2B,EAAEjD,OAAOmD,QAAQ,kCAChD,IAAdF,EAAEG,SAA2BH,EAAEG,UAAYC,mBAASC,OAASL,EAAEG,UAAYC,mBAASE,QAChGzB,iBAAiBT,iBAAkBC,eACnCvD,SAASyF,oBAAoB,QAASR,eACtCjF,SAASyF,oBAAoB,QAASR,eACtCjF,SAASyF,oBAAoB,QAASC,oBAI9C1F,SAAS2F,iBAAiB,QAASV,eACnCjF,SAAS2F,iBAAiB,QAASV,qBAE7BS,iBAAmBR,IACjBA,EAAEG,UAAYC,mBAASM,SACvB7B,iBAAiBT,iBAAkBC,eACnCvD,SAASyF,oBAAoB,QAASC,kBACtC1F,SAASyF,oBAAoB,QAASR,iBAI9CjF,SAAS2F,iBAAiB,QAASD,qBAIvC3F,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQC,KAAKmC,MAAM,eAE7DW,YAAcxG,SAASS,MAAMC,kBAC3B+F,aAAe3G,KAAK6C,cAAca,mBAAUC,QAAQC,KAAKsB,SACzD0B,YAAcD,aAAa9D,cAAc,2CAA2CkC,QAAU,EAAI,EAClG8B,UAAYF,aAAa9D,cAAc,yCAAyCkC,QAAU,EAAI,KAE/F6B,aAAgBC,UASd,KACCC,KAAO,CAACC,KAAM,IAEdH,aACAE,KAAKC,KAAKtF,KAAK,KACJ,YACCkF,aAAa9D,cAAc,wCAAwCH,YAClEiE,aAAa9D,cAAc,yCAAyCH,UACtEiE,aAAa9D,cAAc,uCAAuCH,WACjE,SACE,IAIdmE,WACAC,KAAKC,KAAKtF,KAAK,KACJ,UACCkF,aAAa9D,cAAc,sCAAsCH,YAChEiE,aAAa9D,cAAc,uCAAuCH,UACpEiE,aAAa9D,cAAc,qCAAqCH,WAC/D,UACE,WAIZsE,QAAU,CACZC,WAAY,+BACZH,KAAMA,oBAGLI,KAAK,CAACF,UAAU,GAAGG,MAAK,SAASC,YAC9BC,cAAgB,EAChBC,YAAc,KAElBF,OAAOG,WAAWjG,SAAQ,SAASyF,MACd,SAAbA,KAAKhF,IACLsF,cAAgBN,KAAKS,UACD,OAAbT,KAAKhF,MACZuF,YAAcP,KAAKS,cAKvBF,YAAc,GAAKD,cAAgBC,YAAa,OAC1CG,WAAavH,SAAS0C,eAAe,wBAC3C6E,WAAWrD,UAAUG,OAAO,UAC5BkD,WAAWrD,UAAUC,IAAI,gBAEzBqC,YAAYlE,SAAS,uBAAuBE,MAAQ2E,cACpDX,YAAYlE,SAAS,qBAAqBE,MAAQkE,YAClDF,YAAYlE,SAAS,qBAAqBE,MAAQ4E,YAClDZ,YAAYlE,SAAS,mBAAmBE,MAAQmE,UAGhD5D,iBAAiB,iCA7DzByD,YAAYlE,SAAS,uBAAuBE,MAAQ,EACpDgE,YAAYlE,SAAS,qBAAqBE,MAAQkE,YAClDF,YAAYlE,SAAS,qBAAqBE,MAAQ,EAClDgE,YAAYlE,SAAS,mBAAmBE,MAAQmE,UAGhD5D,iBAAiB,4BA6DzBhD,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQC,KAAK8D,kBAAkB,WAC7EpE,uBAAuBI,mBAAUC,QAAQC,KAAK8D,qBAGlDzH,OAAOiC,GAAGsC,mCAAaC,OAAOC,SAAUhB,mBAAUC,QAAQC,KAAK+D,gBAAgB,WAC3ErE,uBAAuBI,mBAAUC,QAAQC,KAAK+D"}PKW\)n#kk#summary/tests/event/events_test.phpnu[. /** * Tests for forum report summary events. * * @package forumreport_summary * @category test * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace forumreport_summary\event; /** * Tests for forum report summary events. * * @package forumreport_summary * @category test * @copyright 2019 Michael Hawkins * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class events_test extends \advanced_testcase { /** * Test report_downloaded event. */ public function test_report_downloaded(): void { global $DB; $this->resetAfterTest(); // Create course and teacher user. $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_user(); $roleteacher = $DB->get_record('role', ['shortname' => 'teacher']); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $roleteacher->id); // Create forum. $this->setUser($teacher); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $context = \context_module::instance($forum->cmid); // Trigger and capture event. $eventparams = [ 'context' => $context, 'other' => [ 'forumid' => $forum->id, 'hasviewall' => true, ], ]; $event = \forumreport_summary\event\report_downloaded::create($eventparams); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $sink->close(); // Check the event contains the expected data. $this->assertInstanceOf('\forumreport_summary\event\report_downloaded', $event); $this->assertEquals($context, $event->get_context()); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($forum->cmid, $event->contextinstanceid); $this->assertEquals($teacher->id, $event->userid); $url = new \moodle_url('/mod/forum/report/summary/index.php', ['courseid' => $course->id, 'forumid' => $forum->id]); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); $this->assertNotEmpty($event->get_description()); } /** * Test report_viewed event. */ public function test_report_viewed(): void { global $DB; $this->resetAfterTest(); // Create course and teacher user. $course = $this->getDataGenerator()->create_course(); $teacher = $this->getDataGenerator()->create_user(); $roleteacher = $DB->get_record('role', ['shortname' => 'teacher']); $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $roleteacher->id); // Create forum. $this->setUser($teacher); $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]); $context = \context_module::instance($forum->cmid); // Trigger and capture event. $eventparams = [ 'context' => $context, 'other' => [ 'forumid' => $forum->id, 'hasviewall' => true, ], ]; $event = \forumreport_summary\event\report_viewed::create($eventparams); $sink = $this->redirectEvents(); $event->trigger(); $events = $sink->get_events(); $this->assertCount(1, $events); $event = reset($events); $sink->close(); // Check the event contains the expected data. $this->assertInstanceOf('\forumreport_summary\event\report_viewed', $event); $this->assertEquals($context, $event->get_context()); $this->assertEquals(CONTEXT_MODULE, $event->contextlevel); $this->assertEquals($forum->cmid, $event->contextinstanceid); $this->assertEquals($teacher->id, $event->userid); $url = new \moodle_url('/mod/forum/report/summary/index.php', ['courseid' => $course->id, 'forumid' => $forum->id]); $this->assertEquals($url, $event->get_url()); $this->assertEventContextNotUsed($event); $this->assertNotEmpty($event->get_name()); $this->assertNotEmpty($event->get_description()); } } PKW\E (summary/tests/behat/bulk_message.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Message users in the summary report In order to encourage users to participate As a teacher I should be able to send messages to those who are not taking part 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 | 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 | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | | teacher1 | forum1 | discussion1 | t1 earliest | | teacher1 | forum1 | discussion2 | t1 between | | student1 | forum1 | discussion3 | s1 latest | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | | teacher1 | forum1 | discussion1 | t1 between | | teacher1 | forum1 | discussion2 | t1 latest | | student1 | forum1 | discussion1 | s1 earliest | @javascript Scenario: Message some users Given the following "users" exist: | username | firstname | lastname | email | | student3 | Student | 3 | student3@example.com | And the following "course enrolments" exist: | user | course | role | | student3 | C1 | student | When I am on the forum1 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Select 'Student 1'" "checkbox" And I click on "Select 'Student 3'" "checkbox" And I set the field "With selected users..." to "Send a message" And I set the field "bulk-message" to "blah blah" And I click on "Send message to 2 people" "button" Then I should see "Message sent to 2 people" And I log out And I log in as "student1" And I should see "1" in the "Toggle messaging drawer" "button" And I log out And I log in as "student3" And I should see "1" in the "Toggle messaging drawer" "button" And I log out And I log in as "student2" And I should not see "1" in the "Toggle messaging drawer" "button" @javascript Scenario: Message all users When I am on the forum1 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration And I click on "Select all" "checkbox" And I set the field "With selected users..." to "Send a message" Then I should see "Send message to 3 people" @javascript Scenario: Ensure no message options when messaging is disabled Given the following config values are set as admin: | messaging | 0 | When I am on the forum1 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then I should not see "With selected users..." And I should not see "Select all" PKW\ i#/summary/tests/behat/summary_data_access.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Report relevant content availability In order to view the appropriate report content As a teacher or student I need to have the associated capabilities 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 | 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 | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | message 1 | | | | student1 | forum1 | discussion2 | message 3 | att1.jpg | in1.jpg | | student2 | forum1 | discussion3 | message 4 | att3.jpg | | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | attachments | inlineattachments | | student1 | forum1 | discussion1 | reply1 | att4.jpg, att5.jpg | in2.jpg | | student2 | forum1 | discussion2 | reply2 | | | @javascript Scenario: Teachers can access report data about other users by default Given I am on the "forum1" "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then the following should exist in the "forumreport_summary_table" table: | -2- | -3- | -4- | -5- | -6- | -7- | -8- | | Student 1 | 1 | 1 | 5 | 0 | 3 | 14 | | Student 2 | 1 | 1 | 1 | 0 | 3 | 14 | | Teacher 1 | 1 | 0 | 0 | 1 | 2 | 8 | And "select-all-users" "checkbox" should be visible And "First name" "link" should be visible And "Last name" "link" should be visible And "Number of discussions posted" "link" should be visible And "Number of replies posted" "link" should be visible And "Number of attachments" "link" should be visible And "Number of views" "link" should be visible And "Word count" "link" should be visible And "Character count" "link" should be visible And "Earliest post" "link" should be visible And "Most recent post" "link" should be visible And I should see "Export posts" And "Export posts" "link" should not exist Scenario: Students cannot access the summary report by default Given I am on the "forum1" "forum activity" page logged in as student1 Then "Forum summary report" "link" should not exist in current page administration @javascript Scenario: Students given the view capability can only view their own report data Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | forumreport/summary:view | Allow | student | Course | C1 | When I am on the "forum1" "forum activity" page logged in as student1 And I navigate to "Reports" in current page administration Then the following should exist in the "forumreport_summary_table" table: | -1- | -2- | -3- | -4- | -5- | -6- | -7- | | Student 1 | 1 | 1 | 5 | 1 | 3 | 14 | And the following should not exist in the "forumreport_summary_table" table: | -1- | | Student 2 | | Teacher 1 | And "select-all-users" "checkbox" should not exist And "First name" "link" should be visible And "Last name" "link" should be visible And "Number of discussions posted" "link" should be visible And "Number of replies posted" "link" should be visible And "Number of attachments" "link" should be visible And "Number of views" "link" should be visible And "Word count" "link" should be visible And "Character count" "link" should be visible And "Earliest post" "link" should be visible And "Most recent post" "link" should be visible And I should not see "Export posts" PKW\Ww  4summary/tests/behat/summary_filter_no_groups.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Groups report filter is not available if no groups exist When no groups exist As a teacher I can view the forum summary report for all users of a forum Scenario: Report data is available without groups filter if no groups exist Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C2 | editingteacher | And the following "groups" exist: | name | course | idnumber | | Group A | C2 | G1 | And the following "group members" exist: | user | group | | teacher1 | G1 | And the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | forum1 | C1 | forum1C1 | 0 | | forum | forum2 | C1 | forum2C1 | 0 | | forum | forum1 | C2 | forum1C2 | 2 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | created | | teacher1 | forum1 | discussion1 | D1 message | ## 1 month ago ## | | teacher1 | forum1 | discussion2 | D2 message | ## 1 week ago ## | | teacher1 | forum2 | discussion3 | D3 message | ## 4 days ago ## | | student1 | forum1 | discussion4 | D4 message | ## 3 days ago ## | | student2 | forum2 | discussion5 | D5 message | ## 2 days ago## | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | created | | teacher1 | forum1 | discussion1 | D1 reply | ## 3 weeks ago ## | | teacher1 | forum1 | discussion2 | D2 reply | ## 6 days ago ## | | teacher1 | forum2 | discussion3 | D3 reply | ## 3 days ago ## | | student1 | forum1 | discussion1 | D1 reply 2 | ## 2 weeks ago ## | | student2 | forum2 | discussion3 | D3 reply | ## 2 days ago ## | And the following forum discussions exist in course "Course 2": | user | forum | name | message | created | | teacher1 | forum1 | discussion1 | D1 other course | ## 1 week ago ## | | teacher1 | forum1 | discussion2 | D2 other course | ## 4 days ago ## | When I am on the forum1C1 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then "Groups" "button" should not exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | | First name | -3- | | Teacher 1 | 2 | | Student 1 | 1 | | Student 2 | 0 | PKW\aF993summary/tests/behat/summary_data_post_dates.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Post date columns data available In order to determine users' earliest and most recent forum posts As a teacher I need to view that data in the forum summary report Scenario: Add posts and view accurate summary report Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C2 | editingteacher | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1C1 | | forum | forum2 | C1 | forum2C1 | | forum | forum1 | C2 | forum1C2 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | created | | teacher1 | forum1 | discussion1 | t1 earliest | ##2018-01-02 09:00:00## | | teacher1 | forum1 | discussion2 | t1 between | ##2018-03-27 10:00:00## | | teacher1 | forum2 | discussion3 | t1 other forum | ##2018-01-01 11:00:00## | | student1 | forum1 | discussion4 | s1 latest | ##2019-03-27 13:00:00## | | student2 | forum2 | discussion5 | s2 other forum | ##2018-03-27 09:00:00## | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | created | | teacher1 | forum1 | discussion1 | t1 between | ##2018-01-02 10:30:00## | | teacher1 | forum1 | discussion2 | t1 latest | ##2019-09-01 07:00:00## | | teacher1 | forum2 | discussion3 | t1 other forum | ##2019-09-12 08:00:00## | | student1 | forum1 | discussion1 | s1 earliest | ##2019-03-27 04:00:00## | | student2 | forum2 | discussion3 | s2 other forum | ##2018-03-27 10:00:00## | And the following forum discussions exist in course "Course 2": | user | forum | name | message | created | | teacher1 | forum1 | discussion1 | t1 other course | ##2017-01-01 03:00:00## | | teacher1 | forum1 | discussion2 | t1 other course | ##2019-09-13 23:59:00## | When I am on the forum1C1 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then "Teacher 1" row "Earliest post" column of "forumreport_summary_table" table should contain "Tuesday, 2 January 2018, 9:00" Then "Teacher 1" row "Most recent post" column of "forumreport_summary_table" table should contain "Sunday, 1 September 2019, 7:00" Then "Student 1" row "Earliest post" column of "forumreport_summary_table" table should contain "Wednesday, 27 March 2019, 4:00" Then "Student 1" row "Most recent post" column of "forumreport_summary_table" table should contain "Wednesday, 27 March 2019, 1:00" Then "Student 2" row "Earliest post" column of "forumreport_summary_table" table should contain "-" Then "Student 2" row "Most recent post" column of "forumreport_summary_table" table should contain "-" PKW\;^!^!*summary/tests/behat/course_summary.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Course level forum summary report In order to gain an overview of students' forum activities across a course As a teacher I should be able to prepare a summary report of all forums in a course 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 | And the following "courses" exist: | fullname | shortname | | Course 1 | C1 | | Course 2 | C2 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | teacher1 | C2 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | student2 | C2 | student | | student3 | C2 | student | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1 | | forum | forum2 | C1 | forum2 | | forum | forum3 | C1 | forum3 | | forum | forum4 | C2 | forum4 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | created | | teacher1 | forum1 | discussion1 | Discussion 1 | ##2018-01-14 09:00:00## | | teacher1 | forum2 | discussion2 | Discussion 2 | ##2019-03-27 12:10:00## | | teacher1 | forum3 | discussion3 | Discussion 3 | ##2019-12-25 15:20:00## | | teacher1 | forum3 | discussion4 | Discussion 4 | ##2019-12-26 09:30:00## | | student1 | forum2 | discussion5 | Discussion 5 | ##2019-06-06 18:40:00## | | student1 | forum3 | discussion6 | Discussion 6 | ##2020-01-25 11:50:00## | And the following forum replies exist in course "Course 1": | user | forum | discussion | subject | message | created | | teacher1 | forum1 | discussion1 | Re d1 | Reply 1 | ##2018-02-15 11:10:00## | | teacher1 | forum2 | discussion5 | Re d5 | Reply 2 | ##2019-06-09 18:20:00## | | teacher1 | forum2 | discussion5 | Re d5 | Reply 3 | ##2019-07-10 09:30:00## | | student1 | forum1 | discussion1 | Re d1 | Reply 4 | ##2018-01-25 16:40:00## | | student1 | forum2 | discussion2 | Re d6 | Reply 5 | ##2019-03-28 11:50:00## | | student1 | forum3 | discussion4 | Re d4 | Reply 6 | ##2019-12-30 20:00:00## | And the following forum discussions exist in course "Course 2": | user | forum | name | message | created | | teacher1 | forum4 | discussion7 | Discussion 7 | ##2020-01-29 15:00:00## | | student2 | forum4 | discussion8 | Discussion 8 | ##2020-02-02 16:00:00## | And the following forum replies exist in course "Course 2": | user | forum | discussion | subject | message | created | | teacher1 | forum4 | discussion8 | Re d8 | Reply 7 | ##2020-02-03 09:45:00## | | student2 | forum4 | discussion7 | Re d7 | Reply 8 | ##2020-02-04 13:50:00## | Scenario: Course forum summary report can be viewed by teacher and contains accurate data When I am on the forum2 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration And I should see "Export posts" And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | | | First name | -3- | -4- | Earliest post | Most recent post | | Student 1 | 1 | 1 | Thursday, 28 March 2019, 11:50 | Thursday, 6 June 2019, 6:40 | | Student 2 | 0 | 0 | - | - | | Teacher 1 | 1 | 2 | Wednesday, 27 March 2019, 12:10 | Wednesday, 10 July 2019, 9:30 | And the following should not exist in the "forumreport_summary_table" table: | First name | | Student 3 | And the "Forum selected" select box should contain "All forums in course" And the "Forum selected" select box should contain "forum1" And the "Forum selected" select box should contain "forum2" And the "Forum selected" select box should contain "forum3" And the "Forum selected" select box should not contain "forum4" Then I select "All forums in course" from the "Forum selected" singleselect And I should not see "Export posts" And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | | | First name | -3- | -4- | Earliest post | Most recent post | | Student 1 | 2 | 3 | Thursday, 25 January 2018, 4:40 | Saturday, 25 January 2020, 11:50 | | Student 2 | 0 | 0 | - | - | | Teacher 1 | 4 | 3 | Sunday, 14 January 2018, 9:00 | Thursday, 26 December 2019, 9:30 | And the following should not exist in the "forumreport_summary_table" table: | First name | | Student 3 | Scenario: Course forum summary report correctly formats forum activity names Given the "multilang" filter is "on" And the "multilang" filter applies to "content and headings" And the following "activity" exists: | activity | forum | | course | C1 | | name | F-EngF-Ger | When I am on the "C1" "Course" page logged in as "teacher1" And I follow "F-Eng" And I navigate to "Reports" in current page administration Then the "Forum selected" select box should contain "F-Eng" Scenario: Students given the view capability can view their own course report data Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | forumreport/summary:view | Allow | student | Course | C1 | When I am on the "forum1" "forum activity" page logged in as student1 And I navigate to "Reports" in current page administration And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | | | First name | -2- | -3- | Earliest post | Most recent post | | Student 1 | 0 | 1 | Thursday, 25 January 2018, 4:40 | Thursday, 25 January 2018, 4:40 | And the following should not exist in the "forumreport_summary_table" table: | First name | | Student 2 | | Student 3 | | Teacher 1 | And the "Forum selected" select box should contain "All forums in course" And the "Forum selected" select box should contain "forum1" And the "Forum selected" select box should contain "forum2" And the "Forum selected" select box should contain "forum3" And the "Forum selected" select box should not contain "forum4" Then I select "All forums in course" from the "Forum selected" singleselect And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | | | First name | -2- | -3- | Earliest post | Most recent post | | Student 1 | 2 | 3 | Thursday, 25 January 2018, 4:40 | Saturday, 25 January 2020, 11:50 | And the following should not exist in the "forumreport_summary_table" table: | First name | | Student 2 | | Student 3 | | Teacher 1 | PKW\$(j j 4summary/tests/behat/summary_data_attachments.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Attachments count column data available In order to gather data on users' forum attachments As a teacher I need to view accurate attachment count data in the forum summary report Scenario: Add discussions and replies with attached files Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C2 | editingteacher | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1C1 | | forum | forum2 | C1 | forum2C1 | | forum | forum1 | C2 | forum1C2 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | message1 | att1.jpg, att2.txt | | | teacher1 | forum2 | discussion2 | message2 | att3.jpg | in1.jpg | | student1 | forum1 | discussion3 | message3 | att4.jpg | in2.jpg | | student2 | forum1 | discussion4 | message4 | | | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | reply1 | att5.jpg | in3.txt | | teacher1 | forum1 | discussion1 | reply2 | att5.jpg | in3.txt | | teacher1 | forum2 | discussion2 | reply2 | att6.jpg | | | student1 | forum1 | discussion3 | reply3 | att7.jpg, att8.jpg | in2.jpg | | student2 | forum1 | discussion4 | reply4 | | | And the following forum discussions exist in course "Course 2": | user | forum | name | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | message1 | att1.jpg, att2.txt | | When I am on the forum1C1 "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then "Teacher 1" row "Number of attachments" column of "forumreport_summary_table" table should contain "6" And "Student 1" row "Number of attachments" column of "forumreport_summary_table" table should contain "5" And "Student 2" row "Number of attachments" column of "forumreport_summary_table" table should contain "0" PKW\>\:summary/tests/behat/double_enrolments_summary_data.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: forum report shows post/reply/word counts correctly In order to gather data on users' forum activities As a teacher I need to view accurate forum summary report when students have more than 1 enrolment Scenario: Add discussions and replies with attached files Given the following "users" exist: | username | firstname | lastname | email | | teacher1 | Teacher | 1 | teacher1@example.com | | student1 | Student | 1 | student1@example.com | | student2 | Student | 2 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "course enrolments" exist: | user | course | role | enrol | | teacher1 | C1 | editingteacher | manual | | student1 | C1 | student | manual | | student1 | C1 | student | self | | student2 | C1 | student | manual | | student2 | C1 | student | self | | teacher1 | C2 | editingteacher | manual | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1 | | forum | forum2 | C1 | forum2 | | forum | forum1 | C2 | forum1 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | message1 | att1.jpg, att2.txt | | | teacher1 | forum2 | discussion2 | message2 | att3.jpg | in1.jpg | | student1 | forum1 | discussion3 | my message3 | att4.jpg | in2.jpg | | student2 | forum1 | discussion4 | my message4 | | | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | reply1 | att5.jpg | in3.txt | | teacher1 | forum1 | discussion1 | reply2 | att5.jpg | in3.txt | | teacher1 | forum2 | discussion2 | reply2 | att6.jpg | | | student1 | forum1 | discussion3 | my reply3 | att7.jpg, att8.jpg | in2.jpg | | student2 | forum1 | discussion4 | my reply4 | | | | student2 | forum1 | discussion4 | my reply5 | | | And the following forum discussions exist in course "Course 2": | user | forum | name | message | attachments | inlineattachments | | teacher1 | forum1 | discussion1 | message1 | att1.jpg, att2.txt | | When I am on the "Course 1" course page logged in as teacher1 And I follow "forum1" And I navigate to "Reports" in current page administration Then "Teacher 1" row "Number of attachments" column of "forumreport_summary_table" table should contain "6" And "Student 1" row "Number of attachments" column of "forumreport_summary_table" table should contain "5" And "Student 1" row "Word count" column of "forumreport_summary_table" table should contain "4" And "Student 1" row "Character count" column of "forumreport_summary_table" table should contain "18" And "Student 1" row "Number of discussions posted" column of "forumreport_summary_table" table should contain "1" And "Student 1" row "Number of replies posted" column of "forumreport_summary_table" table should contain "1" And "Student 2" row "Number of attachments" column of "forumreport_summary_table" table should contain "0" And "Student 2" row "Word count" column of "forumreport_summary_table" table should contain "6" And "Student 2" row "Character count" column of "forumreport_summary_table" table should contain "26" And "Student 2" row "Number of discussions posted" column of "forumreport_summary_table" table should contain "1" And "Student 2" row "Number of replies posted" column of "forumreport_summary_table" table should contain "2" PKW\8V' ' +summary/tests/behat/private_replies.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Include private replies in the summary report In order to generate accurate reports based on what is visible As a teacher I should have private replies being counted as well 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 | | student2 | Student | 2 | 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 | | teacher2 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | And the following "activities" exist: | activity | name | course | idnumber | | forum | forum1 | C1 | forum1 | And the following "mod_forum > discussions" exist: | user | forum | name | message | | teacher1 | forum1 | discussion1 | t1 earliest | | teacher1 | forum1 | discussion2 | t1 between | | teacher1 | forum1 | discussion3 | s1 latest | And the following "mod_forum > posts" exist: | user | parentsubject | subject | message | privatereplyto | | teacher1 | discussion1 | t1 between | t1 between | | | teacher1 | discussion2 | t1 latest | t1 latest | | | student1 | discussion1 | s1 earliest | s1 earliest | | | teacher1 | s1 earliest | This is a private reply | This is a private reply | 1 | Scenario: Private replies are counted for Teacher When I am on the forum1 "forum activity" page logged in as teacher2 And I navigate to "Reports" in current page administration Then "Teacher 1" row "Number of replies posted" column of "forumreport_summary_table" table should contain "3" Scenario: Private replies are not counted when Teacher has not capability Given the following "permission overrides" exist: | capability | permission | role | contextlevel | reference | | mod/forum:readprivatereplies | Prevent | editingteacher | Course | C1 | When I am on the forum1 "forum activity" page logged in as teacher2 And I navigate to "Reports" in current page administration Then "Teacher 1" row "Number of replies posted" column of "forumreport_summary_table" table should contain "2" PKW\bs 3 31summary/tests/behat/summary_filter_groups.featurenu[@mod @mod_forum @forumreport @forumreport_summary Feature: Groups report filter is available if groups exist In order to retrieve targeted forum data As a teacher I can filter the forum summary report by groups of users 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 | student1@example.com | And the following "courses" exist: | fullname | shortname | category | | Course 1 | C1 | 0 | | Course 2 | C2 | 0 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | | student2 | C1 | student | | teacher1 | C2 | editingteacher | And the following "groups" exist: | name | course | idnumber | | Group A | C1 | G1 | | Group B | C1 | G2 | | Group C | C1 | G3 | | Group D | C1 | G4 | | Group E | C2 | G5 | And the following "group members" exist: | user | group | | teacher1 | G1 | | teacher1 | G2 | | teacher1 | G3 | | student1 | G3 | And the following "activities" exist: | activity | name | course | idnumber | groupmode | | forum | forum1 | C1 | c1forum1 | 1 | | forum | forum2 | C1 | c1forum2 | 2 | | forum | forum1 | C2 | c2forum1 | 0 | And the following forum discussions exist in course "Course 1": | user | forum | name | message | group | created | | teacher1 | forum1 | discussion1 | D1 message | G1 | ## 1 month ago ## | | teacher1 | forum1 | discussion2 | D2 message | G2 | ## 1 week ago ## | | teacher1 | forum2 | discussion3 | D3 message | G1 | ## 6 days ago ## | | teacher1 | forum2 | discussion4 | D4 message | G4 | ## 5 days ago ## | | teacher1 | forum2 | discussion5 | D5 message | All participants | ## 4 days ago ## | | student1 | forum1 | discussion6 | D6 message | G3 | ## 3 days ago ## | | student2 | forum2 | discussion7 | D7 message | All participants | ## 2 days ago ## | And the following forum replies exist in course "Course 1": | user | forum | discussion | message | created | | teacher1 | forum1 | discussion1 | D1 reply | ## 3 weeks ago ## | | teacher1 | forum2 | discussion3 | D3 reply | ## 4 days ago ## | | teacher1 | forum1 | discussion6 | D6 reply | ## 2 days ago ## | | student1 | forum1 | discussion6 | D6 reply 2 | ## 2 days ago ## | | student2 | forum2 | discussion4 | D4 reply | ## 4 days ago ## | | student2 | forum2 | discussion5 | D5 reply | ## 3 days ago ## | And the following forum discussions exist in course "Course 2": | user | forum | name | message | created | | teacher1 | forum1 | discussion1 | D1 other course | ## 1 week ago ## | | teacher1 | forum1 | discussion2 | D2 other course | ## 4 days ago ## | @javascript Scenario: All groups can be selected or cleared together in the groups filter, and are unchecked by default When I am on the "c1forum1" "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then "Groups" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Student 2 | 0 | 0 | | Teacher 1 | 2 | 2 | And I click on "Groups" "button" And "Group A" "checkbox" should exist in the "filter-groups-popover" "region" And "Group B" "checkbox" should exist in the "filter-groups-popover" "region" And "Group C" "checkbox" should exist in the "filter-groups-popover" "region" And "Group D" "checkbox" should exist in the "filter-groups-popover" "region" And "No groups" "checkbox" should exist in the "filter-groups-popover" "region" And "Group E" "checkbox" should not exist in the "filter-groups-popover" "region" And the following fields match these values: | Group A | 0 | | Group B | 0 | | Group C | 0 | | Group D | 0 | | No groups | 0 | And I click on "Select all" "button" in the "filter-groups-popover" "region" And the following fields match these values: | Group A | 1 | | Group B | 1 | | Group C | 1 | | Group D | 1 | | No groups | 1 | And I click on "Clear" "button" in the "filter-groups-popover" "region" And the following fields match these values: | Group A | 0 | | Group B | 0 | | Group C | 0 | | Group D | 0 | | No groups | 0 | And I click on "Select all" "button" in the "filter-groups-popover" "region" And I click on "Save" "button" in the "filter-groups-popover" "region" And "Groups (all)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Student 2 | 0 | 0 | | Teacher 1 | 2 | 2 | @javascript Scenario: The summary report can be filtered by a subset of groups, and re-ordering the results retains the filter When I am on the "c1forum1" "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then "Groups" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Student 2 | 0 | 0 | | Teacher 1 | 2 | 2 | And I click on "Groups" "button" And I click on "Clear" "button" in the "filter-groups-popover" "region" And I click on "Group A" "checkbox" in the "filter-groups-popover" "region" And I click on "Group C" "checkbox" in the "filter-groups-popover" "region" And I click on "Group D" "checkbox" in the "filter-groups-popover" "region" And I click on "Save" "button" And "Groups (3)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Teacher 1 | 1 | 2 | And I should not see "Student 2" # Ensure re-ordering retains filter. And I click on "Number of discussions posted" "link" And "Groups (3)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Teacher 1 | 1 | 2 | And I should not see "Student 2" @javascript Scenario: The summary report can be filtered as a mixture of groups and no groups When I am on the "c1forum2" "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then "Groups" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 0 | 0 | | Student 2 | 1 | 2 | | Teacher 1 | 3 | 1 | And I click on "Groups" "button" And I click on "Clear" "button" in the "filter-groups-popover" "region" And I click on "Group A" "checkbox" in the "filter-groups-popover" "region" And I click on "No groups" "checkbox" in the "filter-groups-popover" "region" And I click on "Save" "button" in the "filter-groups-popover" "region" And "Groups (2)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 0 | 0 | | Student 2 | 1 | 1 | | Teacher 1 | 2 | 1 | @javascript Scenario: The summary report can be filtered by no groups only When I am on the "c1forum2" "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration Then the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 0 | 0 | | Student 2 | 1 | 2 | | Teacher 1 | 3 | 1 | And I click on "Groups" "button" And I click on "Clear" "button" in the "filter-groups-popover" "region" And I click on "No groups" "checkbox" in the "filter-groups-popover" "region" And I click on "Save" "button" in the "filter-groups-popover" "region" And "Groups (1)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 0 | 0 | | Student 2 | 1 | 1 | | Teacher 1 | 1 | 0 | @javascript Scenario: Filtering by a group containing no users still allows the page to render # Log in as admin so Teacher 1 not existing on page can be confirmed. When I am on the "c1forum1" "forum activity" page logged in as admin And I navigate to "Reports" in current page administration Then "Groups" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Student 2 | 0 | 0 | | Teacher 1 | 2 | 2 | And I click on "Groups" "button" And I click on "Clear" "button" in the "filter-groups-popover" "region" And I click on "Group D" "checkbox" in the "filter-groups-popover" "region" And I click on "Save" "button" in the "filter-groups-popover" "region" And "Groups (1)" "button" should exist And I should see "Nothing to display" And I should not see "Teacher 1" And I should not see "Student 1" And I should not see "Student 2" And I should not see "With selected users..." And I should not see "Download table data as" @javascript Scenario: Course forum summary report can be filtered by group When I am on the "c1forum2" "forum activity" page logged in as teacher1 And I navigate to "Reports" in current page administration And I select "All forums in course" from the "Forum selected" singleselect And I click on "Groups" "button" And I click on "Clear" "button" in the "filter-groups-popover" "region" And I click on "Group A" "checkbox" in the "filter-groups-popover" "region" And I click on "Group C" "checkbox" in the "filter-groups-popover" "region" And I click on "Save" "button" And "Groups (2)" "button" should exist Then the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Teacher 1 | 2 | 3 | And I should not see "Student 2" # Ensure re-ordering retains filter. And I click on "Number of discussions posted" "link" And "Groups (2)" "button" should exist And the following should exist in the "forumreport_summary_table" table: # | | Discussions | Replies | | First name | -3- | -4- | | Student 1 | 1 | 1 | | Teacher 1 | 2 | 3 | And I should not see "Student 2" PK{\progress/styles.cssnu[PK{\rprogress/db/install.phpnu[PK{\6% progress/db/access.phpnu[PK{\ֆmgg${progress/lang/en/report_progress.phpnu[PK{\i{  6progress/lib.phpnu[PK{\5؉y!progress/version.phpnu[PK{\DVc^%F&progress/classes/privacy/provider.phpnu[PK{\\ !,progress/classes/local/helper.phpnu[PK{\հE$h9progress/classes/output/renderer.phpnu[PK{\ζ٫>>lOprogress/index.phpnu[PK{\D[W'?progress/amd/src/completion_override.jsnu[PK{\ !2 -progress/amd/build/completion_override.min.jsnu[PK{\3>**1progress/amd/build/completion_override.min.js.mapnu[PK{\g %%.progress/tests/report_progress_helper_test.phpnu[PK{\ 3progress/tests/behat/course_report_progress.featurenu[PK{\FF>'progress/tests/behat/activity_completion_report_filter.featurenu[PK{\-H  7progress/tests/behat/activity_completion_report.featurenu[PK{\rМG9view.phpnu[PK{\cc&Bconfiglog/lang/en/report_configlog.phpnu[PK{\VŌHconfiglog/version.phpnu[PK{\U1f]]@Mconfiglog/classes/reportbuilder/local/entities/config_change.phpnu[PK{\0eZPPFrhconfiglog/classes/reportbuilder/local/systemreports/config_changes.phpnu[PK{\U&8yconfiglog/classes/privacy/provider.phpnu[PK{\A$$}configlog/index.phpnu[PK{\y )configlog/tests/behat/view_report.featurenu[PK{\fOPPconfiglog/settings.phpnu[PK{\log/styles.cssnu[PK{\klog/db/install.phpnu[PK{\#Blog/db/access.phpnu[PK{\Zlog/lang/en/report_log.phpnu[PK{\  usersessions/lib.phpnu[PK{\lKRmusersessions/version.phpnu[PK{\~I)usersessions/classes/privacy/provider.phpnu[PK{\= usersessions/locallib.phpnu[PK{\3B3??usersessions/index.phpnu[PK{\h{*ousersessions/tests/lib_test.phpnu[PK{\R4usersessions/tests/behat/usersessions_report.featurenu[PK{\??usersessions/user.phpnu[PK{\@Ҕ.status/db/access.phpnu[PK{\8V-<< status/lang/en/report_status.phpnu[PK{\status/version.phpnu[PK{\̌#kstatus/classes/privacy/provider.phpnu[PK{\6status/index.phpnu[PK{\;P__status/settings.phpnu[PK{\[nparticipation/styles.cssnu[PK{\1|participation/db/install.phpnu[PK{\7R\FFparticipation/db/access.phpnu[PK{\dpp.\participation/lang/en/report_participation.phpnu[PK{\[ * participation/lib.phpnu[PK{\gYZparticipation/version.phpnu[PK{\G**dparticipation/classes/privacy/provider.phpnu[PK{\"0rr- participation/classes/event/report_viewed.phpnu[PK{\ظ 0participation/locallib.phpnu[PK{\d?d?Nparticipation/index.phpnu[PK{\׾ %yparticipation/amd/src/participants.jsnu[PK{\oڤ+participation/amd/build/participants.min.jsnu[PK{\8/Ӥparticipation/amd/build/participants.min.js.mapnu[PK{\a`d ϶participation/tests/lib_test.phpnu[PK{\ ]!!6participation/tests/behat/filter_participation.featurenu[PK{\Al\\=participation/tests/behat/course_report_participation.featurenu[PK{\8Dn$S S 6aparticipation/tests/behat/message_participants.featurenu[PK{\Ǵ}noutline/styles.cssnu[PK{\ioutline/db/install.phpnu[PK{\_(ի8outline/db/access.phpnu[PK{\LHΨ"(outline/lang/en/report_outline.phpnu[PK{\+VZ"outline/lib.phpnu[PK{\䝲E outline/version.phpnu[PK{\K questioninstances/settings.phpnu[PK{\5l$EEP courseoverview/db/access.phpnu[PK{\}}0U courseoverview/lang/en/report_courseoverview.phpnu[PK{\KZkLooZ courseoverview/reportsgraph.phpnu[PK{\E̔bK_ courseoverview/version.phpnu[PK{\@eU+Zd courseoverview/classes/privacy/provider.phpnu[PK{\t{ j courseoverview/locallib.phpnu[PK{\H-v courseoverview/index.phpnu[PK{\zÚ courseoverview/settings.phpnu[PK{\<<0 stats/styles.cssnu[PK{\C  stats/db/install.phpnu[PK{\j.. stats/db/access.phpnu[PK{\՘T stats/lang/en/report_stats.phpnu[PK{\o+K : stats/lib.phpnu[PK{\P = stats/version.phpnu[PK{\$==f,f," stats/classes/privacy/provider.phpnu[PK{\tw % stats/classes/event/report_viewed.phpnu[PK{\=ω * stats/classes/event/user_report_viewed.phpnu[PK{\4%0Q0Q stats/locallib.phpnu[PK{\$$W stats/index.phpnu[PK{\V{{]f stats/graph.phpnu[PK{\,4949%k stats/tests/privacy/provider_test.phpnu[PK{\_ stats/tests/lib_test.phpnu[PK{\P ! stats/tests/event/events_test.phpnu[PK{\ȼ stats/user.phpnu[PK{\po stats/settings.phpnu[PK{\FMzz security/db/access.phpnu[PK{\199$ security/lang/en/report_security.phpnu[PK{\vv" security/version.phpnu[PK{\e%' security/classes/privacy/provider.phpnu[PK{\k!(- security/classes/event/report_viewed.phpnu[PK{\C}}6 security/index.phpnu[PK{\NG^^< security/settings.phpnu[PK{\>qqyA competency/db/services.phpnu[PK{\GR(4G competency/lang/en/report_competency.phpnu[PK{\O0 0 M competency/lib.phpnu[PK{\+*W competency/version.phpnu[PK{\26ԥvv~\ competency/classes/external.phpnu[PK{\s'Cm competency/classes/privacy/provider.phpnu[PK{\@b! &s competency/classes/output/renderer.phpnu[PK{\I%$ competency/classes/output/report.phpnu[PK{\Q4Ė competency/classes/output/user_course_navigation.phpnu[PK{\uY Y 4 competency/templates/user_course_navigation.mustachenu[PK{\%> $m competency/templates/report.mustachenu[PK{\d@5 competency/templates/user_competency_summary.mustachenu[PK{\<2xx competency/index.phpnu[PK{\3#o competency/amd/src/grading_popup.jsnu[PK{\ATt t ,A competency/amd/src/user_course_navigation.jsnu[PK{\ 7^ ) competency/amd/build/grading_popup.min.jsnu[PK{\DC%%6 competency/amd/build/user_course_navigation.min.js.mapnu[PK{\2Qmm2wcompetency/amd/build/user_course_navigation.min.jsnu[PK{\_-Fcompetency/amd/build/grading_popup.min.js.mapnu[PK{\Z)?bb75competency/tests/behat/course_report_competency.featurenu[PK{\Hz!!4~:competency/tests/behat/breakdown_by_activity.featurenu[PK{\{)$rr2Jcompetency/tests/behat/behat_report_competency.phpnu[PK{\G7bBB\insights/db/services.phpnu[PK{\urD $abinsights/lang/en/report_insights.phpnu[PK{\LN N =linsights/action.phpnu[PK{\^vinsights/lib.phpnu[PK{\3ҖΆinsights/version.phpnu[PK{\d insights/classes/external.phpnu[PK{\Dz/%insights/classes/privacy/provider.phpnu[PK{\ľ$insights/classes/output/renderer.phpnu[PK{\-z,insights/classes/output/actions_exporter.phpnu[PK{\t**#tinsights/classes/output/insight.phpnu[PK{\$Ӻ )insights/classes/output/insights_list.phpnu[PK{\M(insights/templates/bulk_actions.mustachenu[PK{\p=n.insights/templates/bulk_action_button.mustachenu[PK{\Zꃽ)insights/templates/insights_list.mustachenu[PK{\lt77+4insights/templates/insight_details.mustachenu[PK{\$--#)Iinsights/templates/insight.mustachenu[PK{\DQ Pinsights/prediction.phpnu[PK{\)!gg]insights/insights.phpnu[PK{\C<88!Quinsights/amd/src/message_users.jsnu[PK{\Khmڏinsights/amd/src/actions.jsnu[PK{\r !۠insights/amd/build/actions.min.jsnu[PK{\!aa%insights/amd/build/actions.min.js.mapnu[PK{\%D%%+insights/amd/build/message_users.min.js.mapnu[PK{\qA A 'insights/amd/build/message_users.min.jsnu[PK{\Տ=insights/UPGRADING.mdnu[PK{\/#minsights/tests/externallib_test.phpnu[PK{\Ц insights/done.phpnu[PK{\^)OOinsights/settings.phpnu[PK{\p/?Jeventlist/styles.cssnu[PK{\g|q &Ceventlist/lang/en/report_eventlist.phpnu[PK{\̇?%eventlist/version.phpnu[PK{\Ӕ]ww *eventlist/classes/renderer.phpnu[PK{\t*!Eeventlist/classes/filter_form.phpnu[PK{\ˑ:&Teventlist/classes/privacy/provider.phpnu[PK{\ 8!!$[eventlist/classes/list_generator.phpnu[PK{\\`|eventlist/upgrade.txtnu[PK{\(~eventlist/yui/src/eventfilter/build.jsonnu[PK{\ҿ3eventlist/yui/src/eventfilter/meta/eventfilter.jsonnu[PK{\p{$$/8eventlist/yui/src/eventfilter/js/eventfilter.jsnu[PK{\Q;ʌ beventlist/yui/build/moodle-report_eventlist-eventfilter/moodle-report_eventlist-eventfilter-min.jsnu[PK{\V,%%^eventlist/yui/build/moodle-report_eventlist-eventfilter/moodle-report_eventlist-eventfilter.jsnu[PK{\V,%%deventlist/yui/build/moodle-report_eventlist-eventfilter/moodle-report_eventlist-eventfilter-debug.jsnu[PK{\ _~~eventlist/index.phpnu[PK{\ kkeventlist/UPGRADING.mdnu[PK{\*. )8 eventlist/tests/behat/mainsection.featurenu[PK{\PIIQeventlist/eventdetail.phpnu[PK{\(eventlist/settings.phpnu[PK\,TT-lib.phpnu[PK5\f'#d3basic/lang/en/scormreport_basic.phpnu[PK5\T8basic/version.phpnu[PK5\Y "<basic/classes/privacy/provider.phpnu[PK5\{:baaJbasic/classes/report.phpnu[PK5\46//basic/upgrade.txtnu[PK5\CJ %basic/tests/privacy/provider_test.phpnu[PK5\Z [reportlib.phpnu[PK5\: YY1Xinteractions/lang/en/scormreport_interactions.phpnu[PK5\interactions/version.phpnu[PK5\x= '1interactions/responsessettings_form.phpnu[PK5\2wQQ)interactions/classes/privacy/provider.phpnu[PK5\E1]]uuinteractions/classes/report.phpnu[PK5\B|5?,jinteractions/tests/privacy/provider_test.phpnu[PK5\ڔ-xobjectives/lang/en/scormreport_objectives.phpnu[PK5\L|x88~objectives/version.phpnu[PK5\W?c c %^objectives/responsessettings_form.phpnu[PK5\+`'objectives/classes/privacy/provider.phpnu[PK5\q)xx|objectives/classes/report.phpnu[PK5\TT T *objectives/tests/privacy/provider_test.phpnu[PK5\ vv-"userreport.phpnu[PK5\9userreportinteractions.phpnu[PK5\4%@Ugraphs/lang/en/scormreport_graphs.phpnu[PK5\-Zgraphs/version.phpnu[PK5\? #)_graphs/classes/privacy/provider.phpnu[PK5\Koo~egraphs/classes/report.phpnu[PK5\Ĉ6graphs/graph.phpnu[PK5\1userreporttracks.phpnu[PKW\J J vsummary/renderer.phpnu[PKW\SSC  summary/db/access.phpnu[PKW\fǢ9x x 'Vsummary/lang/en/forumreport_summary.phpnu[PKW\V.++%summary/version.phpnu[PKW\EE*summary/classes/form/dates_filter_form.phpnu[PKW\::$2summary/classes/privacy/provider.phpnu[PKW\BEE!summary/classes/summary_table.phpnu[PKW\L%k4343"Vesummary/classes/output/filters.phpnu[PKW\_ +ܘsummary/classes/event/report_downloaded.phpnu[PKW\e{Ճ 'ʤsummary/classes/event/report_viewed.phpnu[PKW\ p  +summary/templates/bulk_action_menu.mustachenu[PKW\U??/summary/templates/filter_dates_popover.mustachenu[PKW\_XHH-summary/templates/export_link_button.mustachenu[PKW\SY Y (Ksummary/templates/filter_groups.mustachenu[PKW\ghx "summary/templates/filters.mustachenu[PKW\rH!summary/templates/report.mustachenu[PKW\6v~~',summary/templates/filter_dates.mustachenu[PKW\\Gsummary/index.phpnu[PKW\&1??`summary/amd/src/filters.jsnu[PKW\*%%Hsummary/amd/src/selectors.jsnu[PKW\lG<,, Psummary/amd/build/filters.min.jsnu[PKW\y8> > &}summary/amd/build/selectors.min.js.mapnu[PKW\;Nww"summary/amd/build/selectors.min.jsnu[PKW\&]]$`summary/amd/build/filters.min.js.mapnu[PKW\)n#kk#summary/tests/event/events_test.phpnu[PKW\E (summary/tests/behat/bulk_message.featurenu[PKW\ i#/ summary/tests/behat/summary_data_access.featurenu[PKW\Ww  4summary/tests/behat/summary_filter_no_groups.featurenu[PKW\aF993_*summary/tests/behat/summary_data_post_dates.featurenu[PKW\;^!^!*8summary/tests/behat/course_summary.featurenu[PKW\$(j j 4Zsummary/tests/behat/summary_data_attachments.featurenu[PKW\>\:gsummary/tests/behat/double_enrolments_summary_data.featurenu[PKW\8V' ' +xsummary/tests/behat/private_replies.featurenu[PKW\bs 3 31qsummary/tests/behat/summary_filter_groups.featurenu[PKiio߷