Merge branch 'MDL-74468-400' of https://github.com/call-learning/moodle into MOODLE_400_STABLE

This commit is contained in:
Andrew Nicols
2022-11-07 12:14:17 +08:00
15 changed files with 471 additions and 137 deletions
+4 -2
View File
@@ -28,8 +28,10 @@
// phpcs:disable moodle.Files.MoodleInternal.MoodleInternalGlobalState,moodle.Files.RequireLogin.Missing
require(__DIR__ . '/../../config.php');
use Firebase\JWT\Key;
use mod_bigbluebuttonbn\broker;
use mod_bigbluebuttonbn\instance;
use mod_bigbluebuttonbn\local\config;
use mod_bigbluebuttonbn\meeting;
global $PAGE, $USER, $CFG, $SESSION, $DB;
@@ -56,12 +58,12 @@ $PAGE->set_context($instance->get_context());
try {
switch (strtolower($action)) {
case 'recording_ready':
broker::recording_ready($instance, $params);
broker::process_recording_ready($instance, $params);
return;
case 'meeting_events':
// When meeting_events callback is implemented by BigBlueButton, Moodle receives a POST request
// which is processed in the function using super globals.
meeting::meeting_events($instance);
broker::process_meeting_events($instance);
return;
}
header("HTTP/1.0 400 Bad request. The action '{$action}' does not exist");
+64 -1
View File
@@ -84,7 +84,7 @@ class broker {
* @param instance $instance
* @param array $params
*/
public static function recording_ready(instance $instance, array $params): void {
public static function process_recording_ready(instance $instance, array $params): void {
// Decodes the received JWT string.
try {
$decodedparameters = JWT::decode(
@@ -127,4 +127,67 @@ class broker {
header('HTTP/1.0 503 Service Unavailable. ' . $error);
}
}
/**
* Process meeting events for instance with provided HTTP headers.
*
* @param instance $instance
* @return void
*/
public static function process_meeting_events(instance $instance) {
try {
// Get the HTTP headers.
$authorization = self::get_authorization_token();
// Pull the Bearer from the headers.
if (empty($authorization)) {
$msg = 'Authorization failed';
header('HTTP/1.0 400 Bad Request. ' . $msg);
return;
}
// Verify the authenticity of the request.
$token = \Firebase\JWT\JWT::decode(
$authorization[1],
new Key(config::get('shared_secret'), 'HS512')
);
// Get JSON string from the body.
$jsonstr = file_get_contents('php://input');
// Convert JSON string to a JSON object.
$jsonobj = json_decode($jsonstr);
$headermsg = meeting::meeting_events($instance, $jsonobj);
header($headermsg);
} catch (Exception $e) {
$msg = 'Caught exception: ' . $e->getMessage();
header('HTTP/1.0 400 Bad Request. ' . $msg);
}
}
/**
* Get authorisation token
*
* We could use getallheaders but this is only compatible with apache types of servers
* some explanations and examples here: https://www.php.net/manual/en/function.getallheaders.php#127190
*
* @return array|null an array composed of the Authorization token provided in the header.
*/
private static function get_authorization_token(): ?array {
$autorization = null;
if (isset($_SERVER['Authorization'])) {
$autorization = trim($_SERVER["Authorization"]);
} else if (isset($_SERVER['HTTP_AUTHORIZATION'])) {
$autorization = trim($_SERVER["HTTP_AUTHORIZATION"]);
} else if (function_exists('apache_request_headers')) {
$requestheaders = apache_request_headers();
$requestheaders = array_combine(array_map('ucwords',
array_keys($requestheaders)), array_values($requestheaders));
if (isset($requestheaders['Authorization'])) {
$autorization = trim($requestheaders['Authorization']);
}
}
return empty($autorization) ? null : explode(" ", $autorization);
}
}
@@ -32,6 +32,18 @@ use stdClass;
*/
class custom_completion extends activity_custom_completion {
/**
* Filters for logs
*/
const FILTERS = [
'completionattendance' => [logger::EVENT_SUMMARY],
'completionengagementchats' => [logger::EVENT_SUMMARY],
'completionengagementtalks' => [logger::EVENT_SUMMARY],
'completionengagementraisehand' => [logger::EVENT_SUMMARY],
'completionengagementpollvotes' => [logger::EVENT_SUMMARY],
'completionengagementemojis' => [logger::EVENT_SUMMARY],
];
/**
* Get current state
*
@@ -47,23 +59,27 @@ class custom_completion extends activity_custom_completion {
}
// Default return value.
$value = COMPLETION_INCOMPLETE;
$logs = logger::get_user_completion_logs($instance, $this->userid, [logger::EVENT_SUMMARY]);
$returnedvalue = COMPLETION_INCOMPLETE;
$filters = self::FILTERS[$rule] ?? [logger::EVENT_SUMMARY];
$logs = logger::get_user_completion_logs($instance, $this->userid, $filters);
if (method_exists($this, "get_{$rule}_value")) {
$valuecount = $this->count_actions($logs, self::class . "::get_{$rule}_value");
if ($valuecount) {
if (!is_null($instance->get_instance_var($rule))) {
if ($instance->get_instance_var($rule) <= $valuecount) {
$value = COMPLETION_COMPLETE;
$completionvalue = $this->aggregate_values($logs, self::class . "::get_{$rule}_value");
if ($completionvalue) {
// So in this case we check the value set in the module setting. If we go over the threshold, then
// this is complete.
$rulevalue = $instance->get_instance_var($rule);
if (!is_null($rulevalue)) {
if ($rulevalue <= $completionvalue) {
$returnedvalue = COMPLETION_COMPLETE;
}
} else {
// If there is at least a hit, we consider it as complete.
$value = $valuecount ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE;
$returnedvalue = $completionvalue ? COMPLETION_COMPLETE : COMPLETION_INCOMPLETE;
}
}
}
return $value;
return $returnedvalue;
}
/**
@@ -71,20 +87,20 @@ class custom_completion extends activity_custom_completion {
*
* @param array $logs
* @param callable $logvaluegetter
* @return int the number of hits on this particular rule
* @return int the sum of all values for this particular event (it can be a duration or a number of hits)
*/
protected function count_actions(array $logs, callable $logvaluegetter): int {
protected function aggregate_values(array $logs, callable $logvaluegetter): int {
if (empty($logs)) {
// As completion by engagement with $rulename hand was required, the activity hasn't been completed.
return 0;
}
$valuecount = 0;
$value = 0;
foreach ($logs as $log) {
$valuecount += $logvaluegetter($log);
$value += $logvaluegetter($log);
}
return $valuecount;
return $value;
}
/**
@@ -149,34 +165,47 @@ class custom_completion extends activity_custom_completion {
}
/**
* Get current state in a friendly version
* Get current states of completion in a human-friendly version
*
* @return string[]
*/
public function get_printable_states(): array {
$result = [];
foreach ($this->get_available_custom_rules() as $rule) {
$result[] = $this->get_printable_state($rule);
}
return $result;
}
/**
* Get current states of completion for a rule in a human-friendly version
*
* @param string $rule
* @return string
*/
public function get_printable_state(string $rule): string {
private function get_printable_state(string $rule): string {
// Get instance details.
$instance = instance::get_from_cmid($this->cm->id);
if (empty($instance)) {
throw new moodle_exception("Can't find bigbluebuttonbn instance {$this->cm->instance}");
}
$summary = "";
$logs = logger::get_user_completion_logs($instance, $this->userid, [logger::EVENT_SUMMARY]);
$filters = self::FILTERS[$rule] ?? [logger::EVENT_SUMMARY];
$logs = logger::get_user_completion_logs($instance, $this->userid, $filters);
if (method_exists($this, "get_{$rule}_value")) {
$summary = get_string(
$rule . '_event_desc',
'mod_bigbluebuttonbn',
$this->count_actions($logs, self::class . "::get_{$rule}_value")
$this->aggregate_values($logs, self::class . "::get_{$rule}_value")
);
}
return $summary;
}
/**
* Get current state in a friendly version
* Get current state in a friendly version
*
* @param string $rule
* @return string
@@ -188,7 +217,7 @@ class custom_completion extends activity_custom_completion {
if (empty($instance)) {
throw new moodle_exception("Can't find bigbluebuttonbn instance {$this->cm->instance}");
}
$filters = $rule != "completionview" ? [logger::EVENT_SUMMARY] : [logger::EVENT_JOIN, logger::EVENT_PLAYED];
$filters = self::FILTERS[$rule] ?? [logger::EVENT_SUMMARY];
return logger::get_user_completion_logs_max_timestamp($instance, $this->userid, $filters);
}
@@ -200,10 +229,7 @@ class custom_completion extends activity_custom_completion {
*/
protected static function get_completionattendance_value(stdClass $log): int {
$summary = json_decode($log->meta);
if ($summary && !empty($summary->data->duration)) {
return COMPLETION_COMPLETE;
}
return COMPLETION_INCOMPLETE;
return empty($summary->data->duration) ? 0 : $summary->data->duration / 60;
}
/**
@@ -243,7 +269,7 @@ class custom_completion extends activity_custom_completion {
* @return int
*/
protected static function get_completionengagementpollvotes_value(stdClass $log): int {
return self::get_completionengagement_value($log, 'pollvotes');
return self::get_completionengagement_value($log, 'poll_votes');
}
/**
@@ -265,9 +291,6 @@ class custom_completion extends activity_custom_completion {
*/
protected static function get_completionengagement_value(stdClass $log, string $type): int {
$summary = json_decode($log->meta);
if ($summary && !empty($summary->data->engagement->$type)) {
return COMPLETION_COMPLETE;
}
return COMPLETION_INCOMPLETE;
return intval($summary->data->engagement->$type ?? 0);
}
}
@@ -242,10 +242,12 @@ class bigbluebutton_proxy extends proxy_base {
$bbbcompletion = new custom_completion($cm, $userid);
if ($bbbcompletion->get_overall_completion_state()) {
mtrace("Completion succeeded for user $userid");
mtrace("Completion for userid $userid and bigbluebuttonid {$bigbluebuttonbn->id} updated.");
$completion->update_state($cm, COMPLETION_COMPLETE, $userid, true);
} else {
mtrace("Completion did not succeed for user $userid");
// Still update state to current value (prevent unwanted caching).
$completion->update_state($cm, COMPLETION_UNKNOWN, $userid);
mtrace("Activity not completed for userid $userid and bigbluebuttonid {$bigbluebuttonbn->id}.");
}
}
+27 -19
View File
@@ -157,7 +157,7 @@ EOF;
[$wheresql, $params] = static::get_user_completion_sql_params($instance, $userid, $filters, $timestart);
$select = "SELECT MAX(timecreated) ";
$lastlogtime = $DB->get_field_sql($select . ' FROM {bigbluebuttonbn_logs} WHERE ' . $wheresql, $params);
return $lastlogtime;
return $lastlogtime ?? 0;
}
/**
@@ -257,7 +257,7 @@ EOF;
json_encode($meta)
);
return self::count_callback_events($meta['recordid'], 'meeting_events');
return self::count_callback_events($meta['internalmeetingid'], 'meeting_events');
}
/**
@@ -441,28 +441,36 @@ EOF;
/**
* Helper function to count the number of callback logs matching the supplied specifications.
*
* @param string $recordid
* @param string $id
* @param string $callbacktype
* @return int
*/
protected static function count_callback_events(string $recordid, string $callbacktype = 'recording_ready'): int {
protected static function count_callback_events(string $id, string $callbacktype = 'recording_ready'): int {
global $DB;
$sql = 'SELECT count(DISTINCT id) FROM {bigbluebuttonbn_logs} WHERE log = ? AND meta LIKE ? AND meta LIKE ?';
// Callback type added on version 2.4, validate recording_ready first or assume it on records with no callback.
if ($callbacktype == 'recording_ready') {
$sql .= ' AND (meta LIKE ? OR meta NOT LIKE ? )';
$count =
$DB->count_records_sql($sql, [
self::EVENT_CALLBACK, '%recordid%',
"%$recordid%",
$callbacktype, 'callback'
]);
return $count;
// Look for a log record that is of "Callback" type and is related to the given event.
$conditions = [
"log = :logtype",
$DB->sql_like('meta', ':cbtypelike')
];
$params = [
'logtype' => self::EVENT_CALLBACK,
'cbtypelike' => "%meeting_events%" // All callbacks are meeting events, even recording events.
];
$basesql = 'SELECT COUNT(DISTINCT id) FROM {bigbluebuttonbn_logs}';
switch ($callbacktype) {
case 'recording_ready':
$conditions[] = $DB->sql_like('meta', ':isrecordid');
$params['isrecordid'] = '%recordid%'; // The recordid field in the meta field (json encoded).
break;
case 'meeting_events':
$conditions[] = $DB->sql_like('meta', ':idlike');
$params['idlike'] = "%$id%"; // The unique id of the meeting is the meta field (json encoded).
break;
}
$sql .= ' AND meta LIKE ?;';
$count = $DB->count_records_sql($sql,
[self::EVENT_CALLBACK, '%recordid%', "%$recordid%", "%$callbacktype%"]);
return $count;
$wheresql = join(' AND ', $conditions);
return $DB->count_records_sql($basesql . ' WHERE ' . $wheresql, $params);
}
/**
+11 -41
View File
@@ -454,61 +454,31 @@ class meeting {
* - Body: <A JSON Object>
*
* @param instance $instance
* @return void
* @param object $data
* @return string
*/
public static function meeting_events(instance $instance) {
public static function meeting_events(instance $instance, object $data): string {
$bigbluebuttonbn = $instance->get_instance_data();
// Decodes the received JWT string.
try {
// Get the HTTP headers (getallheaders is a PHP function that may only work with Apache).
$headers = getallheaders();
// Pull the Bearer from the headers.
if (!array_key_exists('Authorization', $headers)) {
$msg = 'Authorization failed';
header('HTTP/1.0 400 Bad Request. ' . $msg);
return;
}
$authorization = explode(" ", $headers['Authorization']);
// Verify the authenticity of the request.
$token = \Firebase\JWT\JWT::decode(
$authorization[1],
new Key(config::get('shared_secret'), 'HS512')
);
// Get JSON string from the body.
$jsonstr = file_get_contents('php://input');
// Convert JSON string to a JSON object.
$jsonobj = json_decode($jsonstr);
} catch (Exception $e) {
$msg = 'Caught exception: ' . $e->getMessage();
header('HTTP/1.0 400 Bad Request. ' . $msg);
return;
}
// Validate that the bigbluebuttonbn activity corresponds to the meeting_id received.
$meetingidelements = explode('[', $jsonobj->{'meeting_id'});
$meetingidelements = explode('[', $data->{'meeting_id'});
$meetingidelements = explode('-', $meetingidelements[0]);
if (!isset($bigbluebuttonbn) || $bigbluebuttonbn->meetingid != $meetingidelements[0]) {
$msg = 'The activity may have been deleted';
header('HTTP/1.0 410 Gone. ' . $msg);
return;
return 'HTTP/1.0 410 Gone. The activity may have been deleted';
}
// We make sure events are processed only once.
$overrides = ['meetingid' => $jsonobj->{'meeting_id'}];
$meta['recordid'] = $jsonobj->{'internal_meeting_id'};
$overrides = ['meetingid' => $data->{'meeting_id'}];
$meta['internalmeetingid'] = $data->{'internal_meeting_id'};
$meta['callback'] = 'meeting_events';
$meta['meetingid'] = $data->{'meeting_id'};
$eventcount = logger::log_event_callback($instance, $overrides, $meta);
if ($eventcount === 1) {
// Process the events.
self::process_meeting_events($instance, $jsonobj);
header('HTTP/1.0 200 Accepted. Enqueued.');
self::process_meeting_events($instance, $data);
return 'HTTP/1.0 200 Accepted. Enqueued.';
} else {
header('HTTP/1.0 202 Accepted. Already processed.');
return 'HTTP/1.0 202 Accepted. Already processed.';
}
}
@@ -92,28 +92,28 @@ $string['privacy:metadata:bigbluebuttonbn_recordings'] = 'Stores metadata about
$string['privacy:metadata:bigbluebuttonbn_recordings:userid'] = 'The user ID of the user who last changed a recording.';
$string['completionattendance'] = 'Student must attend the session for:';
$string['completionattendance_desc'] = 'Student must enter the room and remain in the session for at least {$a} minute(s)';
$string['completionattendance_desc'] = 'Enter and remain in the room for at least {$a} minute(s).';
$string['completionattendance_event_desc'] = 'Student has entered the room and remained in the session for at least {$a} minute(s)';
$string['completionattendancegroup'] = 'Require attendance';
$string['completionattendancegroup_help'] = 'Attending the meeting for (n) minutes is required for completion.';
$string['completionengagementchats'] = 'Chats';
$string['completionengagementchats_desc'] = 'Student must participate in {$a} chat(s) to complete it.';
$string['completionengagementchats_desc'] = 'Participate in {$a} chat(s).';
$string['completionengagementchats_event_desc'] = 'Has raised {$a} chat(s)';
$string['completionengagementtalks'] = 'Talk';
$string['completionengagementtalks_desc'] = 'Student must talk {$a} time(s) to complete it';
$string['completionengagementtalks_desc'] = 'Talk {$a} time(s)';
$string['completionengagementtalks_event_desc'] = 'Has raised {$a} talk(s)';
$string['completionengagementraisehand'] = 'Require raised hand';
$string['completionengagementraisehand_desc'] = 'Student must raise hand {$a} time(s) to complete it.';
$string['completionengagementraisehand_desc'] = 'Raise hand {$a} time(s).';
$string['completionengagementraisehand_event_desc'] = 'Has raised hand {$a} times';
$string['completionengagementpollvotes'] = 'Poll votes';
$string['completionengagementpollvotes_desc'] = 'Student must vote in polls {$a} time(s) to complete it.';
$string['completionengagementpollvotes_event_desc'] = 'Has raised {$a} poll vote(s)';
$string['completionengagementpollvotes_desc'] = 'Vote in polls {$a} time(s).';
$string['completionengagementpollvotes_event_desc'] = 'Has answered {$a} poll vote(s)';
$string['completionengagementemojis'] = 'Emojis';
$string['completionengagementemojis_desc'] = 'Student must send {$a} emoji(s) into polls to complete it.';
$string['completionengagementemojis_event_desc'] = 'Has raised {$a} emoji(s)';
$string['completionengagementemojis_desc'] = 'Change {$a} times his/her emoji(s).';
$string['completionengagementemojis_event_desc'] = 'Changed {$a} time his/her emoji(s)';
$string['completionengagement_desc'] = 'Student must engage in activities during the meeting';
$string['completionengagement_desc'] = 'Engage in activities during the meeting.';
$string['completionengagementgroup'] = 'Require participation';
$string['completionengagementgroup_help'] = 'Active participation during the session is required for completion.';
+12 -2
View File
@@ -337,7 +337,14 @@ function bigbluebuttonbn_get_coursemodule_info($coursemodule) {
global $DB;
$dbparams = ['id' => $coursemodule->instance];
$fields = 'id, name, intro, introformat, completionattendance';
$customcompletionfields = custom_completion::get_defined_custom_rules();
$fieldsarray = array_merge([
'id',
'name',
'intro',
'introformat',
], $customcompletionfields);
$fields = join(',', $fieldsarray);
$bigbluebuttonbn = $DB->get_record('bigbluebuttonbn', $dbparams, $fields);
if (!$bigbluebuttonbn) {
return null;
@@ -350,7 +357,10 @@ function bigbluebuttonbn_get_coursemodule_info($coursemodule) {
}
// Populate the custom completion rules as key => value pairs, but only if the completion mode is 'automatic'.
if ($coursemodule->completion == COMPLETION_TRACKING_AUTOMATIC) {
$info->customdata['customcompletionrules']['completionattendance'] = $bigbluebuttonbn->completionattendance;
foreach ($customcompletionfields as $completiontype) {
$info->customdata['customcompletionrules'][$completiontype] =
$bigbluebuttonbn->$completiontype ?? 0;
}
}
return $info;
+2 -2
View File
@@ -212,7 +212,7 @@ class mod_bigbluebuttonbn_mod_form extends moodleform_mod {
$mform->setType('completionattendance', PARAM_INT);
$mform->addGroup($attendance['group'], 'completionattendancegroup', $attendance['grouplabel'], [' '], false);
$mform->addHelpButton('completionattendancegroup', 'completionattendancegroup', 'bigbluebuttonbn');
$mform->disabledIf('completionattendancegroup', 'completionview', 'notchecked');
$mform->disabledIf('completionattendancegroup', 'completion', 'neq', COMPLETION_AGGREGATION_ANY);
$mform->disabledIf('completionattendance', 'completionattendanceenabled', 'notchecked');
// Elements for completion by Engagement.
@@ -236,7 +236,7 @@ class mod_bigbluebuttonbn_mod_form extends moodleform_mod {
]
]);
$mform->addHelpButton('completionengagementgroup', 'completionengagementgroup', 'bigbluebuttonbn');
$mform->disabledIf('completionengagementgroup', 'completionview', 'notchecked');
$mform->disabledIf('completionengagementgroup', 'completion', 'neq', COMPLETION_AGGREGATION_ANY);
return ['completionattendancegroup', 'completionengagementgroup'];
}
@@ -26,6 +26,7 @@
require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
use Behat\Gherkin\Node\TableNode;
use Moodle\BehatExtension\Exception\SkippedException;
/**
@@ -171,10 +172,56 @@ XPATH
* @Given the BigBlueButtonBN server has sent recording ready notifications
*/
public function trigger_recording_ready_notification(): void {
$this->send_mock_request('backoffice/sendNotifications', [
$this->send_mock_request('backoffice/sendRecordingReadyNotifications', [
'secret' => \mod_bigbluebuttonbn\local\config::DEFAULT_SHARED_SECRET,
]
);
}
/**
* Trigger a meeting event on BBB side
*
* @Given /^the BigBlueButtonBN server has received the following events from user "(?P<element_string>(?:[^"]|\\")*)":$/
* @param string $username
* @param TableNode $data
*/
public function trigger_meeting_event(string $username, TableNode $data): void {
global $DB;
$user = core_user::get_user_by_username($username);
$rows = $data->getHash();
foreach ($rows as $elementdata) {
$instanceid = $DB->get_field('bigbluebuttonbn', 'id', [
'name' => $elementdata['instancename'],
]);
$instance = \mod_bigbluebuttonbn\instance::get_from_instanceid($instanceid);
$this->send_mock_request('backoffice/addMeetingEvent', [
'secret' => \mod_bigbluebuttonbn\local\config::DEFAULT_SHARED_SECRET,
'meetingID' => $instance->get_meeting_id(),
'attendeeID' => $user->id,
'attendeeName' => fullname($user),
'eventType' => $elementdata['eventtype'],
'eventData' => $elementdata['eventdata'] ?? '',
]
);
}
}
/**
* Send all events received for this meeting back to moodle
*
* @Given /^the BigBlueButtonBN activity "(?P<element_string>(?:[^"]|\\")*)" has sent recording all its events$/
* @param string $instancename
*/
public function trigger_all_events(string $instancename): void {
global $DB;
$instanceid = $DB->get_field('bigbluebuttonbn', 'id', [
'name' => $instancename,
]);
$instance = \mod_bigbluebuttonbn\instance::get_from_instanceid($instanceid);
$this->send_mock_request('backoffice/sendAllEvents', [
'meetingID' => $instance->get_meeting_id(),
'sendQuery' => true
]
);
}
}
@@ -0,0 +1,63 @@
@mod @mod_bigbluebuttonbn
Feature: As a user I can complete a BigblueButtonBN activity by usual or custom criteria
Background: Make sure that a course is created
Given a BigBlueButton mock server is configured
And I enable "bigbluebuttonbn" "mod" plugin
And the following "courses" exist:
| fullname | shortname | category | enablecompletion |
| Test course | C1 | 0 | 1 |
And the following "activities" exist:
| activity | name | intro | course | idnumber | type | recordings_imported |
| bigbluebuttonbn | RoomRecordings | Test Room Recording description | C1 | bigbluebuttonbn1 | 0 | 0 |
And the following "users" exist:
| username | firstname | lastname | email |
| traverst | Terry | Travers | t.travers@example.com |
And the following "course enrolments" exist:
| user | course | role |
| traverst | C1 | student |
Scenario: I set the completion to standard type of completion.
Given I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin
And I click on "Settings" "link"
And I expand all fieldsets
And I set the following fields to these values:
| Completion tracking | Show activity as complete when conditions are met |
| Require view | 1 |
And I press "Save and display"
And I log out
Given I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as traverst
Then I should see "Done: View"
@javascript
Scenario: I set the completion type to custom completion
Given the following config values are set as admin:
| bigbluebuttonbn_meetingevents_enabled | 1 |
And I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as admin
And I click on "Settings" "link"
And I expand all fieldsets
And I set the following fields to these values:
| Completion tracking | Show activity as complete when conditions are met |
| Chats | 1 |
And I press "Save and display"
# We start the meeting here so to make sure that meta_analytics-callback-url is set.
And the following "mod_bigbluebuttonbn > meeting" exists:
| activity | RoomRecordings |
And I log out
Given I am on the "RoomRecordings" "bigbluebuttonbn activity" page logged in as traverst
When I click on "Join session" "link"
And I switch to "bigbluebutton_conference" window
And I wait until the page is ready
Then I follow "End Meeting"
And the BigBlueButtonBN server has received the following events from user "traverst":
| instancename | eventtype | eventdata |
| RoomRecordings | chats | 1 |
# Selenium driver does not like the click action to be done before we
# automatically close the window so we need to make sure that the window
# is closed before.
And I close all opened windows
And I switch to the main window
Given the BigBlueButtonBN activity "RoomRecordings" has sent recording all its events
And I run all adhoc tasks
And I reload the page
Then I should see "Done: Participate in 1 chat(s)"
@@ -14,11 +14,14 @@
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace mod_bigbluebuttonbn;
namespace mod_bigbluebuttonbn\completion;
use completion_info;
use context_module;
use mod_bigbluebuttonbn\completion\custom_completion;
use mod_bigbluebuttonbn\instance;
use mod_bigbluebuttonbn\local\config;
use mod_bigbluebuttonbn\logger;
use mod_bigbluebuttonbn\meeting;
use mod_bigbluebuttonbn\test\testcase_helper_trait;
/**
@@ -38,11 +41,12 @@ class completion_test extends \advanced_testcase {
*/
public function setUp(): void {
parent::setUp();
$this->initialise_mock_server();
set_config('enablecompletion', true); // Enable completion for all tests.
}
/**
* Completion with no rules
* Completion with no rules: the completion is completed as soons as we view the course.
*/
public function test_get_completion_state_no_rules() {
$this->resetAfterTest();
@@ -103,7 +107,15 @@ class completion_test extends \advanced_testcase {
public function test_get_completion_state_complete() {
$this->resetAfterTest();
list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance();
list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(
$this->get_course(),
[
'completion' => '2',
'completionengagementtalks' => 2,
'completionengagementchats' => 2,
'completionattendance' => 15
]
);
$instance = instance::get_from_instanceid($bbactivity->id);
$user = $this->getDataGenerator()->create_user();
@@ -114,18 +126,24 @@ class completion_test extends \advanced_testcase {
$meta = [
'origin' => 0,
'data' => [
'duration' => 120,
'duration' => 300, // 300 seconds, i.e 5 mins.
'engagement' => [
'chats' => 2,
'talks' => 2,
],
],
];
logger::log_meeting_joined_event($instance, 0);
logger::log_event_summary($instance, $overrides, $meta);
logger::log_event_summary($instance, $overrides, $meta);
// Now 2 x 120 mins of duration.
// We setup a couple of logs as per engagement and duration.
logger::log_event_summary($instance, $overrides, $meta);
logger::log_event_summary($instance, $overrides, $meta);
$completion = new custom_completion($bbactivitycm, $user->id);
$result = $completion->get_overall_completion_state();
$this->assertEquals(COMPLETION_INCOMPLETE, $result);
// Now we have 15 mins.
logger::log_event_summary($instance, $overrides, $meta);
// Now that the meeting was joined, it should be complete.
$completion = new custom_completion($bbactivitycm, $user->id);
$result = $completion->get_overall_completion_state();
$this->assertEquals(COMPLETION_COMPLETE, $result);
@@ -141,21 +159,15 @@ class completion_test extends \advanced_testcase {
// Two activities, both with automatic completion. One has the 'completionsubmit' rule, one doesn't.
// Inspired from the same test in forum.
list($bbactivitycontext, $cm1, $bbactivity) = $this->create_instance($this->get_course(),
['completion' => '2', 'completionattendance' => '1']);
['completion' => '2']);
$cm1->override_customdata('customcompletionrules', [
'completionattendance' => '1'
]);
list($bbactivitycontext, $cm2, $bbactivity) = $this->create_instance($this->get_course(),
['completion' => '2', 'completionattendance' => '0']);
// Data for the stdClass input type.
// This type of input would occur when checking the default completion rules for an activity type, where we don't have
// any access to cm_info, rather the input is a stdClass containing completion and customdata attributes, just like cm_info.
$moddefaults = (object) [
'customdata' => [
'customcompletionrules' => [
'completionsubmit' => '1',
],
],
'completion' => 2,
];
['completion' => '2']);
$cm2->override_customdata('customcompletionrules', [
'completionattendance' => '0'
]);
$completioncm1 = new custom_completion($cm1, $user->id);
// TODO: check the return value here as there might be an issue with the function compared to the forum for example.
@@ -194,6 +206,12 @@ class completion_test extends \advanced_testcase {
// Trigger and capture the event.
$sink = $this->redirectEvents();
// Check completion before viewing.
$completion = new completion_info($this->get_course());
$completiondata = $completion->get_data($bbactivitycm);
$this->assertEquals(0, $completiondata->viewed);
$this->assertEquals(COMPLETION_NOT_VIEWED, $completiondata->completionstate);
bigbluebuttonbn_view($bbactivity, $this->get_course(), $bbactivitycm, context_module::instance($bbactivitycm->id));
$events = $sink->get_events();
@@ -213,7 +231,89 @@ class completion_test extends \advanced_testcase {
$completion = new completion_info($this->get_course());
$completiondata = $completion->get_data($bbactivitycm);
$this->assertEquals(1, $completiondata->viewed);
// A view means COMPLETE.
$this->assertEquals(COMPLETION_COMPLETE, $completiondata->completionstate);
}
/**
* Completion with no rules and join meeting
*
* @param array $customcompletionrules
* @param array $events
* @param int $expectedstate
* @dataProvider custom_completion_data_provider
*/
public function test_get_completion_with_events(array $customcompletionrules, array $events, int $expectedstate) {
$this->resetAfterTest();
list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance(
$this->get_course(),
[
'completion' => '2',
]
);
$bbactivitycm->override_customdata('customcompletionrules', $customcompletionrules);
$plugingenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn');
$user = $this->getDataGenerator()->create_user();
$this->setUser($user);
// Now create a couple of events.
$instance = instance::get_from_instanceid($bbactivity->id);
set_config('bigbluebuttonbn_meetingevents_enabled', true);
$meeting = $plugingenerator->create_meeting([
'instanceid' => $instance->get_instance_id(),
'groupid' => $instance->get_group_id(),
'participants' => json_encode([$user->id])
]);
foreach ($events as $edesc) {
$plugingenerator->add_meeting_event($user, $instance, $edesc->name, $edesc->data ?? '');
}
$result = $plugingenerator->send_all_events($instance);
$this->assertNotEmpty($result->data);
$data = json_decode(json_encode($result->data));
meeting::meeting_events($instance, $data);
$completion = new custom_completion($bbactivitycm, $user->id);
$result = $completion->get_overall_completion_state();
$this->assertEquals($expectedstate, $result);
}
/**
* Data generator
*
* @return array[]
*/
public function custom_completion_data_provider() {
return [
'simple' => [
'customcompletionrules' => [
'completionengagementtalks' => 1,
'completionengagementchats' => 1,
],
'events' => [
(object) ['name' => 'talks'],
(object) ['name' => 'chats']
],
'expectedstate' => COMPLETION_COMPLETE
],
'not right events' => [
'customcompletionrules' => [
'completionengagementchats' => 1,
],
'events' => [
(object) ['name' => 'talks']
],
'expectedstate' => COMPLETION_INCOMPLETE
],
'attendance' => [
'customcompletionrules' => [
'completionattendance' => 1,
],
'events' => [
(object) ['name' => 'talks'],
(object) ['name' => 'attendance', 'data' => '70']
],
'expectedstate' => COMPLETION_COMPLETE
]
];
}
}
+3 -2
View File
@@ -215,7 +215,8 @@ class get_join_url_test extends \externallib_advanced_testcase {
$course = $generator->create_course();
$record = $generator->create_module('bigbluebuttonbn', ['course' => $course->id, 'userlimit' => 2]);
$user = $generator->create_and_enrol($course, 'student');
$user1 = $generator->create_and_enrol($course, 'student');
$user2 = $generator->create_and_enrol($course, 'student');
$instance = instance::get_from_instanceid($record->id);
$bbbgenerator = $this->getDataGenerator()->get_plugin_generator('mod_bigbluebuttonbn');
@@ -225,7 +226,7 @@ class get_join_url_test extends \externallib_advanced_testcase {
'groupid' => $instance->get_group_id(),
'participants' => 2
]);
$this->setUser($user);
$this->setUser($user1);
$joinurl = $this->get_join_url($instance->get_cm_id());
$this->assertNotNull($joinurl['warnings']);
$this->assertEquals('userlimitreached', $joinurl['warnings'][0]['warningcode']);
+45 -2
View File
@@ -26,6 +26,7 @@
use core\plugininfo\mod;
use mod_bigbluebuttonbn\instance;
use mod_bigbluebuttonbn\local\config;
use mod_bigbluebuttonbn\logger;
use mod_bigbluebuttonbn\recording;
@@ -304,6 +305,12 @@ class mod_bigbluebuttonbn_generator extends \testing_module_generator {
'bbb-recording-name' => $instance->get_meeting_name(),
],
]);
if ((boolean) config::get('recordingready_enabled')) {
$roomconfig['meta']['bn-recording-ready-url'] = $instance->get_record_ready_url()->out(false);
}
if ((boolean) config::get('meetingevents_enabled')) {
$roomconfig['meta']['analytics-callback-url'] = $instance->get_meeting_event_notification_url()->out(false);
}
if (!empty($roomconfig['isBreakout'])) {
// If it is a breakout meeting, we do not have any way to know the real Id of the meeting
// For now we will just send the parent ID and let the mock server deal with the sequence + parentID
@@ -312,9 +319,7 @@ class mod_bigbluebuttonbn_generator extends \testing_module_generator {
} else {
$roomconfig['meetingID'] = $meetingid;
}
$this->send_mock_request('backoffice/createMeeting', [], $roomconfig);
return (object) $roomconfig;
}
@@ -394,6 +399,44 @@ class mod_bigbluebuttonbn_generator extends \testing_module_generator {
return $retvalue;
}
/**
* Trigger a meeting event on BBB side
*
* @param object $user
* @param instance $instance
* @param string $eventtype
* @param string|null $eventdata
* @return void
*/
public function add_meeting_event(object $user, instance $instance, string $eventtype, string $eventdata = ''): void {
$this->send_mock_request('backoffice/addMeetingEvent', [
'secret' => \mod_bigbluebuttonbn\local\config::DEFAULT_SHARED_SECRET,
'meetingID' => $instance->get_meeting_id(),
'attendeeID' => $user->id,
'attendeeName' => fullname($user),
'eventType' => $eventtype,
'eventData' => $eventdata
]
);
}
/**
* Send all previously store events
*
* @param instance $instance
* @return object|null
*/
public function send_all_events(instance $instance): ?object {
if (defined('TEST_MOD_BIGBLUEBUTTONBN_MOCK_SERVER')) {
return $this->send_mock_request('backoffice/sendAllEvents', [
'meetingID' => $instance->get_meeting_id(),
'sendQuery' => false, // We get the result directly here.
'secret' => \mod_bigbluebuttonbn\local\config::DEFAULT_SHARED_SECRET,
]);
}
return null;
}
/**
* Reset the mock server
*/
+2
View File
@@ -397,6 +397,7 @@ class lib_test extends \advanced_testcase {
list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance();
$this->getDataGenerator()->enrol_user($user->id, $this->course->id);
$this->setUser($user);
logger::log_meeting_joined_event(instance::get_from_instanceid($bbactivity->id), 0);
$data->courseid = $this->get_course()->id;
@@ -462,6 +463,7 @@ class lib_test extends \advanced_testcase {
list($bbactivitycontext, $bbactivitycm, $bbactivity) = $this->create_instance();
$this->getDataGenerator()->enrol_user($user->id, $this->course->id);
$this->setUser($user);
logger::log_meeting_joined_event(instance::get_from_instanceid($bbactivity->id), 0);
$data->courseid = $this->get_course()->id;