Merge branch 'MDL-74468-400' of https://github.com/call-learning/moodle into MOODLE_400_STABLE
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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}.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)"
|
||||
+124
-24
@@ -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
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user