From efaf8534265a831da66d20b53ab59eced63174cc Mon Sep 17 00:00:00 2001 From: David Monllao Date: Tue, 23 May 2017 17:15:55 +0800 Subject: [PATCH] MDL-58859 tool_models: Added to core Part of MDL-57791 epic. --- admin/tool/models/amd/build/log_info.min.js | 1 + admin/tool/models/amd/src/log_info.js | 38 ++++ .../classes/local/target/course_dropout.php | 179 +++++++++++++++ .../models/classes/output/form/edit_model.php | 117 ++++++++++ admin/tool/models/classes/output/helper.php | 47 ++++ .../tool/models/classes/output/model_logs.php | 196 +++++++++++++++++ .../models/classes/output/models_list.php | 123 +++++++++++ admin/tool/models/classes/output/renderer.php | 207 ++++++++++++++++++ .../models/classes/task/predict_models.php | 62 ++++++ .../tool/models/classes/task/train_models.php | 61 ++++++ admin/tool/models/cli/enable_model.php | 73 ++++++ admin/tool/models/cli/evaluate_model.php | 132 +++++++++++ .../models/cli/guess_course_start_and_end.php | 190 ++++++++++++++++ admin/tool/models/db/install.php | 42 ++++ admin/tool/models/db/tasks.php | 45 ++++ admin/tool/models/index.php | 37 ++++ admin/tool/models/lang/en/tool_models.php | 90 ++++++++ admin/tool/models/model.php | 137 ++++++++++++ admin/tool/models/settings.php | 28 +++ .../models/templates/models_list.mustache | 91 ++++++++ admin/tool/models/version.php | 29 +++ 21 files changed, 1925 insertions(+) create mode 100644 admin/tool/models/amd/build/log_info.min.js create mode 100644 admin/tool/models/amd/src/log_info.js create mode 100644 admin/tool/models/classes/local/target/course_dropout.php create mode 100644 admin/tool/models/classes/output/form/edit_model.php create mode 100644 admin/tool/models/classes/output/helper.php create mode 100644 admin/tool/models/classes/output/model_logs.php create mode 100644 admin/tool/models/classes/output/models_list.php create mode 100644 admin/tool/models/classes/output/renderer.php create mode 100644 admin/tool/models/classes/task/predict_models.php create mode 100644 admin/tool/models/classes/task/train_models.php create mode 100644 admin/tool/models/cli/enable_model.php create mode 100644 admin/tool/models/cli/evaluate_model.php create mode 100644 admin/tool/models/cli/guess_course_start_and_end.php create mode 100644 admin/tool/models/db/install.php create mode 100644 admin/tool/models/db/tasks.php create mode 100644 admin/tool/models/index.php create mode 100644 admin/tool/models/lang/en/tool_models.php create mode 100644 admin/tool/models/model.php create mode 100644 admin/tool/models/settings.php create mode 100644 admin/tool/models/templates/models_list.mustache create mode 100644 admin/tool/models/version.php diff --git a/admin/tool/models/amd/build/log_info.min.js b/admin/tool/models/amd/build/log_info.min.js new file mode 100644 index 00000000000..fd56b0d5eda --- /dev/null +++ b/admin/tool/models/amd/build/log_info.min.js @@ -0,0 +1 @@ +define(["jquery","core/str","core/modal_factory"],function(a,b,c){return{loadInfo:function(d,e){var f=a('[data-model-log-id="'+d+'"]');b.get_string("loginfo","tool_inspire").done(function(b){var d=a(""),c.create({title:b,body:d.html(),large:!0},f)})}}}); \ No newline at end of file diff --git a/admin/tool/models/amd/src/log_info.js b/admin/tool/models/amd/src/log_info.js new file mode 100644 index 00000000000..a5799c92736 --- /dev/null +++ b/admin/tool/models/amd/src/log_info.js @@ -0,0 +1,38 @@ +/* + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + + /** + * @module tool_models/log_info + */ +define(['jquery', 'core/str', 'core/modal_factory'], function($, str, ModalFactory) { + + return { + + /** + * Prepares a modal info for a log's results. + * @access public + * @param {int} id + * @return {String} HTML info + */ + loadInfo : function(id, info) { + + var link = $('[data-model-log-id="' + id + '"]'); + str.get_string('loginfo', 'tool_models').done(function(langString) { + + var bodyInfo = $(""); + ModalFactory.create({ + title: langString, + body: bodyInfo.html(), + large: true, + }, link); + }); + } + }; +}); diff --git a/admin/tool/models/classes/local/target/course_dropout.php b/admin/tool/models/classes/local/target/course_dropout.php new file mode 100644 index 00000000000..dc8d835bc9d --- /dev/null +++ b/admin/tool/models/classes/local/target/course_dropout.php @@ -0,0 +1,179 @@ +. + +/** + * Drop out course target. + * + * @package tool_models + * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_models\local\target; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/course/lib.php'); +require_once($CFG->dirroot . '/lib/gradelib.php'); +require_once($CFG->dirroot . '/lib/completionlib.php'); +require_once($CFG->dirroot . '/completion/completion_completion.php'); + +/** + * Drop out course target. + * + * @package tool_models + * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class course_dropout extends \core_analytics\local\target\binary { + + protected $coursegradeitem = null; + + public static function get_name() { + return get_string('target:coursedropout', 'tool_models'); + } + + public function prediction_actions(\core_analytics\prediction $prediction) { + global $USER; + + $actions = parent::prediction_actions($prediction); + + $sampledata = $prediction->get_sample_data(); + $studentid = $sampledata['user']->id; + + // Send a message. + $url = new \moodle_url('/message/index.php', array('user' => $USER->id, 'id' => $studentid)); + $pix = new \pix_icon('t/message', get_string('sendmessage', 'message')); + $actions['studentmessage'] = new \core_analytics\prediction_action('studentmessage', $prediction, $url, $pix, get_string('sendmessage', 'message')); + + // View outline report. + $url = new \moodle_url('/report/outline/user.php', array('id' => $studentid, 'course' => $sampledata['course']->id, + 'mode' => 'outline')); + $pix = new \pix_icon('i/report', get_string('outlinereport')); + $actions['viewoutlinereport'] = new \core_analytics\prediction_action('viewoutlinereport', $prediction, $url, $pix, get_string('outlinereport')); + + return $actions; + } + + protected static function classes_description() { + return array( + get_string('labelstudentdropoutno', 'tool_models'), + get_string('labelstudentdropoutyes', 'tool_models') + ); + } + + /** + * Returns the predicted classes that will be ignored. + * + * Overwriten because we are also interested in knowing when the student is far from the risk of dropping out. + * + * @return array + */ + protected function ignored_predicted_classes() { + return array(); + } + + public function get_analyser_class() { + return '\\core_analytics\\local\\analyser\\student_enrolments'; + } + + public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) { + global $DB; + + if (!$course->was_started()) { + return get_string('coursenotyetstarted', 'tool_models'); + } + + if (!$students = $course->get_students()) { + return get_string('nocoursestudents', 'tool_models'); + } + + if (!course_format_uses_sections($course->get_course_data()->format)) { + // We can not split activities in time ranges. + return get_string('nocoursesections', 'tool_models'); + } + + // Ongoing courses data can not be used to train. + if ($fortraining && !$course->is_finished()) { + if ($course->get_end() === 0) { + // More specific error. + return get_string('nocourseendtime', 'tool_models'); + } + return get_string('coursenotyetfinished', 'tool_models'); + } + + if ($fortraining) { + + // Not a valid target for training if there are not enough course accesses. + $params = array('courseid' => $course->get_id(), 'anonymous' => 0, 'start' => $course->get_start(), + 'end' => $course->get_end()); + list($studentssql, $studentparams) = $DB->get_in_or_equal($students, SQL_PARAMS_NAMED); + $select = 'courseid = :courseid AND anonymous = :anonymous AND timecreated > :start AND timecreated < :end ' . + 'AND userid ' . $studentssql; + // Using anonymous to use the db index, not filtering by timecreated to speed it up. + $nlogs = $DB->count_records_select('logstore_standard_log', $select, array_merge($params, $studentparams)); + + // At least a minimum of students activity. + $nstudents = count($students); + if ($nlogs / $nstudents < 10) { + return get_string('nocourseactivity', 'tool_models'); + } + } + + return true; + } + + /** + * calculate_sample + * + * The meaning of a drop out changes depending on the settings enabled in the course. Following these priorities order: + * 1.- Course completion + * 2.- No logs during the last quarter of the course + * + * @param int $sampleid + * @param \core_analytics\analysable $course + * @return void + */ + public function calculate_sample($sampleid, \core_analytics\analysable $course) { + global $DB; + + // TODO Even if targets are aware of the data the analyser returns, we can probably still feed samples + // data with cached data. + $sql = "SELECT ue.* FROM {user_enrolments} ue JOIN {user} u ON u.id = ue.userid WHERE ue.id = :ueid"; + $userenrol = $DB->get_record_sql($sql, array('ueid' => $sampleid)); + + // We use completion as a success metric only when it is enabled. + $completion = new \completion_info($course->get_course_data()); + if ($completion->is_enabled() && $completion->has_criteria()) { + $ccompletion = new \completion_completion(array('userid' => $userenrol->userid, 'course' => $course->get_id())); + if ($ccompletion->is_complete()) { + return 0; + } else { + return 1; + } + } + + // No logs during the last quarter of the course. + $courseduration = $course->get_end() - $course->get_start(); + $limit = intval($course->get_end() - ($courseduration / 4)); + $params = array('userid' => $userenrol->userid, 'courseid' => $course->get_id(), 'limit' => $limit); + $sql = "SELECT id FROM {logstore_standard_log} WHERE courseid = :courseid AND userid = :userid AND timecreated > :limit"; + if ($DB->record_exists_sql($sql, $params)) { + return 0; + } + return 1; + } +} diff --git a/admin/tool/models/classes/output/form/edit_model.php b/admin/tool/models/classes/output/form/edit_model.php new file mode 100644 index 00000000000..86a3b6de6b8 --- /dev/null +++ b/admin/tool/models/classes/output/form/edit_model.php @@ -0,0 +1,117 @@ +. + +/** + * Model edit form. + * + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_models\output\form; + +require_once($CFG->dirroot.'/lib/formslib.php'); + +/** + * Model edit form. + * + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class edit_model extends \moodleform { + + /** + * Form definition + */ + public function definition() { + global $OUTPUT; + + $mform = $this->_form; + + if ($this->_customdata['model']->get_model_obj()->trained == 1) { + $message = get_string('edittrainedwarning', 'tool_models'); + $mform->addElement('html', $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING)); + } + + $mform->addElement('advcheckbox', 'enabled', get_string('enabled', 'tool_models')); + + $indicators = array(); + foreach ($this->_customdata['indicators'] as $classname => $indicator) { + $optionname = \tool_models\output\helper::class_to_option($classname); + $indicators[$optionname] = $indicator->get_name(); + } + $options = array( + 'multiple' => true + ); + $mform->addElement('autocomplete', 'indicators', get_string('indicators', 'tool_models'), $indicators, $options); + $mform->setType('indicators', PARAM_ALPHANUMEXT); + + $timesplittings = array('' => ''); + foreach ($this->_customdata['timesplittings'] as $classname => $timesplitting) { + $optionname = \tool_models\output\helper::class_to_option($classname); + $timesplittings[$optionname] = $timesplitting->get_name(); + } + + $mform->addElement('select', 'timesplitting', get_string('timesplittingmethod', 'analytics'), $timesplittings); + $mform->addHelpButton('timesplitting', 'timesplittingmethod', 'analytics'); + + $mform->addElement('hidden', 'id', $this->_customdata['id']); + $mform->setType('id', PARAM_INT); + + $mform->addElement('hidden', 'action', 'edit'); + $mform->setType('action', PARAM_ALPHANUMEXT); + + $this->add_action_buttons(); + } + + /** + * Form validation + * + * @param array $data data from the form. + * @param array $files files uploaded. + * + * @return array of errors. + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + if (!empty($data['timesplitting'])) { + $realtimesplitting = \tool_models\output\helper::option_to_class($data['timesplitting']); + if (\core_analytics\manager::is_valid($realtimesplitting, '\core_analytics\local\time_splitting\base') === false) { + $errors['timesplitting'] = get_string('errorinvalidtimesplitting', 'analytics'); + } + } + + if (empty($data['indicators'])) { + $errors['indicators'] = get_string('errornoindicators', 'analytics'); + } else { + foreach ($data['indicators'] as $indicator) { + $realindicatorname = \tool_models\output\helper::option_to_class($indicator); + if (\core_analytics\manager::is_valid($realindicatorname, '\core_analytics\local\indicator\base') === false) { + $errors['indicators'] = get_string('errorinvalidindicator', 'analytics', $realindicatorname); + } + } + } + + if (!empty($data['enabled']) && empty($data['timesplitting'])) { + $errors['enabled'] = get_string('errorcantenablenotimesplitting', 'tool_models'); + } + + return $errors; + } +} diff --git a/admin/tool/models/classes/output/helper.php b/admin/tool/models/classes/output/helper.php new file mode 100644 index 00000000000..8420b1a8fc8 --- /dev/null +++ b/admin/tool/models/classes/output/helper.php @@ -0,0 +1,47 @@ +. + +/** + * Typical crappy helper class with tiny functions. + * + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_models\output; + +/** + * Typical crappy helper class with tiny functions. + * + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class helper { + + public static function class_to_option($class) { + // Form field is PARAM_ALPHANUMEXT and we are sending fully qualified class names + // as option names, but replacing the backslash for a string that is really unlikely + // to ever be part of a class name. + return str_replace('\\', '2015102400ouuu', $class); + } + + public static function option_to_class($option) { + // Really unlikely but yeah, I'm a bad booyyy. + return str_replace('2015102400ouuu', '\\', $option); + } +} diff --git a/admin/tool/models/classes/output/model_logs.php b/admin/tool/models/classes/output/model_logs.php new file mode 100644 index 00000000000..cc2cf549f5d --- /dev/null +++ b/admin/tool/models/classes/output/model_logs.php @@ -0,0 +1,196 @@ +. + +/** + * Model logs table class. + * + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_models\output; + +defined('MOODLE_INTERNAL') || die; +require_once($CFG->libdir . '/tablelib.php'); + +/** + * Model logs table class. + * + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class model_logs extends \table_sql { + + /** + * @var int + */ + protected $modelid = null; + + /** + * Sets up the table_log parameters. + * + * @param string $uniqueid unique id of form. + * @param int $modelid model id + */ + public function __construct($uniqueid, $modelid) { + global $PAGE; + + parent::__construct($uniqueid); + + $this->modelid = $modelid; + + $this->set_attribute('class', 'modellog generaltable generalbox'); + $this->set_attribute('aria-live', 'polite'); + + $this->define_columns(array('time', 'version', 'indicators', 'timesplitting', 'accuracy', 'info', 'usermodified')); + $this->define_headers(array( + get_string('time'), + get_string('version'), + get_string('indicators', 'tool_models'), + get_string('timesplittingmethod', 'analytics'), + get_string('accuracy', 'tool_models'), + get_string('info', 'tool_models'), + get_string('fullnameuser'), + )); + $this->pageable(true); + $this->collapsible(false); + $this->sortable(false); + $this->is_downloadable(false); + + $this->define_baseurl($PAGE->url); + } + + /** + * Generate the version column. + * + * @param stdClass $log log data. + * @return string HTML for the version column + */ + public function col_version($log) { + $recenttimestr = get_string('strftimerecent', 'core_langconfig'); + return userdate($log->version, $recenttimestr); + } + + /** + * Generate the time column. + * + * @param stdClass $log log data. + * @return string HTML for the time column + */ + public function col_time($log) { + $recenttimestr = get_string('strftimerecent', 'core_langconfig'); + return userdate($log->timecreated, $recenttimestr); + } + + /** + * Generate the indicators column. + * + * @param stdClass $log log data. + * @return string HTML for the indicators column + */ + public function col_indicators($log) { + $indicatorclasses = json_decode($log->indicators); + $indicators = array(); + foreach ($indicatorclasses as $indicatorclass) { + $indicator = \core_analytics\manager::get_indicator($indicatorclass); + if ($indicator) { + $indicators[] = $indicator->get_name(); + } else { + debugging('Can\'t load ' . $indicatorclass . ' indicator', DEBUG_DEVELOPER); + } + } + return ''; + } + + /** + * Generate the context column. + * + * @param stdClass $log log data. + * @return string HTML for the context column + */ + public function col_timesplitting($log) { + $timesplitting = \core_analytics\manager::get_time_splitting($log->timesplitting); + return $timesplitting->get_name(); + } + + /** + * Generate the accuracy column. + * + * @param stdClass $log log data. + * @return string HTML for the accuracy column + */ + public function col_accuracy($log) { + return strval(round($log->score * 100, 2)) . '%'; + } + + /** + * Generate the info column. + * + * @param stdClass $log log data. + * @return string HTML for the score column + */ + public function col_info($log) { + global $PAGE; + + if (empty($log->info) && empty($log->dir)) { + return ''; + } + + $info = array(); + if (!empty($log->info)) { + $info = json_decode($log->info); + } + if (!empty($log->dir)) { + $info[] = get_string('predictorresultsin', 'tool_models', $log->dir); + } + $PAGE->requires->js_call_amd('tool_models/log_info', 'loadInfo', array($log->id, $info)); + return \html_writer::link('#', get_string('view'), array('data-model-log-id' => $log->id)); + } + + /** + * Generate the usermodified column. + * + * @param stdClass $log log data. + * @return string HTML for the usermodified column + */ + public function col_usermodified($log) { + $user = \core_user::get_user($log->usermodified); + return fullname($user); + } + + /** + * Query the logs table. Store results in the object for use by build_table. + * + * @param int $pagesize size of page for paginated displayed table. + * @param bool $useinitialsbar do you want to use the initials bar. + */ + public function query_db($pagesize, $useinitialsbar = true) { + global $DB; + + $total = $DB->count_records('analytics_models_log', array('modelid' => $this->modelid)); + $this->pagesize($pagesize, $total); + + $this->rawdata = $DB->get_records('analytics_models_log', array('modelid' => $this->modelid), 'timecreated DESC', '*', + $this->get_page_start(), $this->get_page_size()); + + // Set initial bars. + if ($useinitialsbar) { + $this->initialbars($total > $pagesize); + } + } +} diff --git a/admin/tool/models/classes/output/models_list.php b/admin/tool/models/classes/output/models_list.php new file mode 100644 index 00000000000..79e0bcd7621 --- /dev/null +++ b/admin/tool/models/classes/output/models_list.php @@ -0,0 +1,123 @@ +. + +/** + * Inspire models list page. + * + * @package tool_models + * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_models\output; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Shows tool_models models list. + * + * @package tool_models + * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class models_list implements \renderable, \templatable { + + protected $models = array(); + + public function __construct($models) { + $this->models = $models; + } + + /** + * Exports the data. + * + * @param \renderer_base $output + * @return \stdClass + */ + public function export_for_template(\renderer_base $output) { + + $data = new \stdClass(); + + $data->models = array(); + foreach ($this->models as $model) { + $modeldata = $model->export(); + + // Model predictions list. + $predictioncontexts = $model->get_predictions_contexts(); + if ($predictioncontexts) { + + foreach ($predictioncontexts as $contextid => $unused) { + // We prepare this to be used as single_select template options. + $context = \context::instance_by_id($contextid); + if (empty($context)) { + // The context may have been deleted. + unset($predictioncontexts[$contextid]); + continue; + } + $predictioncontexts[$contextid] = shorten_text($context->get_context_name(true, true), 90); + } + \core_collator::asort($predictioncontexts); + + if (!empty($predictioncontexts)) { + $url = new \moodle_url('/report/insights/insights.php', array('modelid' => $model->get_id())); + $singleselect = new \single_select($url, 'contextid', $predictioncontexts); + $modeldata->predictions = $singleselect->export_for_template($output); + } + } + + // Actions. + $actionsmenu = new \action_menu(); + $actionsmenu->set_menu_trigger(get_string('edit')); + $actionsmenu->set_owner_selector('model-actions-' . $model->get_id()); + $actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL); + + // Edit model. + $url = new \moodle_url('model.php', array('action' => 'edit', 'id' => $model->get_id())); + $icon = new \action_menu_link_secondary($url, new \pix_icon('t/edit', get_string('edit')), get_string('edit')); + $actionsmenu->add($icon); + + // Evaluate model. + if ($model->get_indicators()) { + $url = new \moodle_url('model.php', array('action' => 'evaluate', 'id' => $model->get_id())); + $icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_models')), + get_string('evaluate', 'tool_models')); + $actionsmenu->add($icon); + } + + if ($modeldata->enabled && !empty($modeldata->timesplitting)) { + $url = new \moodle_url('model.php', array('action' => 'execute', 'id' => $model->get_id())); + $icon = new \action_menu_link_secondary($url, new \pix_icon('i/notifications', + get_string('executemodel', 'tool_models')), get_string('executemodel', 'tool_models')); + $actionsmenu->add($icon); + } + + $url = new \moodle_url('model.php', array('action' => 'log', 'id' => $model->get_id())); + $icon = new \action_menu_link_secondary($url, new \pix_icon('i/report', get_string('viewlog', 'tool_models')), + get_string('viewlog', 'tool_models')); + $actionsmenu->add($icon); + + $modeldata->actions = $actionsmenu->export_for_template($output); + + $data->models[] = $modeldata; + } + + $data->warnings = array( + (object)array('message' => get_string('bettercli', 'tool_models'), 'closebutton' => true) + ); + + return $data; + } +} diff --git a/admin/tool/models/classes/output/renderer.php b/admin/tool/models/classes/output/renderer.php new file mode 100644 index 00000000000..efab2c7efb2 --- /dev/null +++ b/admin/tool/models/classes/output/renderer.php @@ -0,0 +1,207 @@ +. + +/** + * Renderer. + * + * @package tool_models + * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_models\output; + +defined('MOODLE_INTERNAL') || die(); + +use plugin_renderer_base; +use templatable; +use renderable; + +/** + * Renderer class. + * + * @package tool_models + * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class renderer extends plugin_renderer_base { + + /** + * Defer to template. + * + * @param templatable $renderable + * @return string HTML + */ + protected function render_models_list(templatable $renderable) { + $data = $renderable->export_for_template($this); + return parent::render_from_template('tool_models/models_list', $data); + } + + /** + * Renders a table. + * + * @param \table_sql $table + * @return string HTML + */ + public function render_table(\table_sql $table) { + + ob_start(); + $table->out(10, true); + $output = ob_get_contents(); + ob_end_clean(); + + return $output; + } + + /** + * Web interface evaluate results. + * + * @param \stdClass[] $results + * @param string[] $executionlog + * @return string HTML + */ + public function render_evaluate_results($results, $executionlog = array()) { + global $OUTPUT; + + $output = ''; + + foreach ($results as $timesplittingid => $result) { + + if (!CLI_SCRIPT) { + $output .= $OUTPUT->box_start('generalbox m-b-3'); + } + + // Check that the array key is a string, not all results depend on time splitting methods (e.g. general errors). + if (!is_numeric($timesplittingid)) { + $timesplitting = \core_analytics\manager::get_time_splitting($timesplittingid); + $langstrdata = (object)array('name' => $timesplitting->get_name(), 'id' => $timesplittingid); + + if (CLI_SCRIPT) { + $output .= $OUTPUT->heading(get_string('executionresultscli', 'tool_models', $langstrdata), 3); + } else { + $output .= $OUTPUT->heading(get_string('executionresults', 'tool_models', $langstrdata), 3); + } + } + + if ($result->status == 0) { + $output .= $OUTPUT->notification(get_string('goodmodel', 'tool_models'), + \core\output\notification::NOTIFY_SUCCESS); + } else if ($result->status === \core_analytics\model::NO_DATASET) { + $output .= $OUTPUT->notification(get_string('nodatatoevaluate', 'tool_models'), + \core\output\notification::NOTIFY_WARNING); + } + + if (isset($result->score)) { + // Score. + $output .= $OUTPUT->heading(get_string('accuracy', 'tool_models') . ': ' . round(floatval($result->score), 4) * 100 . '%', 4); + } + + if (!empty($result->info)) { + foreach ($result->info as $message) { + $output .= $OUTPUT->notification($message, \core\output\notification::NOTIFY_WARNING); + } + } + + if (!CLI_SCRIPT) { + $output .= $OUTPUT->box_end(); + } + } + + // Info logged during execution. + if (!empty($executionlog) && debugging()) { + $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_models'), 3); + foreach ($executionlog as $log) { + $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING); + } + } + + if (!CLI_SCRIPT) { + $output .= $OUTPUT->single_button(new \moodle_url('/admin/tool/models/index.php'), get_string('continue')); + } + + return $output; + } + + + /** + * Web interface execution results. + * + * @param array $trainresults + * @param string[] $trainlogs + * @param array $predictresults + * @param string[] $predictlogs + * @return string HTML + */ + public function render_execute_results($trainresults = false, $trainlogs = array(), $predictresults = false, $predictlogs = array()) { + global $OUTPUT; + + $output = ''; + + if ($trainresults || (!empty($trainlogs) && debugging())) { + $output .= $OUTPUT->heading(get_string('trainingresults', 'tool_models'), 3); + } + + if ($trainresults) { + if ($trainresults->status == 0) { + $output .= $OUTPUT->notification(get_string('trainingprocessfinished', 'tool_models'), + \core\output\notification::NOTIFY_SUCCESS); + } else if ($trainresults->status === \core_analytics\model::NO_DATASET) { + $output .= $OUTPUT->notification(get_string('nodatatotrain', 'analytics'), + \core\output\notification::NOTIFY_WARNING); + } else { + $output .= $OUTPUT->notification(get_string('generalerror', 'analytics', $result->status), + \core\output\notification::NOTIFY_ERROR); + } + } + + if (!empty($trainlogs) && debugging()) { + $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_models'), 4); + foreach ($trainlogs as $log) { + $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING); + } + } + + if ($predictresults || (!empty($predictlogs) && debugging())) { + $output .= $OUTPUT->heading(get_string('predictionresults', 'tool_models'), 3); + } + + if ($predictresults) { + if ($predictresults->status == 0) { + $output .= $OUTPUT->notification(get_string('predictionprocessfinished', 'tool_models'), + \core\output\notification::NOTIFY_SUCCESS); + } else if ($predictresults->status === \core_analytics\model::NO_DATASET) { + $output .= $OUTPUT->notification(get_string('nodatatopredict', 'analytics'), + \core\output\notification::NOTIFY_WARNING); + } else { + $output .= $OUTPUT->notification(get_string('generalerror', 'analytics', $result->status), + \core\output\notification::NOTIFY_ERROR); + } + } + + if (!empty($predictlogs) && debugging()) { + $output .= $OUTPUT->heading(get_string('extrainfo', 'tool_models'), 4); + foreach ($predictlogs as $log) { + $output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING); + } + } + + if (!CLI_SCRIPT) { + $output .= $OUTPUT->single_button(new \moodle_url('/admin/tool/models/index.php'), get_string('continue')); + } + + return $output; + } +} diff --git a/admin/tool/models/classes/task/predict_models.php b/admin/tool/models/classes/task/predict_models.php new file mode 100644 index 00000000000..e27e1fb7167 --- /dev/null +++ b/admin/tool/models/classes/task/predict_models.php @@ -0,0 +1,62 @@ +. + +/** + * Predict system models with new data available. + * + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_models\task; + +/** + * Predict system models with new data available. + * + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class predict_models extends \core\task\scheduled_task { + + public function get_name() { + return get_string('predictmodels', 'tool_models'); + } + + public function execute() { + global $DB, $OUTPUT, $PAGE; + + $models = $DB->get_records_select('analytics_models', 'enabled = 1 AND trained = 1 AND timesplitting IS NOT NULL'); + if (!$models) { + mtrace(get_string('errornoenabledandtrainedmodels', 'tool_models')); + return; + } + + foreach ($models as $modelobj) { + $model = new \core_analytics\model($modelobj); + + $result = $model->predict(); + if ($result) { + echo $OUTPUT->heading(get_string('modelresults', 'tool_models', $model->get_target()->get_name())); + $renderer = $PAGE->get_renderer('tool_models'); + echo $renderer->render_execute_results(false, array(), $result, $model->get_analyser()->get_logs()); + } + } + + } +} diff --git a/admin/tool/models/classes/task/train_models.php b/admin/tool/models/classes/task/train_models.php new file mode 100644 index 00000000000..3989b59bbff --- /dev/null +++ b/admin/tool/models/classes/task/train_models.php @@ -0,0 +1,61 @@ +. + +/** + * Train system models with new data available. + * + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_models\task; + +/** + * Train system models with new data available. + * + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class train_models extends \core\task\scheduled_task { + + public function get_name() { + return get_string('trainmodels', 'tool_models'); + } + + public function execute() { + global $DB, $OUTPUT, $PAGE; + + $models = $DB->get_records_select('analytics_models', 'enabled = 1 AND timesplitting IS NOT NULL'); + if (!$models) { + mtrace(get_string('errornoenabledmodels', 'tool_models')); + return; + } + foreach ($models as $modelobj) { + $model = new \core_analytics\model($modelobj); + + $result = $model->train(); + if ($result) { + echo $OUTPUT->heading(get_string('modelresults', 'tool_models', $model->get_target()->get_name())); + + $renderer = $PAGE->get_renderer('tool_models'); + echo $renderer->render_execute_results($result, $model->get_analyser()->get_logs()); + } + } + } +} diff --git a/admin/tool/models/cli/enable_model.php b/admin/tool/models/cli/enable_model.php new file mode 100644 index 00000000000..13ceca8959b --- /dev/null +++ b/admin/tool/models/cli/enable_model.php @@ -0,0 +1,73 @@ +. + +/** + * Enables the provided model. + * + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('CLI_SCRIPT', true); + +require_once(__DIR__ . '/../../../../config.php'); +require_once($CFG->libdir.'/clilib.php'); + +$help = "Enables the provided model. + +Options: +--modelid Model id +--timesplitting Time splitting method full class name +-h, --help Print out this help + +Example: +\$ php admin/tool/models/cli/enable_model.php --modelid=1 --timesplitting=\"\\core_analytics\\local\\time_splitting\\quarters\" +"; + +// Now get cli options. +list($options, $unrecognized) = cli_get_params( + array( + 'help' => false, + 'modelid' => false, + 'timesplitting' => false + ), + array( + 'h' => 'help', + ) +); + +if ($options['help']) { + echo $help; + exit(0); +} + +if ($options['modelid'] === false || $options['timesplitting'] === false) { + echo $help; + exit(0); +} + +// We need admin permissions. +\core\session\manager::set_user(get_admin()); + +$modelobj = $DB->get_record('analytics_models', array('id' => $options['modelid']), '*', MUST_EXIST); +$model = new \core_analytics\model($modelobj); + +// Evaluate its suitability to predict accurately. +$model->enable($options['timesplitting']); + +cli_heading(get_string('success')); +exit(0); diff --git a/admin/tool/models/cli/evaluate_model.php b/admin/tool/models/cli/evaluate_model.php new file mode 100644 index 00000000000..52934907a41 --- /dev/null +++ b/admin/tool/models/cli/evaluate_model.php @@ -0,0 +1,132 @@ +. + +/** + * Evaluates the provided model. + * + * @package tool_models + * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('CLI_SCRIPT', true); + +require_once(__DIR__ . '/../../../../config.php'); +require_once($CFG->libdir.'/clilib.php'); + +$help = "Evaluates the provided model. + +Options: +--modelid Model id +--non-interactive Not interactive questions +--timesplitting Restrict the evaluation to 1 single time splitting method (Optional) +--filter Analyser dependant. e.g. A courseid would evaluate the model using a single course (Optional) +--reuse-prev-analysed Reuse recently analysed courses instead of analysing the whole site. Set it to false while" . + " coding indicators. Defaults to true (Optional)" . " +-h, --help Print out this help + +Example: +\$ php admin/tool/models/cli/evaluate_model.php --modelid=1 --timesplitting='\\core_analytics\\local\\time_splitting\\quarters' --filter=123,321 +"; + +// Now get cli options. +list($options, $unrecognized) = cli_get_params( + array( + 'help' => false, + 'modelid' => false, + 'timesplitting' => false, + 'reuse-prev-analysed' => true, + 'non-interactive' => false, + 'filter' => false + ), + array( + 'h' => 'help', + ) +); + +if ($options['help']) { + echo $help; + exit(0); +} + +if ($options['modelid'] === false) { + echo $help; + exit(0); +} + +// Reformat them as an array. +if ($options['filter'] !== false) { + $options['filter'] = explode(',', $options['filter']); +} + +// We need admin permissions. +\core\session\manager::set_user(get_admin()); + +$modelobj = $DB->get_record('analytics_models', array('id' => $options['modelid']), '*', MUST_EXIST); +$model = new \core_analytics\model($modelobj); + +mtrace(get_string('analysingsitedata', 'tool_models')); + +if ($options['reuse-prev-analysed']) { + mtrace(get_string('evaluationinbatches', 'tool_models')); +} + +$analyseroptions = array( + 'filter' => $options['filter'], + 'timesplitting' => $options['timesplitting'], + 'reuseprevanalysed' => $options['reuse-prev-analysed'], +); +// Evaluate its suitability to predict accurately. +$results = $model->evaluate($analyseroptions); + +$renderer = $PAGE->get_renderer('tool_models'); +echo $renderer->render_evaluate_results($results, $model->get_analyser()->get_logs()); + +// Check that we have, at leasa,t 1 valid dataset (not necessarily good) to use. +foreach ($results as $result) { + if ($result->status !== \core_analytics\model::NO_DATASET && + $result->status !== \core_analytics\model::GENERAL_ERROR) { + $validdatasets = true; + } +} + +if (!empty($validdatasets) && !$model->is_enabled() && $options['non-interactive'] === false) { + + // Select a dataset, train and enable the model. + $input = cli_input(get_string('clienablemodel', 'tool_models')); + while (!\core_analytics\manager::is_valid($input, '\core_analytics\local\time_splitting\base') && $input !== 'none') { + mtrace(get_string('errorunexistingtimesplitting', 'tool_models')); + $input = cli_input(get_string('clienablemodel', 'tool_models')); + } + + if ($input === 'none') { + exit(0); + } + + // Refresh the instance to prevent unexpected issues. + $model = new \core_analytics\model($modelobj); + + // Set the time splitting method file and enable it. + $model->enable($input); + + mtrace(get_string('executingmodel', 'tool_models')); + + // Train the model with the selected time splitting method and start predicting. + $model->train(); + $model->predict(); +} + +exit(0); diff --git a/admin/tool/models/cli/guess_course_start_and_end.php b/admin/tool/models/cli/guess_course_start_and_end.php new file mode 100644 index 00000000000..e4d8b37cb23 --- /dev/null +++ b/admin/tool/models/cli/guess_course_start_and_end.php @@ -0,0 +1,190 @@ +. + +/** + * Guesses course start and end dates based on activity logs. + * + * @package tool_models + * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +define('CLI_SCRIPT', true); + +require_once(__DIR__ . '/../../../../config.php'); +require_once($CFG->libdir.'/clilib.php'); + +require_once($CFG->dirroot . '/course/lib.php'); +require_once($CFG->dirroot . '/course/format/weeks/lib.php'); + +$help = "Guesses course start and end dates based on activity logs. + +Options: +--guessstart Guess the course start date (default to true) +--guessend Guess the course end date (default to true) +--guessall Guess all start and end dates, even if they are already set (default to false) +--update Update the db or just notify the guess (default to false) +--filter Analyser dependant. e.g. A courseid would evaluate the model using a single course (Optional) +-h, --help Print out this help + +Example: +\$ php admin/tool/models/cli/guess_course_start_and_end_dates.php --update=1 --filter=123,321 +"; + +// Now get cli options. +list($options, $unrecognized) = cli_get_params( + array( + 'help' => false, + 'guessstart' => true, + 'guessend' => true, + 'guessall' => false, + 'update' => false, + 'filter' => false + ), + array( + 'h' => 'help', + ) +); + +if ($options['help']) { + echo $help; + exit(0); +} + +if ($options['guessstart'] === false && $options['guessend'] === false && $options['guessall'] === false) { + echo $help; + exit(0); +} + +// Reformat them as an array. +if ($options['filter'] !== false) { + $options['filter'] = explode(',', clean_param($options['filter'], PARAM_SEQUENCE)); +} + +// We need admin permissions. +\core\session\manager::set_user(get_admin()); + +$conditions = array(); +if (!$options['guessall']) { + if ($options['guessstart']) { + $conditions[] = '(startdate is null or startdate = 0)'; + } + if ($options['guessend']) { + $conditions[] = '(enddate is null or enddate = 0)'; + } +} + +$coursessql = ''; +$params = null; +if ($options['filter']) { + list($coursessql, $params) = $DB->get_in_or_equal($options['filter'], SQL_PARAMS_NAMED); + $conditions[] = 'id ' . $coursessql; +} + +$courses = $DB->get_recordset_select('course', implode(' AND ', $conditions), $params, 'sortorder ASC'); +foreach ($courses as $course) { + tool_models_calculate_course_dates($course, $options); +} +$courses->close(); + + +function tool_models_calculate_course_dates($course, $options) { + global $DB, $OUTPUT; + + $courseman = new \core_analytics\course($course); + + $notification = $course->shortname . ' (id = ' . $course->id . '): '; + + if ($options['guessstart'] || $options['guessall']) { + + $originalstartdate = $course->startdate; + + $guessedstartdate = $courseman->guess_start(); + if ($guessedstartdate == $originalstartdate) { + if (!$guessedstartdate) { + $notification .= PHP_EOL . ' ' . get_string('cantguessstartdate', 'tool_models'); + } else { + // No need to update. + $notification .= PHP_EOL . ' ' . get_string('samestartdate', 'tool_models') . ': ' . userdate($guessedstartdate); + } + } else if (!$guessedstartdate) { + $notification .= PHP_EOL . ' ' . get_string('cantguessstartdate', 'tool_models'); + } else { + // Update it to something we guess. + + // We set it to $course even if we don't update because may be needed to guess the end one. + $course->startdate = $guessedstartdate; + $notification .= PHP_EOL . ' ' . get_string('startdate') . ': ' . userdate($guessedstartdate); + + // Two different course updates because week's end date may be recalculated after setting the start date. + if ($options['update']) { + update_course($course); + + // Refresh course data as end date may have been updated. + $course = $DB->get_record('course', array('id' => $course->id)); + $courseman = new \core_analytics\course($course); + } + } + } + + if ($options['guessend'] || $options['guessall']) { + + $originalenddate = $course->enddate; + + $format = course_get_format($course); + $formatoptions = $format->get_format_options(); + + if ($course->format === 'weeks' && $formatoptions['automaticenddate']) { + // Special treatment for weeks with automatic end date. + + if ($options['update']) { + format_weeks::update_end_date($course->id); + $course->enddate = $DB->get_field('course', 'enddate', array('id' => $course->id)); + $notification .= PHP_EOL . ' ' . get_string('weeksenddateautomaticallyset', 'tool_models') . ': ' . userdate($course->enddate); + } else { + // We can't provide more info without actually updating it in db. + $notification .= PHP_EOL . ' ' . get_string('weeksenddatedefault', 'tool_models'); + } + } else { + $guessedenddate = $courseman->guess_end(); + + if ($guessedenddate == $originalenddate) { + if (!$guessedenddate) { + $notification .= PHP_EOL . ' ' . get_string('cantguessenddate', 'tool_models'); + } else { + // No need to update. + $notification .= PHP_EOL . ' ' . get_string('sameenddate', 'tool_models') . ': ' . userdate($guessedenddate); + } + } else if (!$guessedenddate) { + $notification .= PHP_EOL . ' ' . get_string('cantguessenddate', 'tool_models'); + } else { + // Update it to something we guess. + + $course->enddate = $guessedenddate; + $notification .= PHP_EOL . ' ' . get_string('enddate') . ': ' . userdate($guessedenddate); + + if ($options['update']) { + update_course($course); + } + } + } + + } + + echo mtrace($notification); +} + +exit(0); diff --git a/admin/tool/models/db/install.php b/admin/tool/models/db/install.php new file mode 100644 index 00000000000..894c5e19fb0 --- /dev/null +++ b/admin/tool/models/db/install.php @@ -0,0 +1,42 @@ +. + +/** + * Tool models install function. + * + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Tool models install function. + * + * @return void + */ +function xmldb_tool_models_install() { + + // TODO All of them for the moment, we will define a limited set of them once in core. + $target = \core_analytics\manager::get_target('\tool_models\local\target\course_dropout'); + $indicators = \core_analytics\manager::get_all_indicators(); + + // We need the model to be created in order to know all its potential indicators and set them. + $model = \core_analytics\model::create($target, array()); + + $model->update(0, $model->get_potential_indicators()); +} diff --git a/admin/tool/models/db/tasks.php b/admin/tool/models/db/tasks.php new file mode 100644 index 00000000000..276a2775328 --- /dev/null +++ b/admin/tool/models/db/tasks.php @@ -0,0 +1,45 @@ +. + +/** + * This file defines tasks performed by the tool. + * + * @package tool_monitor + * @copyright 2014 Mark Nelson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// List of tasks. +$tasks = array( + array( + 'classname' => 'tool_models\task\train_models', + 'blocking' => 0, + 'minute' => '0', + 'hour' => 'R', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), + array( + 'classname' => 'tool_models\task\predict_models', + 'blocking' => 0, + 'minute' => '0', + 'hour' => 'R', + 'day' => '*', + 'dayofweek' => '*', + 'month' => '*' + ), +); diff --git a/admin/tool/models/index.php b/admin/tool/models/index.php new file mode 100644 index 00000000000..90ca5b3283c --- /dev/null +++ b/admin/tool/models/index.php @@ -0,0 +1,37 @@ +. + +/** + * Inspire tool frontend. + * + * @package tool_models + * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); +require_once($CFG->libdir . '/adminlib.php'); + +admin_externalpage_setup('analyticmodels', '', null, '', array('pagelayout'=>'report')); + +$models = \core_analytics\manager::get_all_models(); + +echo $OUTPUT->header(); + +$renderable = new \tool_models\output\models_list($models); +echo $PAGE->get_renderer('tool_models')->render($renderable); + +echo $OUTPUT->footer(); diff --git a/admin/tool/models/lang/en/tool_models.php b/admin/tool/models/lang/en/tool_models.php new file mode 100644 index 00000000000..5c5cf629a34 --- /dev/null +++ b/admin/tool/models/lang/en/tool_models.php @@ -0,0 +1,90 @@ +. + +/** + * Strings for tool_models. + * + * @package tool_models + * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['accuracy'] = 'Accuracy'; +$string['allindicators'] = 'All indicators'; +$string['analysingsitedata'] = 'Analysing the site'; +$string['analyticmodels'] = 'Analytic models'; +$string['bettercli'] = 'Models\' evaluation and execution are heavy processes, it is better to run them through command line interface'; +$string['cantguessstartdate'] = 'Can\'t guess the start date'; +$string['cantguessenddate'] = 'Can\'t guess the end date'; +$string['clienablemodel'] = 'You can enable the model by selecting a time splitting method by its id. Note that you can also enable it later using the web interface (\'none\' to exit)'; +$string['coursenotyetstarted'] = 'The course is not yet started'; +$string['coursenotyetfinished'] = 'The course is not yet finished'; +$string['editmodel'] = 'Edit model {$a}'; +$string['edittrainedwarning'] = 'This model has already been trained, note that changing its indicators or its time splitting method will delete its previous predictions and start generating the new ones'; +$string['enabled'] = 'Enabled'; +$string['errorcantenablenotimesplitting'] = 'You need to select a time splitting method before enabling the model'; +$string['errornoenabledandtrainedmodels'] = 'There are not enabled and trained models to predict'; +$string['errornoenabledmodels'] = 'There are not enabled models to train'; +$string['evaluate'] = 'Evaluate'; +$string['evaluatemodel'] = 'Evaluate model'; +$string['evaluationinbatches'] = 'The site contents are calculated and stored in batches, during evaluation you can stop the process at any moment, the next time you run it it will continue from the point you stopped it.'; +$string['executemodel'] = 'Execute'; +$string['executingmodel'] = 'Training model and calculating predictions'; +$string['executionresultscli'] = 'Results using {$a->name} (id: {$a->id}) course duration splitting'; +$string['executionresults'] = 'Results using {$a->name} course duration splitting'; +$string['extrainfo'] = 'Info'; +$string['generalerror'] = 'Evaluation error. Status code {$a}'; + +$string['goodmodel'] = 'This is a good model and it can be used to predict, enable it and execute it to start getting predictions.'; +$string['indicators'] = 'Indicators'; +$string['info'] = 'Info'; +$string['labelstudentdropoutyes'] = 'Student at risk of dropping out'; +$string['labelstudentdropoutno'] = 'Not at risk'; +$string['loginfo'] = 'Log extra info'; +$string['lowaccuracy'] = 'The model accuracy is low'; +$string['modelslist'] = 'Models list'; +$string['modeltimesplitting'] = 'Time splitting'; +$string['nocompletiondetection'] = 'No method available to detect course completion (no completion nor competencies nor course grade pass)'; +$string['nocourseactivity'] = 'Not enough course activity between the start and the end of the course'; +$string['nocourseendtime'] = 'The course does not have an end time'; +$string['nocoursesections'] = 'No course sections'; +$string['nocoursestudents'] = 'No students'; +$string['nodatatoevaluate'] = 'There is no data to evaluate the model'; +$string['nodatatopredict'] = 'There is no data to use for predictions'; +$string['notdefined'] = 'Not yet defined'; +$string['prediction'] = 'Prediction'; +$string['predictionresults'] = 'Prediction results'; +$string['predictions'] = 'Predictions'; +$string['predictmodels'] = 'Predict models'; +$string['predictorresultsin'] = 'Predictor logged information in {$a} directory'; +$string['predictiondetails'] = 'Prediction details'; +$string['predictionprocessfinished'] = 'Prediction process finished'; + +$string['pluginname'] = 'Analytic models'; +$string['modelresults'] = '{$a} results'; +$string['samestartdate'] = 'Current start date is good'; +$string['sameenddate'] = 'Current end date is good'; +$string['target'] = 'Target'; +$string['target:coursedropout'] = 'Students at risk of dropping out'; +$string['target:coursedropoutinfo'] = 'Here you can find a list of students at risk of dropping out.'; +$string['timemodified'] = 'Last modification'; +$string['trainingprocessfinished'] = 'Training process finished'; +$string['trainingresults'] = 'Training results'; +$string['trainmodels'] = 'Train models'; +$string['viewlog'] = 'Log'; +$string['viewpredictions'] = 'View model predictions'; +$string['weeksenddateautomaticallyset'] = 'End date automatically set based on start date and the number of sections'; +$string['weeksenddatedefault'] = 'End date would be automatically calculated from the course start date'; diff --git a/admin/tool/models/model.php b/admin/tool/models/model.php new file mode 100644 index 00000000000..a85c6447f17 --- /dev/null +++ b/admin/tool/models/model.php @@ -0,0 +1,137 @@ +. + +/** + * Model-related actions. + * + * @package tool_model + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../config.php'); + +$id = required_param('id', PARAM_INT); +$action = required_param('action', PARAM_ALPHANUMEXT); + +$context = context_system::instance(); + +require_login(); +require_capability('moodle/analytics:managemodels', $context); + +$model = new \core_analytics\model($id); + +$params = array('id' => $id, 'action' => $action); +$url = new \moodle_url('/admin/tool/models/model.php', $params); + +switch ($action) { + + case 'edit': + $title = get_string('editmodel', 'tool_models', $model->get_target()->get_name()); + break; + case 'evaluate': + $title = get_string('evaluatemodel', 'tool_models'); + break; + case 'execute': + $title = get_string('executemodel', 'tool_models'); + break; + case 'log': + $title = get_string('viewlog', 'tool_models'); + break; + default: + throw new moodle_exception('errorunknownaction', 'tool_models'); +} + +$PAGE->set_context($context); +$PAGE->set_url($url); +$PAGE->set_pagelayout('report'); +$PAGE->set_title($title); +$PAGE->set_heading($title); + +switch ($action) { + + case 'edit': + + $customdata = array( + 'id' => $model->get_id(), + 'model' => $model, + 'indicators' => $model->get_potential_indicators(), + 'timesplittings' => \core_analytics\manager::get_enabled_time_splitting_methods() + ); + $mform = new \tool_models\output\form\edit_model(null, $customdata); + + if ($mform->is_cancelled()) { + redirect(new \moodle_url('/admin/tool/models/index.php')); + + } else if ($data = $mform->get_data()) { + confirm_sesskey(); + + // Converting option names to class names. + $indicators = array(); + foreach ($data->indicators as $indicator) { + $indicatorclass = \tool_models\output\helper::option_to_class($indicator); + $indicators[] = \core_analytics\manager::get_indicator($indicatorclass); + } + $timesplitting = \tool_models\output\helper::option_to_class($data->timesplitting); + $model->update($data->enabled, $indicators, $timesplitting); + redirect(new \moodle_url('/admin/tool/models/index.php')); + } + + echo $OUTPUT->header(); + + $modelobj = $model->get_model_obj(); + + $callable = array('\tool_models\output\helper', 'class_to_option'); + $modelobj->indicators = array_map($callable, json_decode($modelobj->indicators)); + $modelobj->timesplitting = \tool_models\output\helper::class_to_option($modelobj->timesplitting); + $mform->set_data($modelobj); + $mform->display(); + break; + + case 'evaluate': + echo $OUTPUT->header(); + // Web interface is used by people who can not use CLI nor code stuff, always use + // cached stuff as they will change the model through the web interface as well + // which invalidates the previously analysed stuff. + $results = $model->evaluate(array('reuseprevanalysed' => true)); + $renderer = $PAGE->get_renderer('tool_models'); + echo $renderer->render_evaluate_results($results, $model->get_analyser()->get_logs()); + break; + + case 'execute': + echo $OUTPUT->header(); + + $trainresults = $model->train(); + $trainlogs = $model->get_analyser()->get_logs(); + + // Looks dumb to get a new instance but better be conservative. + $model = new \core_analytics\model($model->get_model_obj()); + $predictresults = $model->predict(); + $predictlogs = $model->get_analyser()->get_logs(); + + $renderer = $PAGE->get_renderer('tool_models'); + echo $renderer->render_execute_results($trainresults, $trainlogs, $predictresults, $predictlogs); + break; + + case 'log': + echo $OUTPUT->header(); + $renderer = $PAGE->get_renderer('tool_models'); + $modellogstable = new \tool_models\output\model_logs('model-' . $model->get_id(), $model->get_id()); + echo $renderer->render_table($modellogstable); + break; +} + +echo $OUTPUT->footer(); diff --git a/admin/tool/models/settings.php b/admin/tool/models/settings.php new file mode 100644 index 00000000000..94b0c6c9c97 --- /dev/null +++ b/admin/tool/models/settings.php @@ -0,0 +1,28 @@ +. + +/** + * Adds settings links to admin tree. + * + * @package tool_models + * @copyright 2016 David Monllao {@link http://www.davidmonllao.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$ADMIN->add('reports', new admin_externalpage('analyticmodels', get_string('analyticmodels', 'tool_models'), + "$CFG->wwwroot/$CFG->admin/tool/models/index.php", 'moodle/analytics:managemodels')); diff --git a/admin/tool/models/templates/models_list.mustache b/admin/tool/models/templates/models_list.mustache new file mode 100644 index 00000000000..6ab51f9eec9 --- /dev/null +++ b/admin/tool/models/templates/models_list.mustache @@ -0,0 +1,91 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tool_models/models_list + + Template for models list. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * none + + Example context (json): + { + } +}} + +{{#warnings}} + {{> core/notification_warning}} +{{/warnings}} +
+ + + + + + + + + + + + + + {{#models}} + + + + + + + + + {{/models}} + +
{{#str}}modelslist, tool_models{{/str}}
{{#str}}target, tool_models{{/str}}{{#str}}enabled, tool_models{{/str}}{{#str}}indicators, tool_models{{/str}}{{#str}}modeltimesplitting, tool_models{{/str}}{{#str}}viewpredictions, tool_models{{/str}}{{#str}}actions{{/str}}
{{target}} + {{#enabled}} + {{#pix}}i/checked, core, {{#str}}yes{{/str}}{{/pix}} + {{/enabled}} + {{^enabled}} + {{#str}}no{{/str}} + {{/enabled}} + +
    + {{#indicators}} +
  • {{.}}
  • + {{/indicators}} +
+
+ {{#timesplitting}}{{timesplitting}}{{/timesplitting}}{{^timesplitting}}{{#str}}notdefined, tool_models{{/str}}{{/timesplitting}} + + {{#predictions}} + {{> core/single_select }} + {{/predictions}} + {{^predictions}} + {{#str}}nopredictionsyet, analytics{{/str}} + {{/predictions}} + + {{#actions}} + {{> core/action_menu}} + {{/actions}} +
+
diff --git a/admin/tool/models/version.php b/admin/tool/models/version.php new file mode 100644 index 00000000000..e3a2adb3ce1 --- /dev/null +++ b/admin/tool/models/version.php @@ -0,0 +1,29 @@ +. + +/** + * Version details. + * + * @package tool_models + * @copyright 2017 David Monllao {@link http://www.davidmonllao.com/} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2017051500; // The current plugin version (Date: YYYYMMDDXX). +$plugin->requires = 2017050500; // Requires this Moodle version. +$plugin->component = 'tool_models'; // Full name of the plugin (used for diagnostics).