diff --git a/analytics/classes/stats.php b/analytics/classes/stats.php new file mode 100644 index 00000000000..0caf9758fc7 --- /dev/null +++ b/analytics/classes/stats.php @@ -0,0 +1,78 @@ +. + +/** + * Provides the {@link \core_analytics\stats} class. + * + * @package core_analytics + * @copyright 2019 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_analytics; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Provides stats and meta information about the analytics usage on this site. + * + * @copyright 2019 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class stats { + + /** + * Return the number of models enabled on this site. + * + * @return int + */ + public static function enabled_models() : int { + return count(manager::get_all_models(true)); + } + + /** + * Return the number of predictions generated by the system. + * + * @return int + */ + public static function predictions() : int { + global $DB; + + return $DB->count_records('analytics_predictions'); + } + + /** + * Return the number of suggested actions executed by users. + * + * @return int + */ + public static function actions() : int { + global $DB; + + return $DB->count_records('analytics_prediction_actions'); + } + + /** + * Return the number of suggested actions flagged as not useful. + * + * @return int + */ + public static function actions_not_useful() : int { + global $DB; + + return $DB->count_records('analytics_prediction_actions', ['actionname' => prediction::ACTION_NOT_USEFUL]); + } +} diff --git a/analytics/tests/stats_test.php b/analytics/tests/stats_test.php new file mode 100644 index 00000000000..d92f40375cb --- /dev/null +++ b/analytics/tests/stats_test.php @@ -0,0 +1,162 @@ +. + +/** + * Provides the {@link analytics_stats_testcase} class. + * + * @package core_analytics + * @category test + * @copyright 2019 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/fixtures/test_indicator_fullname.php'); +require_once(__DIR__ . '/fixtures/test_target_shortname.php'); + +/** + * Unit tests for the analytics stats. + * + * @copyright 2019 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class analytics_stats_testcase extends advanced_testcase { + + /** + * Set up the test environment. + */ + public function setUp() { + + $this->setAdminUser(); + } + + /** + * Test the {@link \core_analytics\stats::enabled_models()} implementation. + */ + public function test_enabled_models() { + + $this->resetAfterTest(true); + + // By default, sites have {@link \core\analytics\target\no_teaching} enabled. + $this->assertEquals(1, \core_analytics\stats::enabled_models()); + + $model = \core_analytics\model::create( + \core_analytics\manager::get_target('\core\analytics\target\course_dropout'), + [ + \core_analytics\manager::get_indicator('\core\analytics\indicator\any_write_action'), + ] + ); + + // Purely adding a new model does not make it included in the stats. + $this->assertEquals(1, \core_analytics\stats::enabled_models()); + + // New models must be enabled to have them counted. + $model->enable('\core\analytics\time_splitting\quarters'); + $this->assertEquals(2, \core_analytics\stats::enabled_models()); + } + + /** + * Test the {@link \core_analytics\stats::predictions()} implementation. + */ + public function test_predictions() { + + $this->resetAfterTest(true); + + $model = \core_analytics\model::create( + \core_analytics\manager::get_target('test_target_shortname'), + [ + \core_analytics\manager::get_indicator('test_indicator_fullname'), + ] + ); + + $model->enable('\core\analytics\time_splitting\no_splitting'); + + // Train the model. + $this->getDataGenerator()->create_course(['shortname' => 'a', 'fullname' => 'a', 'visible' => 1]); + $this->getDataGenerator()->create_course(['shortname' => 'b', 'fullname' => 'b', 'visible' => 1]); + $model->train(); + + // No predictions yet. + $this->assertEquals(0, \core_analytics\stats::predictions()); + + // Get one new prediction. + $this->getDataGenerator()->create_course(['shortname' => 'aa', 'fullname' => 'aa', 'visible' => 0]); + $result = $model->predict(); + + $this->assertEquals(1, count($result->predictions)); + $this->assertEquals(1, \core_analytics\stats::predictions()); + + // Nothing changes if there is no new prediction. + $result = $model->predict(); + $this->assertFalse(isset($result->predictions)); + $this->assertEquals(1, \core_analytics\stats::predictions()); + + // Get two more predictions, we have three in total now. + $this->getDataGenerator()->create_course(['shortname' => 'bb', 'fullname' => 'bb', 'visible' => 0]); + $this->getDataGenerator()->create_course(['shortname' => 'cc', 'fullname' => 'cc', 'visible' => 0]); + + $result = $model->predict(); + $this->assertEquals(2, count($result->predictions)); + $this->assertEquals(3, \core_analytics\stats::predictions()); + } + + /** + * Test the {@link \core_analytics\stats::actions()} and {@link \core_analytics\stats::actions_not_useful()} implementation. + */ + public function test_actions() { + global $DB; + $this->resetAfterTest(true); + + $model = \core_analytics\model::create( + \core_analytics\manager::get_target('test_target_shortname'), + [ + \core_analytics\manager::get_indicator('test_indicator_fullname'), + ] + ); + + $model->enable('\core\analytics\time_splitting\no_splitting'); + + // Train the model. + $this->getDataGenerator()->create_course(['shortname' => 'a', 'fullname' => 'a', 'visible' => 1]); + $this->getDataGenerator()->create_course(['shortname' => 'b', 'fullname' => 'b', 'visible' => 1]); + $model->train(); + + // Generate two predictions. + $this->getDataGenerator()->create_course(['shortname' => 'aa', 'fullname' => 'aa', 'visible' => 0]); + $this->getDataGenerator()->create_course(['shortname' => 'bb', 'fullname' => 'bb', 'visible' => 0]); + $model->predict(); + + list($p1, $p2) = array_values($DB->get_records('analytics_predictions')); + + $p1 = new \core_analytics\prediction($p1, []); + $p2 = new \core_analytics\prediction($p2, []); + + // No actions executed at the start. + $this->assertEquals(0, \core_analytics\stats::actions()); + $this->assertEquals(0, \core_analytics\stats::actions_not_useful()); + + // The user has acknowledged the first prediction. + $p1->action_executed(\core_analytics\prediction::ACTION_FIXED, $model->get_target()); + $this->assertEquals(1, \core_analytics\stats::actions()); + $this->assertEquals(0, \core_analytics\stats::actions_not_useful()); + + // The user has marked the other prediction as not useful. + $p2->action_executed(\core_analytics\prediction::ACTION_NOT_USEFUL, $model->get_target()); + $this->assertEquals(2, \core_analytics\stats::actions()); + $this->assertEquals(1, \core_analytics\stats::actions_not_useful()); + } +} diff --git a/lang/en/hub.php b/lang/en/hub.php index 2e2bcf75e9d..92af304eecb 100644 --- a/lang/en/hub.php +++ b/lang/en/hub.php @@ -29,6 +29,10 @@ $string['advertised'] = 'For people to join'; $string['advertiseon'] = 'Share this course on {$a}'; $string['readvertiseon'] = 'Update advertising information on {$a}'; $string['advertisepublication_help'] = 'This course will be listed on Moodle.net as a course that people can enrol in and participate. Email-based self-registration should be enabled on the site and you need to enable self enrolment in this course.'; +$string['analyticsactions'] = 'Number of actions taken on generated predictions ({$a})'; +$string['analyticsactionsnotuseful'] = 'Number of actions marking a prediction as not useful ({$a})'; +$string['analyticsenabledmodels'] = 'Number of enabled prediction models ({$a})'; +$string['analyticspredictions'] = 'Number of generated predictions ({$a})'; $string['audience'] = 'Audience'; $string['audience_help'] = 'Select the intended audience for the course.'; $string['audienceeducators'] = 'Educators'; diff --git a/lib/classes/hub/registration.php b/lib/classes/hub/registration.php index 823e47f92c5..805434fa9fd 100644 --- a/lib/classes/hub/registration.php +++ b/lib/classes/hub/registration.php @@ -56,6 +56,8 @@ class registration { 'commnews', // Receive communication news. This was added in 3.4 and is "On" by default. Admin must confirm or opt-out. 'mobileservicesenabled', 'mobilenotificationsenabled', 'registereduserdevices', 'registeredactiveuserdevices' // Mobile stats added in 3.4. ], + // Analytics stats added in Moodle 3.7. + 2019022200 => ['analyticsenabledmodels', 'analyticspredictions', 'analyticsactions', 'analyticsactionsnotuseful'], ]; /** @var Site privacy: not displayed */ @@ -196,6 +198,12 @@ class registration { } } + // Analytics related data follow. + $siteinfo['analyticsenabledmodels'] = \core_analytics\stats::enabled_models(); + $siteinfo['analyticspredictions'] = \core_analytics\stats::predictions(); + $siteinfo['analyticsactions'] = \core_analytics\stats::actions(); + $siteinfo['analyticsactionsnotuseful'] = \core_analytics\stats::actions_not_useful(); + // IMPORTANT: any new fields in siteinfo have to be added to the constant CONFIRM_NEW_FIELDS. return $siteinfo; @@ -236,6 +244,10 @@ class registration { 'mobilenotificationsenabled' => get_string('mobilenotificationsenabled', 'hub', $mobilenotificationsenabled), 'registereduserdevices' => get_string('registereduserdevices', 'hub', $siteinfo['registereduserdevices']), 'registeredactiveuserdevices' => get_string('registeredactiveuserdevices', 'hub', $siteinfo['registeredactiveuserdevices']), + 'analyticsenabledmodels' => get_string('analyticsenabledmodels', 'hub', $siteinfo['analyticsenabledmodels']), + 'analyticspredictions' => get_string('analyticspredictions', 'hub', $siteinfo['analyticspredictions']), + 'analyticsactions' => get_string('analyticsactions', 'hub', $siteinfo['analyticsactions']), + 'analyticsactionsnotuseful' => get_string('analyticsactionsnotuseful', 'hub', $siteinfo['analyticsactionsnotuseful']), ]; foreach ($senddata as $key => $str) {