Merge branch 'MDL-60819-master' of https://github.com/snake/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 12 Jun 2018 00:58:49 +0000 (08:58 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 12 Jun 2018 00:58:49 +0000 (08:58 +0800)
14 files changed:
admin/tool/dataprivacy/classes/task/initiate_data_request_task.php
admin/tool/dataprivacy/classes/task/process_data_request_task.php
admin/tool/dataprivacy/createdatarequest_form.php
backup/util/ui/backup_ui_stage.class.php
backup/util/ui/base_moodleform.class.php
backup/util/ui/renderer.php
course/classes/management_renderer.php
lang/en/backup.php
lib/phpunit/classes/hint_resultprinter.php
mod/chat/lib.php
mod/chat/locallib.php
mod/chat/tests/externallib_test.php
mod/chat/tests/lib_test.php
question/type/multianswer/renderer.php

index 2ee4a0a..70402f4 100644 (file)
@@ -54,7 +54,7 @@ class initiate_data_request_task extends adhoc_task {
     public function execute() {
         global $CFG;
 
-        require_once($CFG->dirroot . '/admin/tool/dataprivacy/lib.php');
+        require_once($CFG->dirroot . "/{$CFG->admin}/tool/dataprivacy/lib.php");
 
         if (!isset($this->get_custom_data()->requestid)) {
             throw new coding_exception('The custom data \'requestid\' is required.');
index 6a93217..c58f574 100644 (file)
@@ -57,7 +57,7 @@ class process_data_request_task extends adhoc_task {
     public function execute() {
         global $CFG, $PAGE, $SITE;
 
-        require_once($CFG->dirroot . '/admin/tool/dataprivacy/lib.php');
+        require_once($CFG->dirroot . "/{$CFG->admin}/tool/dataprivacy/lib.php");
 
         if (!isset($this->get_custom_data()->requestid)) {
             throw new coding_exception('The custom data \'requestid\' is required.');
index 680b93a..4c94a8a 100644 (file)
@@ -44,19 +44,28 @@ class tool_dataprivacy_data_request_form extends moodleform {
     /**
      * Form definition.
      *
-     * @throws HTML_QuickForm_Error
      * @throws coding_exception
-     * @throws dml_exception
      */
     public function definition() {
-        global $DB, $USER;
+        global $USER;
         $mform =& $this->_form;
 
         $this->manage = $this->_customdata['manage'];
         if ($this->manage) {
             $options = [
                 'ajax' => 'tool_dataprivacy/form-user-selector',
-                'multiple' => false
+                'valuehtmlcallback' => function($value) {
+                    global $OUTPUT;
+
+                    $allusernames = get_all_user_name_fields(true);
+                    $fields = 'id, email, ' . $allusernames;
+                    $user = \core_user::get_user($value, $fields);
+                    $useroptiondata = [
+                        'fullname' => fullname($user),
+                        'email' => $user->email
+                    ];
+                    return $OUTPUT->render_from_template('tool_dataprivacy/form-user-selector-suggestion', $useroptiondata);
+                }
             ];
             $mform->addElement('autocomplete', 'userid', get_string('requestfor', 'tool_dataprivacy'), [], $options);
             $mform->addRule('userid', null, 'required', null, 'client');
index ec1b3b1..1e1caf8 100644 (file)
@@ -183,7 +183,11 @@ class backup_ui_stage_initial extends backup_ui_stage {
             foreach ($tasks as &$task) {
                 // For the initial stage we are only interested in the root settings.
                 if ($task instanceof backup_root_task) {
-                    $form->add_heading('rootsettings', get_string('rootsettings', 'backup'));
+                    if ($this->ui instanceof import_ui) {
+                        $form->add_heading('rootsettings', get_string('importrootsettings', 'backup'));
+                    } else {
+                        $form->add_heading('rootsettings', get_string('rootsettings', 'backup'));
+                    }
                     $settings = $task->get_settings();
                     // First add all settings except the filename setting.
                     foreach ($settings as &$setting) {
@@ -469,7 +473,11 @@ class backup_ui_stage_confirmation extends backup_ui_stage {
             foreach ($tasks as $task) {
                 if ($task instanceof backup_root_task) {
                     // If its a backup root add a root settings heading to group nicely.
-                    $form->add_heading('rootsettings', get_string('rootsettings', 'backup'));
+                    if ($this->ui instanceof import_ui) {
+                        $form->add_heading('rootsettings', get_string('importrootsettings', 'backup'));
+                    } else {
+                        $form->add_heading('rootsettings', get_string('rootsettings', 'backup'));
+                    }
                 } else if (!$courseheading) {
                     // We haven't already add a course heading.
                     $form->add_heading('coursesettings', get_string('includeditems', 'backup'));
index 4685a66..32ac8fb 100644 (file)
@@ -383,7 +383,13 @@ abstract class base_moodleform extends moodleform {
         $this->require_definition_after_data();
 
         $config = new stdClass;
-        $config->title = get_string('confirmcancel', 'backup');
+        if ($this->uistage->get_ui() instanceof import_ui) {
+            $config->title = get_string('confirmcancelimport', 'backup');
+        } else if ($this->uistage->get_ui() instanceof restore_ui) {
+            $config->title = get_string('confirmcancelrestore', 'backup');
+        } else {
+            $config->title = get_string('confirmcancel', 'backup');
+        }
         $config->question = get_string('confirmcancelquestion', 'backup');
         $config->yesLabel = get_string('confirmcancelyes', 'backup');
         $config->noLabel = get_string('confirmcancelno', 'backup');
index f656577..23462cf 100644 (file)
@@ -125,7 +125,13 @@ class core_backup_renderer extends plugin_renderer_base {
         $html .= html_writer::end_tag('div');
 
         $html .= html_writer::start_tag('div', array('class' => 'backup-section settings-section'));
-        $html .= $this->output->heading(get_string('backupsettings', 'backup'), 2, array('class' => 'header'));
+        if ($this instanceof import_ui_stage_inital) {
+            $html .= $this->output->heading(get_string('importrootsettings', 'backup'), 2, array('class' => 'header'));
+        } else if ($this instanceof restore_ui_stage_settings) {
+            $html .= $this->output->heading(get_string('restorerootsettings', 'backup'), 2, array('class' => 'header'));
+        } else {
+            $html .= $this->output->heading(get_string('backupsettings', 'backup'), 2, array('class' => 'header'));
+        }
         foreach ($details->root_settings as $label => $value) {
             if ($label == 'filename' or $label == 'user_files') {
                 continue;
index f66cd8c..4390d5a 100644 (file)
@@ -139,7 +139,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         $listing = coursecat::get(0)->get_children();
 
         $attributes = array(
-            'class' => 'ml-1',
+            'class' => 'ml',
             'role' => 'tree',
             'aria-labelledby' => 'category-listing-title'
         );
@@ -530,7 +530,7 @@ class core_course_management_renderer extends plugin_renderer_base {
             array('id' => 'course-listing-title', 'tabindex' => '0'));
         $html .= $this->course_listing_actions($category, $course, $perpage);
         $html .= $this->listing_pagination($category, $page, $perpage, false, $viewmode);
-        $html .= html_writer::start_tag('ul', array('class' => 'ml-1 course-list', 'role' => 'group'));
+        $html .= html_writer::start_tag('ul', array('class' => 'ml course-list', 'role' => 'group'));
         foreach ($category->get_courses($options) as $listitem) {
             $html .= $this->course_listitem($category, $listitem, $courseid);
         }
@@ -1117,7 +1117,7 @@ class core_course_management_renderer extends plugin_renderer_base {
         ));
         $html .= html_writer::tag('h3', get_string('courses'));
         $html .= $this->search_pagination($totalcourses, $page, $perpage);
-        $html .= html_writer::start_tag('ul', array('class' => 'ml-1'));
+        $html .= html_writer::start_tag('ul', array('class' => 'ml'));
         foreach ($courses as $listitem) {
             $i++;
             if ($i == $totalcourses) {
index e9f514b..dc711e8 100644 (file)
@@ -120,6 +120,8 @@ $string['configrestoreroleassignments'] = 'If enabled by default roles assignmen
 $string['configrestoreuserscompletion'] = 'If enabled user completion information will be restored by default if it was included in the backup.';
 $string['configrestoreusers'] = 'Sets the default for whether to restore users if they were included in the backup.';
 $string['confirmcancel'] = 'Cancel backup';
+$string['confirmcancelrestore'] = 'Cancel restore';
+$string['confirmcancelimport'] = 'Cancel import';
 $string['confirmcancelquestion'] = 'Are you sure you wish to cancel?
 Any information you have entered will be lost.';
 $string['confirmcancelyes'] = 'Cancel';
@@ -198,6 +200,7 @@ $string['importcurrentstage2'] = 'Schema settings';
 $string['importcurrentstage4'] = 'Confirmation and review';
 $string['importcurrentstage8'] = 'Perform import';
 $string['importcurrentstage16'] = 'Complete';
+$string['importrootsettings'] = 'Import settings';
 $string['importsettings'] = 'General import settings';
 $string['importsuccess'] = 'Import complete. Click continue to return to the course.';
 $string['includeactivities'] = 'Include:';
index a21ca51..fb10e5a 100644 (file)
@@ -116,7 +116,7 @@ class Hint_ResultPrinter extends PHPUnit\TextUI\ResultPrinter {
             }
         }
 
-        $this->write("\nTo re-run:\n $executable $testName $file\n");
+        $this->write("\nTo re-run:\n $executable \"$testName\" $file\n");
     }
 }
 
index 20d6de0..7e2aaad 100644 (file)
@@ -29,6 +29,9 @@ require_once($CFG->dirroot.'/calendar/lib.php');
 // Event types.
 define('CHAT_EVENT_TYPE_CHATTIME', 'chattime');
 
+// Gap between sessions. 5 minutes or more of idleness between messages in a chat means the messages belong in different sessions.
+define('CHAT_SESSION_GAP', 300);
+
 // The HTML head for the message window to start with (<!-- nix --> is used to get some browsers starting with output.
 global $CHAT_HTMLHEAD;
 $CHAT_HTMLHEAD = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\"><html><head></head>\n<body>\n\n".padding(200);
@@ -1486,58 +1489,63 @@ function mod_chat_core_calendar_provide_event_action(calendar_event $event,
 /**
  * Given a set of messages for a chat, return the completed chat sessions (including optionally not completed ones).
  *
- * @param  array $messages list of messages from a chat
+ * @param  array $messages list of messages from a chat. It is assumed that these are sorted by timestamp in DESCENDING order.
  * @param  bool $showall   whether to include incomplete sessions or not
  * @return array           the list of sessions
- * @since  Moodle 3.4
+ * @since  Moodle 3.5
  */
 function chat_get_sessions($messages, $showall = false) {
-    $sessions     = array();
-    $sessiongap   = 5 * 60;    // 5 minutes silence means a new session.
-    $sessionend   = 0;
-    $sessionstart = 0;
-    $sessionusers = array();
-    $lasttime     = 0;
-
-    $messagesleft = count($messages);
-
-    foreach ($messages as $message) {  // We are walking BACKWARDS through the messages.
-
-        $messagesleft --;              // Countdown.
+    $sessions     = [];
+    $start        = 0;
+    $end          = 0;
+    $sessiontimes = [];
+
+    // Group messages by session times.
+    foreach ($messages as $message) {
+        // Initialise values start-end times if necessary.
+        if (empty($start)) {
+            $start = $message->timestamp;
+        }
+        if (empty($end)) {
+            $end = $message->timestamp;
+        }
 
-        if (!$lasttime) {
-            $lasttime = $message->timestamp;
+        // If this message's timestamp has been more than the gap, it means it's been idle.
+        if ($start - $message->timestamp > CHAT_SESSION_GAP) {
+            // Mark this as the session end of the next session.
+            $end = $message->timestamp;
         }
-        if (!$sessionend) {
-            $sessionend = $message->timestamp;
+        // Use this time as the session's start (until it gets overwritten on the next iteration, if needed).
+        $start = $message->timestamp;
+
+        // Set this start-end pair in our list of session times.
+        $sessiontimes[$end]['sessionstart'] = $start;
+        if (!isset($sessiontimes[$end]['sessionend'])) {
+            $sessiontimes[$end]['sessionend'] = $end;
         }
-        if ((($lasttime - $message->timestamp) < $sessiongap) and $messagesleft) {  // Same session.
-            if ($message->userid and !$message->issystem) {       // Remember user and count messages.
-                if (empty($sessionusers[$message->userid])) {
-                    $sessionusers[$message->userid] = 1;
-                } else {
-                    $sessionusers[$message->userid] ++;
-                }
-            }
-        } else {
-            $sessionstart = $lasttime;
-
-            $iscomplete = ($sessionend - $sessionstart > 60 and count($sessionusers) > 1);
-            if ($showall or $iscomplete) {
-                $sessions[] = (object) array(
-                    'sessionstart' => $sessionstart,
-                    'sessionend' => $sessionend,
-                    'sessionusers' => $sessionusers,
-                    'iscomplete' => $iscomplete,
-                );
+        if ($message->userid && !$message->issystem) {
+            if (!isset($sessiontimes[$end]['sessionusers'][$message->userid])) {
+                $sessiontimes[$end]['sessionusers'][$message->userid] = 1;
+            } else {
+                $sessiontimes[$end]['sessionusers'][$message->userid]++;
             }
+        }
+    }
+
+    // Go through each session time and prepare the session data to be returned.
+    foreach ($sessiontimes as $sessionend => $sessiondata) {
+        if (!isset($sessiondata['sessionusers'])) {
+            $sessiondata['sessionusers'] = [];
+        }
+        $sessionusers = $sessiondata['sessionusers'];
+        $sessionstart = $sessiondata['sessionstart'];
 
-            $sessionend = $message->timestamp;
-            $sessionusers = array();
-            $sessionusers[$message->userid] = 1;
+        $iscomplete = $sessionend - $sessionstart > 60 && count($sessionusers) > 1;
+        if ($showall || $iscomplete) {
+            $sessions[] = (object) ($sessiondata + ['iscomplete' => $iscomplete]);
         }
-        $lasttime = $message->timestamp;
     }
+
     return $sessions;
 }
 
@@ -1550,7 +1558,7 @@ function chat_get_sessions($messages, $showall = false) {
  * @param  int $end         the session end timestamp (0 to not filter by time)
  * @param  string $sort     an order to sort the results in (optional, a valid SQL ORDER BY parameter)
  * @return array session messages
- * @since  Moodle 3.4
+ * @since  Moodle 3.5
  */
 function chat_get_session_messages($chatid, $group = false, $start = 0, $end = 0, $sort = '') {
     global $DB;
index 98e1ab0..06635fb 100644 (file)
@@ -116,14 +116,13 @@ class chat_portfolio_caller extends portfolio_module_caller_base {
     public function prepare_package() {
         $content = '';
         $lasttime = 0;
-        $sessiongap = 5 * 60;    // 5 minutes silence means a new session
         foreach ($this->messages as $message) {  // We are walking FORWARDS through messages
             $m = clone $message; // grrrrrr - this causes the sha1 to change as chat_format_message changes what it's passed.
             $formatmessage = chat_format_message($m, $this->cm->course, $this->user);
             if (!isset($formatmessage->html)) {
                 continue;
             }
-            if (empty($lasttime) || (($message->timestamp - $lasttime) > $sessiongap)) {
+            if (empty($lasttime) || (($message->timestamp - $lasttime) > CHAT_SESSION_GAP)) {
                 $content .= '<hr />';
                 $content .= userdate($message->timestamp);
             }
index 2443f71..050753b 100644 (file)
@@ -428,7 +428,8 @@ class mod_chat_external_testcase extends externallib_advanced_testcase {
         $result = external_api::clean_returnvalue(mod_chat_external::get_sessions_returns(), $result);
         $this->assertCount(1, $result['sessions']); // One session.
         $this->assertTrue($result['sessions'][0]['iscomplete']); // Session complete.
-        $this->assertEquals($timenow - HOURSECS + 70, $result['sessions'][0]['sessionstart']);  // First not system message time.
+        // The session started when user1 entered the chat.
+        $this->assertEquals($timenow - HOURSECS, $result['sessions'][0]['sessionstart']);
         $this->assertEmpty($result['warnings']);
     }
 
index 4edef1a..b233af1 100644 (file)
@@ -143,6 +143,200 @@ class mod_chat_lib_testcase extends advanced_testcase {
         $this->assertFalse($actionevent->is_actionable());
     }
 
+    /**
+     * Test for chat_get_sessions().
+     */
+    public function test_chat_get_sessions() {
+        global $DB;
+
+        $this->resetAfterTest();
+
+        $generator = $this->getDataGenerator();
+
+        // Setup test data.
+        $this->setAdminUser();
+        $course = $generator->create_course();
+        $chat = $generator->create_module('chat', ['course' => $course->id]);
+
+        $user1 = $generator->create_user();
+        $user2 = $generator->create_user();
+        $studentrole = $DB->get_record('role', ['shortname' => 'student']);
+        $generator->enrol_user($user1->id, $course->id, $studentrole->id);
+        $generator->enrol_user($user2->id, $course->id, $studentrole->id);
+
+        // Login as user 1.
+        $this->setUser($user1);
+        $chatsid = chat_login_user($chat->id, 'ajax', 0, $course);
+        $chatuser = $DB->get_record('chat_users', ['sid' => $chatsid]);
+
+        // This is when the session starts (when the user enters the chat).
+        $sessionstart = $chatuser->lastping;
+
+        // Send some messages.
+        chat_send_chatmessage($chatuser, 'hello!');
+        chat_send_chatmessage($chatuser, 'bye bye!');
+
+        // Login as user 2.
+        $this->setUser($user2);
+        $chatsid = chat_login_user($chat->id, 'ajax', 0, $course);
+        $chatuser = $DB->get_record('chat_users', ['sid' => $chatsid]);
+
+        // Send a message and take note of this message ID.
+        $messageid = chat_send_chatmessage($chatuser, 'greetings!');
+
+        // This is when the session ends (timestamp of the last message sent to the chat).
+        $sessionend = $DB->get_field('chat_messages', 'timestamp', ['id' => $messageid]);
+
+        // Get the messages for this chat session.
+        $messages = chat_get_session_messages($chat->id, false, 0, 0, 'timestamp DESC');
+
+        // We should have 3 user and 2 system (enter) messages.
+        $this->assertCount(5, $messages);
+
+        // Fetch the chat sessions from the messages we retrieved.
+        $sessions = chat_get_sessions($messages, true);
+
+        // There should be only one session.
+        $this->assertCount(1, $sessions);
+
+        // Get this session.
+        $session = reset($sessions);
+
+        // Confirm that the start and end times of the session matches.
+        $this->assertEquals($sessionstart, $session->sessionstart);
+        $this->assertEquals($sessionend, $session->sessionend);
+        // Confirm we have 2 participants in the chat.
+        $this->assertCount(2, $session->sessionusers);
+    }
+
+    /**
+     * Test for chat_get_sessions with messages belonging to multiple sessions.
+     */
+    public function test_chat_get_sessions_multiple() {
+        $messages = [];
+        $gap = 5; // 5 secs.
+
+        $now = time();
+        $timestamp = $now;
+
+        // Messages belonging to 3 sessions. Session 1 has 10 messages, 2 has 15, 3 has 25.
+        $sessionusers = [];
+        $sessiontimes = [];
+        $session = 0; // Incomplete session.
+        for ($i = 1; $i <= 50; $i++) {
+            // Take note of expected session times as we go through.
+            switch ($i) {
+                case 1:
+                    // Session 1 start time.
+                    $sessiontimes[0]['start'] = $timestamp;
+                    break;
+                case 10:
+                    // Session 1 end time.
+                    $sessiontimes[0]['end'] = $timestamp;
+                    break;
+                case 11:
+                    // Session 2 start time.
+                    $sessiontimes[1]['start'] = $timestamp;
+                    break;
+                case 25:
+                    // Session 2 end time.
+                    $sessiontimes[1]['end'] = $timestamp;
+                    break;
+                case 26:
+                    // Session 3 start time.
+                    $sessiontimes[2]['start'] = $timestamp;
+                    break;
+                case 50:
+                    // Session 3 end time.
+                    $sessiontimes[2]['end'] = $timestamp;
+                    break;
+            }
+
+            // User 1 to 5.
+            $user = rand(1, 5);
+
+            // Let's also include system messages as well. Give them to pop in 1-in-10 chance.
+            $issystem = rand(1, 10) == 10;
+
+            if ($issystem) {
+                $message = 'enter';
+            } else {
+                $message = 'Message ' . $i;
+                if (!isset($sessionusers[$session][$user])) {
+                    $sessionusers[$session][$user] = 1;
+                } else {
+                    $sessionusers[$session][$user]++;
+                }
+            }
+            $messages[] = (object)[
+                'id' => $i,
+                'chatid' => 1,
+                'userid' => $user,
+                'message' => $message,
+                'issystem' => $issystem,
+                'timestamp' => $timestamp,
+            ];
+
+            // Set the next timestamp.
+            if ($i == 10 || $i == 25) {
+                // New session.
+                $session++;
+                $timestamp += CHAT_SESSION_GAP + 1;
+            } else {
+                $timestamp += $gap;
+            }
+        }
+        // Reverse sort the messages so they're in descending order.
+        rsort($messages);
+
+        // Get chat sessions showing only complete ones.
+        $completesessions = chat_get_sessions($messages);
+        // Session 1 is incomplete, so there should only be 2 sessions when $showall is false.
+        $this->assertCount(2, $completesessions);
+
+        // Reverse sort sessions so they are in ascending order matching our expected session times and users.
+        $completesessions = array_reverse($completesessions);
+        foreach ($completesessions as $index => $session) {
+            // We increment index by 1 because the incomplete expected session (index=0) is not included.
+            $expectedindex = $index + 1;
+
+            // Check the session users.
+            $users = $sessionusers[$expectedindex];
+            $this->assertCount(count($users), $session->sessionusers);
+            // Check the message counts for each user in this session.
+            foreach ($users as $userid => $messagecount) {
+                $this->assertEquals($messagecount, $session->sessionusers[$userid]);
+            }
+
+            $sessionstart = $sessiontimes[$expectedindex]['start'];
+            $sessionend = $sessiontimes[$expectedindex]['end'];
+            $this->assertEquals($sessionstart, $session->sessionstart);
+            $this->assertEquals($sessionend, $session->sessionend);
+        }
+
+        // Get all the chat sessions.
+        $allsessions = chat_get_sessions($messages, true);
+        // When showall is true, we should get 3 sessions.
+        $this->assertCount(3, $allsessions);
+
+        // Reverse sort sessions so they are in ascending order matching our expected session times and users.
+        $allsessions = array_reverse($allsessions);
+        foreach ($allsessions as $index => $session) {
+            // Check the session users.
+            $users = $sessionusers[$index];
+            $this->assertCount(count($users), $session->sessionusers);
+            // Check the message counts for each user in this session.
+            foreach ($users as $userid => $messagecount) {
+                $this->assertEquals($messagecount, $session->sessionusers[$userid]);
+            }
+
+            $sessionstart = $sessiontimes[$index]['start'];
+            $sessionend = $sessiontimes[$index]['end'];
+            $this->assertEquals($sessionstart, $session->sessionstart);
+            $this->assertEquals($sessionend, $session->sessionend);
+        }
+    }
+
     /**
      * Creates an action event.
      *
index 2308f1a..26c92d6 100644 (file)
@@ -222,7 +222,7 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render
             'value' => $response,
             'id' => $qa->get_qt_field_name($fieldname),
             'size' => $size,
-            'class' => 'form-control',
+            'class' => 'form-control mb-1',
         );
         if ($options->readonly) {
             $inputattributes['readonly'] = 'readonly';
@@ -245,7 +245,7 @@ class qtype_multianswer_textfield_renderer extends qtype_multianswer_subq_render
                         $qa, 'question', 'answerfeedback', $matchinganswer->id),
                 s($correctanswer->answer), $options);
 
-        $output = html_writer::start_tag('span', array('class' => 'subquestion form-inline'));
+        $output = html_writer::start_tag('span', array('class' => 'subquestion form-inline d-inline'));
         $output .= html_writer::tag('label', get_string('answer'),
                 array('class' => 'subq accesshide', 'for' => $inputattributes['id']));
         $output .= html_writer::empty_tag('input', $inputattributes);