Merge branch 'MDL-84296-500' of https://github.com/davewoloszyn/moodle into MOODLE_500_STABLE

This commit is contained in:
Huong Nguyen
2025-05-21 10:00:08 +07:00
4 changed files with 262 additions and 24 deletions
+10
View File
@@ -24,6 +24,7 @@
*/
$string['activeparticipantnumberaverage'] = 'Average number of recently active participants ({$a})';
$string['activeusersnumber'] = 'Number of recently active users ({$a})';
$string['aiusagedata'] = 'AI usage data';
$string['aiusagestats'] = 'AI usage stats ({$a->timefrom} - {$a->timeto})';
$string['analyticsactions'] = 'Number of actions taken on generated predictions ({$a})';
$string['analyticsactionsnotuseful'] = 'Number of actions marking a prediction as not useful ({$a})';
@@ -34,6 +35,7 @@ $string['audience_help'] = 'Select the intended audience for the course.';
$string['audienceeducators'] = 'Educators';
$string['audiencestudents'] = 'Students';
$string['audienceadmins'] = 'Moodle administrators';
$string['average_time'] = 'Average processing time ({$a})';
$string['badgesnumber'] = 'Number of badges ({$a})';
$string['communityremoved'] = 'That course link has been removed from your list';
$string['confirmregistration'] = 'Confirm registration';
@@ -66,6 +68,7 @@ $string['errorwstokenreset'] = '{$a}. Registration token on this site has been r
$string['errorregistrationupdate'] = 'An error occurred during registration update ({$a})';
$string['experttipsandinsights'] = 'Expert tips and insights';
$string['experttipsandinsightsdesc'] = 'Yes, sign me up for exclusive tips, strategies, and insights from Moodle experts and Certified Partners. Update your preferences or unsubscribe any time, as covered in our <a href="{$a}">Privacy Notice</a>.';
$string['fail_count'] = 'Fail count ({$a})';
$string['geolocation'] = 'Geolocation';
$string['geolocation_help'] = 'In future we may provide location-based searching. If you want to specify the location for your course use a latitude/longitude value here (eg: -31.947884,115.871285). One way to find this is to use Google Maps.';
$string['imageurl'] = 'Image URL';
@@ -80,6 +83,7 @@ $string['licence_link'] = 'licenses';
$string['logourl'] = 'Logo URL';
$string['mobilenotificationsenabled'] = 'Mobile notifications enabled ({$a})';
$string['mobileservicesenabled'] = 'Mobile services enabled ({$a})';
$string['models'] = 'Models';
$string['modulenumberaverage'] = 'Average number of course modules ({$a})';
$string['mustselectsubject'] = 'You must select a subject';
$string['name'] = 'Name';
@@ -87,6 +91,7 @@ $string['name_help'] = 'This name will be showing in the course listing.';
$string['neverchecked'] = 'Never checked';
$string['next'] = 'Next';
$string['no'] = 'No';
$string['noaiusagedata'] = 'AI usage data (none)';
$string['nocheckstatusfromunreghub'] = 'The site is not registered on the hub so the status can not be checked.';
$string['nohubselected'] = 'No hub selected';
$string['none'] = 'None';
@@ -98,6 +103,7 @@ $string['policyagreeddesc'] = 'I agree to the <a href="{$a}" target="_blank">Pri
$string['postaladdress'] = 'Postal address';
$string['postaladdress_help'] = 'Postal address of this site, or of the entity represented by this site.';
$string['postsnumber'] = 'Number of posts ({$a})';
$string['predominant_error'] = 'Predominant HTTP error ({$a})';
$string['previousregistrationdeleted'] = 'The previous registration has been deleted from {$a}. You can restart the registration process. Thank you.';
$string['primaryauthtype'] = 'Primary authentication type ({$a})';
$string['questionsnumber'] = 'Number of questions ({$a})';
@@ -184,8 +190,12 @@ $string['siteurl_help'] = 'The URL is the address of this site. If privacy sett
$string['siteversion'] = 'Moodle version';
$string['siteversion_help'] = 'The Moodle version of this site.';
$string['skipregistration'] = 'Skip';
$string['success_count'] = 'Success count ({$a})';
$string['subject'] = 'Subject';
$string['subject_help'] = 'Select the main subject area which the course covers.';
$string['timefrom'] = 'Time from ({$a})';
$string['timeto'] = 'Time to ({$a})';
$string['time_range'] = 'Time range';
$string['type'] = 'Shared';
$string['unregister'] = 'Unregister';
$string['unregistrationerror'] = 'An error occurred while attempting to unregister the site: {$a}';
+94 -1
View File
@@ -239,6 +239,8 @@ class registration {
* @return string
*/
public static function get_stats_summary($siteinfo) {
global $OUTPUT;
$fieldsneedconfirm = self::get_new_registration_fields();
$summary = html_writer::tag('p', get_string('sendfollowinginfo_help', 'hub')) .
html_writer::start_tag('ul');
@@ -284,7 +286,7 @@ class registration {
'sitetheme' => get_string('sitetheme', 'hub', $siteinfo['sitetheme']),
'primaryauthtype' => get_string('primaryauthtype', 'hub', $siteinfo['primaryauthtype']),
'pluginusage' => get_string('pluginusagedata', 'hub', $pluginusagelinks),
'aiusage' => get_string('aiusagestats', 'hub', self::get_ai_usage_time_range(true)),
'aiusage' => $OUTPUT->render_from_template('core/ai_usage_data', ['aiusagedata' => self::show_ai_usage()]),
];
foreach ($senddata as $key => $str) {
@@ -725,6 +727,97 @@ class registration {
];
}
/**
* Displays AI usage data for all providers.
*
* @return array Array containing usage data, grouped by provider
*/
public static function show_ai_usage(): array {
// Initialize aiusage collection.
$aiusage = [];
// Process each provider's data.
foreach (self::get_ai_usage_data() as $provider => $actions) {
if ($provider === 'time_range') {
$aiusage['timerange'] = [
'label' => get_string($provider, 'hub'),
'values' => self::format_ai_usage_actions($actions),
];
} else {
// Initialize provider data structure.
$aiusage['providers'][] = [
'providername' => get_string('pluginname', $provider),
'aiactions' => self::format_ai_usage_actions($actions),
];
}
}
return $aiusage;
}
/**
* Formats individual actions for a provider.
*
* @param array $actions Raw actions data
* @return array Formatted action data
*/
private static function format_ai_usage_actions(array $actions): array {
$formattedactions = [];
foreach ($actions as $action => $values) {
if (in_array($action, ['timefrom', 'timeto'])) {
$formattedactions[] = get_string($action, 'hub', userdate($values));
} else {
$formattedactions[] = [
'actionname' => get_string("action_$action", 'core_ai'),
'aiactionvalues' => self::format_ai_usage_action_values($values),
];
}
}
return $formattedactions;
}
/**
* Formats action values into formatted strings.
*
* @param array $values Action values to format
* @return array Formatted action values
*/
private static function format_ai_usage_action_values(array $values): array {
$formattedvalues = [];
foreach ($values as $key => $value) {
if (get_string_manager()->string_exists($key, 'hub')) {
if ($key === 'models') {
$formattedvalues[] = [
'label' => get_string($key, 'hub'),
'models' => self::format_ai_usage_model_values($value),
];
} else {
$formattedvalues[]['values'] = get_string($key, 'hub', $value);
}
}
}
return $formattedvalues;
}
/**
* Formats model values with their counts.
*
* @param array $values Formatted model values
*/
private static function format_ai_usage_model_values(array $values): array {
$modelvalues = [];
foreach ($values as $model => $count) {
$modelvalues[] = "$model ($count[count])";
}
return $modelvalues;
}
/**
* Get AI usage data.
*
+93
View File
@@ -0,0 +1,93 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template core/ai_usage_data
Template for AI usage data.
Example context (json):
{
"aiusagedata": [{
"providers": [{
"providerName": "AI provider name",
"aiactions": [{
"actionName": "AI action name",
"aiactionvalues": [{
"success_count": "1",
"fail_count": "2",
"average_time": "15",
"predominant_error": "403",
"models": [{
"gpt-4o": "1"
}]
}]
}]
}],
"timerange": [{
"label": "Time range for stats",
"values": [{
"timefrom": "Time from: Friday, 2 May 2025, 1:22 AM",
"timeto": "Time from: Friday, 3 May 2025, 1:22 AM"
}]
}]
}]
}
}}
{{#aiusagedata}}
{{#str}} aiusagedata, hub {{/str}}:
<ul>
{{! AI providers. }}
{{#providers}}
<li>{{providername}}:</li>
{{#aiactions}}
<ul>
<li>{{actionname}}:</li>
{{#aiactionvalues}}
<ul>
{{#values}}
<li>{{values}}</li>
{{/values}}
{{^values}}
<li>{{label}}:</li>
{{#models}}
<ul>
<li>{{.}}</li>
</ul>
{{/models}}
{{/values}}
</ul>
{{/aiactionvalues}}
</ul>
{{/aiactions}}
{{/providers}}
{{! Time range for stats. }}
{{#timerange}}
<li>{{label}}:</li>
{{#values}}
<ul>
<li>{{.}}</li>
</ul>
{{/values}}
{{/timerange}}
</ul>
{{/aiusagedata}}
{{^aiusagedata}}
{{#str}} noaiusagedata, hub {{/str}}
{{/aiusagedata}}
+65 -23
View File
@@ -90,16 +90,44 @@ final class registration_test extends \advanced_testcase {
* Test the AI usage data is calculated correctly.
*/
public function test_get_ai_usage(): void {
global $CFG, $DB;
$this->resetAfterTest();
$clock = $this->mock_clock_with_frozen();
$clock = $this->mock_clock_with_frozen(1700000000);
$this->generate_ai_usage_data();
// Get our site info and check the expected calculations are correct.
$siteinfo = registration::get_site_info();
$aisuage = json_decode($siteinfo['aiusage']);
// Check generated text.
$this->assertEquals(1, $aisuage->aiprovider_openai->generate_text->success_count);
$this->assertEquals(0, $aisuage->aiprovider_openai->generate_text->fail_count);
// Check generated images.
$this->assertEquals(2, $aisuage->aiprovider_openai->generate_image->success_count);
$this->assertEquals(3, $aisuage->aiprovider_openai->generate_image->fail_count);
$this->assertEquals(15, $aisuage->aiprovider_openai->generate_image->average_time);
$this->assertEquals(403, $aisuage->aiprovider_openai->generate_image->predominant_error);
// Check time range is set correctly.
$this->assertEquals($clock->time() - WEEKSECS, $aisuage->time_range->timefrom);
$this->assertEquals($clock->time(), $aisuage->time_range->timeto);
// Check model counts.
$gpt4omodel = 'gpt-4o';
$dalle3model = 'dall-e-3';
$this->assertEquals(1, $aisuage->aiprovider_openai->generate_text->models->{$gpt4omodel}->count);
$this->assertEquals(2, $aisuage->aiprovider_openai->generate_image->models->{$dalle3model}->count);
$this->assertEquals(3, $aisuage->aiprovider_openai->generate_image->models->unknown->count);
}
/**
* Create some dummy AI usage data.
*/
private function generate_ai_usage_data(): void {
global $DB;
$clock = $this->mock_clock_with_frozen(1700000000);
// Record some generated text.
$record = new \stdClass();
$record->provider = 'openai';
$record->provider = 'aiprovider_openai';
$record->actionname = 'generate_text';
$record->actionid = 1;
$record->userid = 1;
@@ -107,14 +135,14 @@ final class registration_test extends \advanced_testcase {
$record->success = true;
$record->timecreated = $clock->time() - 5;
$record->timecompleted = $clock->time();
$record->model = $gpt4omodel;
$record->model = 'gpt-4o';
$DB->insert_record('ai_action_register', $record);
// Record a generated image.
$record->actionname = 'generate_image';
$record->actionid = 111;
$record->timecreated = $clock->time() - 20;
$record->model = $dalle3model;
$record->model = 'dall-e-3';
$DB->insert_record('ai_action_register', $record);
// Record another image.
$record->actionid = 222;
@@ -134,24 +162,38 @@ final class registration_test extends \advanced_testcase {
$record->actionid = 6;
$record->errorcode = 404;
$DB->insert_record('ai_action_register', $record);
}
// Get our site info and check the expected calculations are correct.
$siteinfo = registration::get_site_info();
$aisuage = json_decode($siteinfo['aiusage']);
// Check generated text.
$this->assertEquals(1, $aisuage->openai->generate_text->success_count);
$this->assertEquals(0, $aisuage->openai->generate_text->fail_count);
// Check generated images.
$this->assertEquals(2, $aisuage->openai->generate_image->success_count);
$this->assertEquals(3, $aisuage->openai->generate_image->fail_count);
$this->assertEquals(15, $aisuage->openai->generate_image->average_time);
$this->assertEquals(403, $aisuage->openai->generate_image->predominant_error);
// Check time range is set correctly.
$this->assertEquals($clock->time() - WEEKSECS, $aisuage->time_range->timefrom);
$this->assertEquals($clock->time(), $aisuage->time_range->timeto);
// Check model counts.
$this->assertEquals(1, $aisuage->openai->generate_text->models->{$gpt4omodel}->count);
$this->assertEquals(2, $aisuage->openai->generate_image->models->{$dalle3model}->count);
$this->assertEquals(3, $aisuage->openai->generate_image->models->unknown->count);
/**
* Test the show AI usage data.
*/
public function test_show_ai_usage(): void {
$this->resetAfterTest();
// Init the registration class.
$registration = new registration();
// There should be no data to show yet.
$aisuagedata = $registration->show_ai_usage();
$this->assertTrue(empty($aisuagedata));
// After generating some data, there should now be some data to show.
$this->generate_ai_usage_data();
$aisuagedata = $registration->show_ai_usage();
$this->assertTrue(!empty($aisuagedata));
foreach ($aisuagedata['providers'] as $provider) {
$this->assertEquals('OpenAI API provider', $provider['providername']);
$this->assertTrue(!empty($provider['aiactions']));
foreach ($provider['aiactions'] as $action) {
$actionname = $action['actionname'];
$this->assertTrue(!empty($actionname));
}
}
$timerange = $aisuagedata['timerange'];
$this->assertEquals(get_string('time_range', 'hub'), $timerange['label']);
$this->assertTrue(!empty($timerange['values']));
}
}