diff --git a/report/loglive/classes/renderable.php b/report/loglive/classes/renderable.php new file mode 100644 index 00000000000..6cc840756b0 --- /dev/null +++ b/report/loglive/classes/renderable.php @@ -0,0 +1,213 @@ +. + +/** + * 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 { + + /** @const 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 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 + */ + public function __construct($logreader = "", $course = 0, $url = "", $date = 0, $page = 0, $perpage = 100, + $order = "timecreated DESC") { + + global $PAGE; + + // 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)) { + $course = get_course($course); + } + $this->course = $course; + + if ($date == 0 ) { + $date = time() - self::CUTOFF; + } + $this->date = $date; + + $this->page = $page; + $this->perpage = $perpage; + $this->order = $order; + $this->set_refresh_rate(); + } + + /** + * Get a list of enabled sql_select_reader objects/name + * + * @param bool $nameonly if true only reader names will be returned. + * + * @return array core\log\sql_select_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_select_reader'); + if ($nameonly) { + foreach ($readers as $pluginname => $reader) { + $readers[$pluginname] = $reader->get_name(); + } + } + return $readers; + } + + /** + * Setup table log. + */ + public 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. + */ + public 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; + } else { + $filter->courseid = 0; + } + $filter->logreader = $readers[$this->selectedlogreader]; + $filter->date = $this->date; + $filter->orderby = $this->order; + $filter->anonymous = 0; + + 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_REFERESH; + } else { + // Default. + $this->refresh = 60; + } + } + } + + /** + * Get refresh rate of the live updates. + */ + public function get_refresh_rate() { + return $this->refresh; + } + +} diff --git a/report/loglive/classes/renderer.php b/report/loglive/classes/renderer.php new file mode 100644 index 00000000000..f9a734e1cb8 --- /dev/null +++ b/report/loglive/classes/renderer.php @@ -0,0 +1,86 @@ +. + +/** + * 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 { + + /** + * Render log report page. + * + * @param report_loglive_renderable $reportloglive object of report_log. + */ + public function render_report_loglive_renderable(report_loglive_renderable $reportloglive) { + if (empty($reportloglive->selectedlogreader)) { + echo $this->output->notification(get_string('nologreaderenabled', 'report_loglive'), 'notifyproblem'); + return; + } + + $reportloglive->setup_table(); + $reportloglive->tablelog->out($reportloglive->perpage, true); + } + + /** + * 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')); + $icon = new pix_icon('i/loading_small', 'loading', 'moodle', array('class' => 'spinner')); + return $this->output->render($icon); + } + return null; + } +} + diff --git a/report/loglive/classes/renderer_ajax.php b/report/loglive/classes/renderer_ajax.php new file mode 100644 index 00000000000..b64b43d1c47 --- /dev/null +++ b/report/loglive/classes/renderer_ajax.php @@ -0,0 +1,48 @@ +. +/** + * 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 { + + /** + * Render logs for ajax. + * + * @param report_loglive_renderable $reportloglive object of report_loglive_renderable. + * + * @return string html to be displayed to user. + */ + public function render_report_loglive_renderable(report_loglive_renderable $reportloglive) { + if (empty($reportloglive->selectedlogreader)) { + return null; + } + $reportloglive->setup_table_ajax(); + $reportloglive->tablelog->out($reportloglive->perpage, false); + } +} diff --git a/report/loglive/classes/table_log.php b/report/loglive/classes/table_log.php new file mode 100644 index 00000000000..39ede4526e3 --- /dev/null +++ b/report/loglive/classes/table_log.php @@ -0,0 +1,393 @@ +. + +/** + * 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; + + /** + * 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_select_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 generalbox'); + $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('strftimerecent', '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(); + if ($event->courseid) { + $params['course'] = $event->courseid; + } + $a->realusername = html_writer::link(new moodle_url("/user/view.php", array('id' => $event->userid) + $params), + $this->userfullnames[$logextra['realuserid']]); + $a->asusername = html_writer::link(new moodle_url("/user/view.php", array('id' => $event->userid) + $params), + $this->userfullnames[$event->userid]); + $username = get_string('eventloggedas', 'report_log', $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 { + $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. + if ($this->filterparams->logreader instanceof logstore_legacy\log\store) { + // Hack for support of logstore_legacy. + $eventname = $event->eventname; + } else { + $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) { + // Description. + 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(); + + $url = new moodle_url("/iplookup/index.php?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' => 440, '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 = array(); + $params = array(); + + // Set up filtering. + if (!empty($this->filterparams->courseid)) { + $joins[] = "courseid = :courseid"; + $params['courseid'] = $this->filterparams->courseid; + } + + if (!empty($this->filterparams->date)) { + $joins[] = "timecreated > :date"; + $params['date'] = $this->filterparams->date; + } + + if (!empty($this->filterparams->date)) { + $joins[] = "anonymous = {$this->filterparams->anonymous}"; + } + + $selector = implode(' AND ', $joins); + + $total = $this->filterparams->logreader->get_events_select_count($selector, $params); + $this->pagesize($pagesize, $total); + $this->rawdata = $this->filterparams->logreader->get_events_select($selector, $params, $this->filterparams->orderby, + $this->get_page_start(), $this->get_page_size()); + + // 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); + $users = $DB->get_records_sql("SELECT id," . get_all_user_name_fields(true) . " FROM {user} WHERE id " . $usql, + $uparams); + foreach ($users as $userid => $user) { + $this->userfullnames[$userid] = fullname($user); + } + } + + // 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); + $courses = $DB->get_records_sql("SELECT id,shortname FROM {course} WHERE id " . $coursesql, $courseparams); + foreach ($courses as $courseid => $course) { + $url = new moodle_url("/course/view.php", array('id' => $courseid)); + $this->courseshortnames[$courseid] = html_writer::link($url, format_string($course->shortname)); + } + } + } +} \ No newline at end of file diff --git a/report/loglive/classes/table_log_ajax.php b/report/loglive/classes/table_log_ajax.php new file mode 100644 index 00000000000..d3766aef852 --- /dev/null +++ b/report/loglive/classes/table_log_ajax.php @@ -0,0 +1,61 @@ +. + +/** + * 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 array|void + */ + public function out($pagesize, $useinitialsbar, $downloadhelpbutton = '') { + $this->query_db($pagesize, false); + $html = ''; + $until = time(); + if ($this->rawdata && $this->columns) { + foreach ($this->rawdata as $row) { + $formatedrow = $this->format_row($row, "newrow time$until"); + $formatedrow = $this->get_row_from_keyed($formatedrow); + $html .= $this->get_row_html($formatedrow, "newrow time$until"); + } + } + $result = array('logs' => $html, 'until' => $until); + echo json_encode($result); + } +} diff --git a/report/loglive/index.php b/report/loglive/index.php index c4b8890bfbd..5402b92fb1b 100644 --- a/report/loglive/index.php +++ b/report/loglive/index.php @@ -28,78 +28,69 @@ require('../../config.php'); require_once($CFG->libdir.'/adminlib.php'); require_once($CFG->dirroot.'/course/lib.php'); -if (!defined('REPORT_LOGLIVE_REFRESH')) { - define('REPORT_LOGLIVE_REFRESH', 60); // Seconds -} - -$id = optional_param('id', $SITE->id, PARAM_INT); +$id = optional_param('id', 0, PARAM_INT); $page = optional_param('page', 0, PARAM_INT); -$inpopup = optional_param('inpopup', 0, PARAM_BOOL); +$logreader = optional_param('logreader', '', PARAM_COMPONENT); // Reader which will be used for displaying logs. -$course = $DB->get_record('course', array('id'=>$id), '*', MUST_EXIST); - -if ($course->id == SITEID) { +if (empty($id)) { require_login(); - $PAGE->set_context(context_system::instance()); + $context = context_system::instance(); + $coursename = format_string($SITE->fullname, true, array('context' => $context)); } else { + $course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); require_login($course); + $context = context_course::instance($course->id); + $coursename = format_string($course->fullname, true, array('context' => $context)); } - -$context = context_course::instance($course->id); 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'); + +$renderable = new report_loglive_renderable($logreader, $id, $url, 0, $page); +$refresh = $renderable->get_refresh_rate(); +$logreader = $renderable->selectedlogreader; + +// Include and trigger ajax requests. +if ($page == 0 && !empty($logreader)) { + // Tell Js to fetch new logs only, by passing time(). + $jsparams = array('since' => time() , '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)); +} + $strlivelogs = get_string('livelogs', 'report_loglive'); -if (!empty($page)) { - $strlogs = get_string('logs'). ": ". get_string('page', 'report_loglive', $page + 1); -} else { - $strlogs = get_string('logs'); +$strupdatesevery = get_string('updatesevery', 'moodle', $refresh); + +if (empty($id)) { + admin_externalpage_setup('reportloglive', '', null, '', array('pagelayout' => 'report')); } +$PAGE->set_url($url); +$PAGE->set_context($context); +$PAGE->set_title("$coursename: $strlivelogs ($strupdatesevery)"); +$PAGE->set_heading("$coursename: $strlivelogs ($strupdatesevery)"); -if ($inpopup) { - \core\session\manager::write_close(); +$output = $PAGE->get_renderer('report_loglive'); +echo $output->header(); +echo $output->reader_selector($renderable); +echo $output->toggle_liveupdate_button($renderable); +echo $output->render($renderable); - $date = time() - 3600; +// Trigger a logs viewed event. +$event = \report_loglive\event\report_viewed::create(array('context' => $context)); +$event->trigger(); - $url = new moodle_url('/report/loglive/index.php', array('id'=>$course->id, 'user'=>0, 'date'=>$date, 'inpopup'=>1)); - - $strupdatesevery = get_string('updatesevery', 'moodle', REPORT_LOGLIVE_REFRESH); - - $coursename = format_string($course->fullname, true, array('context'=>$context)); - - $PAGE->set_url($url); - $PAGE->set_pagelayout('popup'); - $PAGE->set_title("$coursename: $strlivelogs ($strupdatesevery)"); - $PAGE->set_periodic_refresh_delay(REPORT_LOGLIVE_REFRESH); - $PAGE->set_heading($strlivelogs); - echo $OUTPUT->header(); - - // Trigger a logs viewed event. - $event = \report_loglive\event\report_viewed::create(array('context' => $context)); - $event->trigger(); - - print_log($course, 0, $date, "l.time DESC", $page, 500, $url); - - echo $OUTPUT->footer(); - exit; -} - - -if ($course->id == SITEID) { - admin_externalpage_setup('reportloglive', '', null, '', array('pagelayout'=>'report')); - echo $OUTPUT->header(); - -} else { - $PAGE->set_url('/report/log/live.php', array('id'=>$course->id)); - $PAGE->set_title($course->shortname .': '. $strlivelogs); - $PAGE->set_heading($course->fullname); - echo $OUTPUT->header(); -} - -echo $OUTPUT->heading(get_string('pluginname', 'report_loglive')); - -echo $OUTPUT->container_start('info'); -$link = new moodle_url('/report/loglive/index.php', array('id'=>$course->id, 'inpopup'=>1)); -echo $OUTPUT->action_link($link, $strlivelogs, new popup_action('click', $link, 'livelog', array('height' => 500, 'width' => 800))); -echo $OUTPUT->container_end(); - -echo $OUTPUT->footer(); +echo $output->footer(); diff --git a/report/loglive/lang/en/report_loglive.php b/report/loglive/lang/en/report_loglive.php index 2f96aedda62..602b240eb77 100644 --- a/report/loglive/lang/en/report_loglive.php +++ b/report/loglive/lang/en/report_loglive.php @@ -25,7 +25,16 @@ */ $string['eventreportviewed'] = 'Live log report viewed'; -$string['loglive:view'] = 'View live logs'; -$string['pluginname'] = 'Live logs'; +$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['page'] = 'Page {$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'; + diff --git a/report/loglive/lib.php b/report/loglive/lib.php index c0d3237bdac..59fcbb312ab 100644 --- a/report/loglive/lib.php +++ b/report/loglive/lib.php @@ -33,14 +33,13 @@ defined('MOODLE_INTERNAL') || die; * @global core_renderer $OUTPUT * @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 + * @param context $context The context of the course */ function report_loglive_extend_navigation_course($navigation, $course, $context) { - global $CFG, $OUTPUT; if (has_capability('report/loglive:view', $context)) { - $url = new moodle_url('/report/loglive/index.php', array('id'=>$course->id, 'inpopup'=>1)); - $action = new action_link($url, '', new popup_action('click', $url)); - $navigation->add(get_string('pluginname', 'report_loglive'), $action, navigation_node::TYPE_SETTING, null, null, new pix_icon('i/report', '')); + $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', '')); } } diff --git a/report/loglive/loglive_ajax.php b/report/loglive/loglive_ajax.php new file mode 100644 index 00000000000..2c90c1a98cd --- /dev/null +++ b/report/loglive/loglive_ajax.php @@ -0,0 +1,50 @@ +. + +/** + * 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. + +// Capability checks. +if (empty($id)) { + require_login(); + $context = context_system::instance(); +} else { + $course = $DB->get_record('course', array('id' => $id), '*', MUST_EXIST); + require_login($course); + $context = context_course::instance($course->id); +} + +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'); +$output->render($renderable); diff --git a/report/loglive/settings.php b/report/loglive/settings.php index 2966e63e197..f36faba5604 100644 --- a/report/loglive/settings.php +++ b/report/loglive/settings.php @@ -26,8 +26,9 @@ 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')); +// 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 +// No report settings. $settings = null; diff --git a/report/loglive/styles.css b/report/loglive/styles.css index f6a9b5b05df..4c3a54fcd8f 100644 --- a/report/loglive/styles.css +++ b/report/loglive/styles.css @@ -1 +1,2 @@ #page-report-loglive-index .info {margin:10px;} +table.flexible > tbody > tr:nth-child(n).newrow > td {background: #D4D4D4;} \ No newline at end of file diff --git a/report/loglive/tests/behat/loglive_report.feature b/report/loglive/tests/behat/loglive_report.feature new file mode 100644 index 00000000000..a272a35090f --- /dev/null +++ b/report/loglive/tests/behat/loglive_report.feature @@ -0,0 +1,79 @@ +@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 I log in as "admin" + And I set the following administration settings values: + | Log legacy data | 1 | + And I navigate to "Manage log stores" node in "Site administration>Plugins>Logging" + And I click on "Enable" "link" in the "Standard log" "table_row" + And I am on homepage + And I follow "Course 1" + And I turn editing mode on + And I add a "Database" to 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 and legacy reader + Given I navigate to "Live logs" node in "Site administration>Reports" + When I set the field "reader" to "Standard log" + Then I should see "Course module created" + And I should see "Test name" + And I set the field "reader" to "Legacy log" + And I wait to be redirected + And I should see "course_add mod" + And I log out + + @javascript + Scenario: Check loglive report entries and make sure the pause/resume button works for standard reader along with ajax calls + Given I am on homepage + When I navigate to "Live logs" node in "Site administration>Reports" + And I set the field "reader" to "Standard log" + And I wait to be redirected + 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 homepage + And I follow "Course 1" + And I add a "Database" to 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" + And I log out + + @javascript + Scenario: Check loglive report entries and make sure the pause/resume button works for legacy reader along with ajax calls + Given I am on homepage + When I navigate to "Live logs" node in "Site administration>Reports" + And I set the field "reader" to "Legacy log" + And I wait to be redirected + And I should not see "Test name2" + And I press "Pause live updates" + And I follow "course_add mod" + And I switch to "action" window + And I am on homepage + And I follow "Course 1" + And I add a "Database" to 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" + And I log out + diff --git a/report/loglive/version.php b/report/loglive/version.php index 5ff6984136e..1b69fb35ea7 100644 --- a/report/loglive/version.php +++ b/report/loglive/version.php @@ -26,6 +26,6 @@ defined('MOODLE_INTERNAL') || die; -$plugin->version = 2013110500; // The current plugin version (Date: YYYYMMDDXX) -$plugin->requires = 2013110500; // Requires this Moodle version -$plugin->component = 'report_loglive'; // Full name of the plugin (used for diagnostics) +$plugin->version = 2014011700; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2014011000; // Requires this Moodle version. +$plugin->component = 'report_loglive'; // Full name of the plugin (used for diagnostics). diff --git a/report/loglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs-debug.js b/report/loglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs-debug.js new file mode 100644 index 00000000000..43d55c9e1f5 --- /dev/null +++ b/report/loglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs-debug.js @@ -0,0 +1,245 @@ +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', + 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 delegations. + * + * @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.delegate('click', this.toggleUpdate, 'button', SELECTORS.PAUSEBUTTON, 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) { + Y.later(600, this, 'hideLoadingIcon'); // Hide loading icon, give sometime to people to actually see it. We should do it, event in case of an error. + 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); + } else { + // @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', responseobject.until); // Remove highlighting from new rows. + } + }, + + /** + * Remove background highlight from the newly added rows. + * + * @method removeHighlight + */ + removeHighlight: function(timeStamp) { + Y.all('.time' + timeStamp).removeClass(CSS.NEWROW); + }, + + /** + * Hide the spinning icon. + * + * @method hideLoadingIcon + */ + hideLoadingIcon: function() { + this.spinner.hide(); + }, + + /** + * 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"]}); diff --git a/report/loglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs-min.js b/report/loglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs-min.js new file mode 100644 index 00000000000..7963b2c0557 --- /dev/null +++ b/report/loglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs-min.js @@ -0,0 +1 @@ +YUI.add("moodle-report_loglive-fetchlogs",function(e,t){function n(){n.superclass.constructor.apply(this,arguments)}var r={NEWROW:"newrow",SPINNER:"spinner",ICONSMALL:"iconsmall"},i={NEWROW:"."+r.NEWROW,TBODY:".flexible tbody",PAUSEBUTTON:"#livelogs-pause-button",SPINNER:"."+r.SPINNER};e.extend(n,e.Base,{callBack:{},spinner:{},pauseButton:{},initializer:function(){this.get("page")===0&&(this.callBack=e.later(this.get("interval")*1e3,this,this.fetchRecentLogs,null,!0)),this.spinner=e.one(i.SPINNER),this.pauseButton=e.one(i.PAUSEBUTTON),this.spinner.hide(),e.delegate("click",this.toggleUpdate,"button",i.PAUSEBUTTON,this)},fetchRecentLogs:function(){this.spinner.show();var t={logreader:this.get("logreader"),since:this.get("since"),page:this.get("page"),id:this.get("courseid")},n={method:"get",context:this,on:{complete:this.updateLogTable},data:t},r=M.cfg.wwwroot+"/report/loglive/loglive_ajax.php";e.io(r,n)},updateLogTable:function(t,n){e.later(600,this,"hideLoadingIcon");var r;try{r=e.JSON.parse(n.responseText);if(r.error)return e.use("moodle-core-notification-ajaxexception",function(){return new M.core.ajaxException(r)}),this}catch(s){return e.use("moodle-core-notification-exception",function(){return new M.core.exception(s)}),this}this.set("since",r.until);var o=r.logs,u=e.one(i.TBODY),a=null;if(u&&o){a=u.get("firstChild"),a&&u.insertBefore(o,a);var f=u.get("children").slice(this.get("perpage"));f.remove(),e.later(5e3,this,"removeHighlight",r.until)}},removeHighlight:function(t){e.all(".time"+t).removeClass(r.NEWROW)},hideLoadingIcon:function(){this.spinner.hide()},toggleUpdate:function(){this.callBack?(this.callBack.cancel(),this.callBack="",this.pauseButton.setContent(M.util.get_string("resume","report_loglive"))):(this.callBack=e.later(this.get("interval")*1e3,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"}}),e.namespace("M.report_loglive.FetchLogs").init=function(e){return new n(e)}},"@VERSION@",{requires:["base","event","node","io","node-event-delegate"]}); diff --git a/report/loglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs.js b/report/loglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs.js new file mode 100644 index 00000000000..43d55c9e1f5 --- /dev/null +++ b/report/loglive/yui/build/moodle-report_loglive-fetchlogs/moodle-report_loglive-fetchlogs.js @@ -0,0 +1,245 @@ +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', + 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 delegations. + * + * @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.delegate('click', this.toggleUpdate, 'button', SELECTORS.PAUSEBUTTON, 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) { + Y.later(600, this, 'hideLoadingIcon'); // Hide loading icon, give sometime to people to actually see it. We should do it, event in case of an error. + 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); + } else { + // @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', responseobject.until); // Remove highlighting from new rows. + } + }, + + /** + * Remove background highlight from the newly added rows. + * + * @method removeHighlight + */ + removeHighlight: function(timeStamp) { + Y.all('.time' + timeStamp).removeClass(CSS.NEWROW); + }, + + /** + * Hide the spinning icon. + * + * @method hideLoadingIcon + */ + hideLoadingIcon: function() { + this.spinner.hide(); + }, + + /** + * 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"]}); diff --git a/report/loglive/yui/src/fetchlogs/build.json b/report/loglive/yui/src/fetchlogs/build.json new file mode 100644 index 00000000000..86e30a5f90e --- /dev/null +++ b/report/loglive/yui/src/fetchlogs/build.json @@ -0,0 +1,10 @@ +{ + "name": "moodle-report_loglive-fetchlogs", + "builds": { + "moodle-report_loglive-fetchlogs": { + "jsfiles": [ + "fetchlogs.js" + ] + } + } +} diff --git a/report/loglive/yui/src/fetchlogs/js/fetchlogs.js b/report/loglive/yui/src/fetchlogs/js/fetchlogs.js new file mode 100644 index 00000000000..bfce19b3c2d --- /dev/null +++ b/report/loglive/yui/src/fetchlogs/js/fetchlogs.js @@ -0,0 +1,240 @@ +/** + * 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', + 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 delegations. + * + * @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.delegate('click', this.toggleUpdate, 'button', SELECTORS.PAUSEBUTTON, 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) { + Y.later(600, this, 'hideLoadingIcon'); // Hide loading icon, give sometime to people to actually see it. We should do it, event in case of an error. + 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); + } else { + // @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', responseobject.until); // Remove highlighting from new rows. + } + }, + + /** + * Remove background highlight from the newly added rows. + * + * @method removeHighlight + */ + removeHighlight: function(timeStamp) { + Y.all('.time' + timeStamp).removeClass(CSS.NEWROW); + }, + + /** + * Hide the spinning icon. + * + * @method hideLoadingIcon + */ + hideLoadingIcon: function() { + this.spinner.hide(); + }, + + /** + * 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); +}; diff --git a/report/loglive/yui/src/fetchlogs/meta/fetchlogs.json b/report/loglive/yui/src/fetchlogs/meta/fetchlogs.json new file mode 100644 index 00000000000..c5d9c69a0d1 --- /dev/null +++ b/report/loglive/yui/src/fetchlogs/meta/fetchlogs.json @@ -0,0 +1,11 @@ +{ + "moodle-report_loglive-fetchlogs": { + "requires": [ + "base", + "event", + "node", + "io", + "node-event-delegate" + ] + } +}