ÿØÿà 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-Ї Ç styles.css000064400000000211152266364370006611 0ustar00#page-report-loglive-index .info { margin: 10px; } table.flexible > tbody > tr:nth-child(n).newrow > td { background: #d4d4d4; }db/access.php000064400000002411152266364370007117 0ustar00. /** * 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 ), ), ); lang/en/report_loglive.php000064400000003223152266364370011652 0ustar00. /** * 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.'; lib.php000064400000004215152266364370006043 0ustar00. /** * 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; } version.php000064400000002264152266364370006764 0ustar00. /** * 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). classes/renderer.php000064400000010423152266364370010536 0ustar00. /** * 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; } } classes/renderable.php000064400000016705152266364370011044 0ustar00. /** * 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; } } classes/privacy/provider.php000064400000002747152266364370012251 0ustar00. /** * 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'; } }classes/event/report_viewed.php000064400000004032152266364370012726 0ustar00. /** * 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)); } } classes/table_log.php000064400000040712152266364370010664 0ustar00. /** * 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; } } classes/table_log_ajax.php000064400000005566152266364370011677 0ustar00. /** * 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); } } classes/renderer_ajax.php000064400000004327152266364370011547 0ustar00. /** * 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); } } yui/src/fetchlogs/build.json000064400000000306152266364370012126 0ustar00{ "name": "moodle-report_loglive-fetchlogs", "builds": { "moodle-report_loglive-fetchlogs": { "jsfiles": [ "fetchlogs.js" ] } } } yui/src/fetchlogs/meta/fetchlogs.json000064400000000244152266364370013734 0ustar00{ "moodle-report_loglive-fetchlogs": { "requires": [ "base", "event", "node", "io", "node-event-delegate" ] } } yui/src/fetchlogs/js/fetchlogs.js000064400000015141152266364370013067 0ustar00/** * 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); }; yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs-debug.js000064400000015373152266364370024674 0ustar00YUI.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"]}); yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs.js000064400000015373152266364370023610 0ustar00YUI.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"]}); yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs-min.js000064400000004377152266364370024373 0ustar00YUI.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"]});index.php000064400000007554152266364370006415 0ustar00. /** * 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(); loglive_ajax.php000064400000003570152266364370007744 0ustar00. /** * 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); tests/table_log_test.php000064400000032432152266364370011430 0ustar00. 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); } } tests/lib_test.php000064400000006533152266364370010251 0ustar00. /** * 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); } } tests/event/events_test.php000064400000004631152266364370012125 0ustar00. /** * 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()); } } tests/behat/loglive_report.feature000064400000005307152266364370013425 0ustar00@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" tests/behat/behat_report_loglive.php000064400000004113152266364370013716 0ustar00. 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}'"); } } } tests/behat/course_report_loglive.feature000064400000002063152266364370015001 0ustar00@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" tests/behat/group_report_loglive.feature000064400000010540152266364370014634 0ustar00@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 | settings.php000064400000002301152266364370007127 0ustar00. /** * 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;