From b927b0eac3c39474b47c048cfffaaa506aac5735 Mon Sep 17 00:00:00 2001
From: Dongsheng Cai <d@tux.im>
Date: Tue, 20 Apr 2021 14:29:22 +1000
Subject: [PATCH] MDL-71330 mod_chat: Implement the activity dates
 functionality

---
 mod/chat/classes/dates.php              |  57 +++++++++++
 mod/chat/lang/en/chat.php               |   5 +-
 mod/chat/lang/en/deprecated.txt         |   1 +
 mod/chat/lib.php                        | 105 ++++++++++++++++----
 mod/chat/tests/behat/behat_mod_chat.php |  76 ++++++++++++++
 mod/chat/tests/dates_test.php           | 127 ++++++++++++++++++++++++
 mod/chat/view.php                       |  12 +--
 7 files changed, 356 insertions(+), 27 deletions(-)
 create mode 100644 mod/chat/classes/dates.php
 create mode 100644 mod/chat/lang/en/deprecated.txt
 create mode 100644 mod/chat/tests/behat/behat_mod_chat.php
 create mode 100644 mod/chat/tests/dates_test.php

diff --git a/mod/chat/classes/dates.php b/mod/chat/classes/dates.php
new file mode 100644
index 00000000000..21f6058f91c
--- /dev/null
+++ b/mod/chat/classes/dates.php
@@ -0,0 +1,57 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains the class for fetching the important dates in mod_chat for a given module instance and a user.
+ *
+ * @package   mod_chat
+ * @copyright 2021 Dongsheng Cai
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types=1);
+
+namespace mod_chat;
+
+use core\activity_dates;
+
+/**
+ * Class for fetching the important dates in mod_chat for a given module instance and a user.
+ *
+ */
+class dates extends activity_dates {
+    /**
+     * Returns a list of important dates in mod_chat.
+     *
+     * @return array
+     */
+    protected function get_dates(): array {
+        $customdata = $this->cm->customdata;
+        $chat = (object) $customdata;
+        $chattime = $chat->chattime ?? 0;
+        $now = time();
+        if (!empty($chat->schedule) && $chattime > $now) {
+            return [
+                [
+                    'label'     => get_string('nextchattime', 'mod_chat'),
+                    'timestamp' => (int) $chattime
+                ]
+            ];
+        }
+
+        return [];
+    }
+}
diff --git a/mod/chat/lang/en/chat.php b/mod/chat/lang/en/chat.php
index 63b55b6a07d..08a0fe926e2 100644
--- a/mod/chat/lang/en/chat.php
+++ b/mod/chat/lang/en/chat.php
@@ -63,6 +63,7 @@ $string['chat:readlog'] = 'View chat logs';
 $string['chatreport'] = 'Chat sessions';
 $string['chat:talk'] = 'Talk in a chat';
 $string['chattime'] = 'Next chat time';
+$string['nextchattime'] = 'Next chat time:';
 $string['chat:view'] = 'View chat activity';
 $string['entermessage'] = "Enter your message";
 $string['eventmessagesent'] = 'Message sent';
@@ -157,7 +158,7 @@ $string['serverip'] = 'Server ip';
 $string['servermax'] = 'Max users';
 $string['serverport'] = 'Server port';
 $string['sessions'] = 'Chat sessions';
-$string['sessionstart'] = 'The next chat session will start on {$a->date}, ({$a->fromnow} from now)';
+$string['sessionstartsin'] = 'The next chat session will start {$a} from now.';
 $string['strftimemessage'] = '%H:%M';
 $string['studentseereports'] = 'Everyone can view past sessions';
 $string['studentseereports_help'] = 'If set to No, only users have mod/chat:readlog capability are able to see the chat logs';
@@ -174,3 +175,5 @@ $string['usingchat_help'] = 'The chat module contains some features to make chat
 * Beeps - You can send a sound to other participants by clicking the "beep" link next to their name. A useful shortcut to beep all the people in the chat at once is to type "beep all".
 * HTML - If you know some HTML code, you can use it in your text to do things like insert images, play sounds or create different coloured text';
 $string['viewreport'] = 'View past chat sessions';
+// Deprecated since Moodle 3.11.
+$string['sessionstart'] = 'The next chat session will start on {$a->date}, ({$a->fromnow} from now)';
diff --git a/mod/chat/lang/en/deprecated.txt b/mod/chat/lang/en/deprecated.txt
new file mode 100644
index 00000000000..5262248af39
--- /dev/null
+++ b/mod/chat/lang/en/deprecated.txt
@@ -0,0 +1 @@
+sessionstart,mod_chat
diff --git a/mod/chat/lib.php b/mod/chat/lib.php
index f7431cad047..2df0acde47a 100644
--- a/mod/chat/lib.php
+++ b/mod/chat/lib.php
@@ -31,6 +31,14 @@ 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);
+// Don't publish next chat time
+define('CHAT_SCHEDULE_NONE', 0);
+// Publish the specified time only.
+define('CHAT_SCHEDULE_SINGLE', 1);
+// Repeat chat session at the same time daily.
+define('CHAT_SCHEDULE_DAILY', 2);
+// Repeat chat session at the same time weekly.
+define('CHAT_SCHEDULE_WEEKLY', 3);
 
 // The HTML head for the message window to start with (<!-- nix --> is used to get some browsers starting with output.
 global $CHAT_HTMLHEAD;
@@ -116,6 +124,7 @@ function chat_add_instance($chat) {
     global $DB;
 
     $chat->timemodified = time();
+    $chat->chattime = chat_calculate_next_chat_time($chat->schedule, $chat->chattime);
 
     $returnid = $DB->insert_record("chat", $chat);
 
@@ -159,6 +168,7 @@ function chat_update_instance($chat) {
 
     $chat->timemodified = time();
     $chat->id = $chat->instance;
+    $chat->chattime = chat_calculate_next_chat_time($chat->schedule, $chat->chattime);
 
     $DB->update_record("chat", $chat);
 
@@ -636,6 +646,35 @@ function chat_delete_old_users() {
     }
 }
 
+/**
+ * Calculate next chat session time based on schedule.
+ *
+ * @param int $schedule
+ * @param int $chattime
+ *
+ * @return int timestamp
+ */
+function chat_calculate_next_chat_time(int $schedule, int $chattime): int {
+    $timenow = time();
+
+    switch ($schedule) {
+        case CHAT_SCHEDULE_DAILY: { // Repeat daily.
+            while ($chattime <= $timenow) {
+                $chattime += DAYSECS;
+            }
+            break;
+        }
+        case CHAT_SCHEDULE_WEEKLY: { // Repeat weekly.
+            while ($chattime <= $timenow) {
+                $chattime += WEEKSECS;
+            }
+            break;
+        }
+    }
+
+    return $chattime;
+}
+
 /**
  * Updates chat records so that the next chat time is correct
  *
@@ -661,29 +700,18 @@ function chat_update_chat_times($chatid=0) {
         }
     }
 
+    $courseids = [];
     foreach ($chats as $chat) {
-        switch ($chat->schedule) {
-            case 1: // Single event - turn off schedule and disable.
-                $chat->chattime = 0;
-                $chat->schedule = 0;
-                break;
-            case 2: // Repeat daily.
-                while ($chat->chattime <= $timenow) {
-                    $chat->chattime += 24 * 3600;
-                }
-                break;
-            case 3: // Repeat weekly.
-                while ($chat->chattime <= $timenow) {
-                    $chat->chattime += 7 * 24 * 3600;
-                }
-                break;
+        $originalchattime = $chat->chattime;
+        $chat->chattime = chat_calculate_next_chat_time($chat->schedule, $chat->chattime);
+        if ($originalchattime != $chat->chattime) {
+            $courseids[] = $chat->course;
         }
         $DB->update_record("chat", $chat);
-
         $event = new stdClass(); // Update calendar too.
 
-        $cond = "modulename='chat' AND instance = :chatid AND timestart <> :chattime";
-        $params = array('chattime' => $chat->chattime, 'chatid' => $chat->id);
+        $cond = "modulename='chat' AND eventtype = :eventtype AND instance = :chatid AND timestart <> :chattime";
+        $params = ['chattime' => $chat->chattime, 'eventtype' => CHAT_EVENT_TYPE_CHATTIME, 'chatid' => $chat->id];
 
         if ($event->id = $DB->get_field_select('event', 'id', $cond, $params)) {
             $event->timestart = $chat->chattime;
@@ -692,6 +720,11 @@ function chat_update_chat_times($chatid=0) {
             $calendarevent->update($event, false);
         }
     }
+
+    $courseids = array_unique($courseids);
+    foreach ($courseids as $courseid) {
+        rebuild_course_cache($courseid, true);
+    }
 }
 
 /**
@@ -1536,3 +1569,39 @@ function chat_get_session_messages($chatid, $group = false, $start = 0, $end = 0
 
     return $DB->get_records_select('chat_messages', $select, $params, $sort);
 }
+
+/**
+ * Add a get_coursemodule_info function in case chat instance wants to add 'extra' information
+ * for the course (see resource).
+ *
+ * Given a course_module object, this function returns any "extra" information that may be needed
+ * when printing this activity in a course listing.  See get_array_of_activities() in course/lib.php.
+ *
+ * @param stdClass $coursemodule The coursemodule object (record).
+ * @return cached_cm_info An object on information that the courses
+ *                        will know about (most noticeably, an icon).
+ */
+function chat_get_coursemodule_info($coursemodule) {
+    global $DB;
+
+    $dbparams = ['id' => $coursemodule->instance];
+    $fields = 'id, name, intro, introformat, chattime, schedule';
+    if (!$chat = $DB->get_record('chat', $dbparams, $fields)) {
+        return false;
+    }
+
+    $result = new cached_cm_info();
+    $result->name = $chat->name;
+    if ($coursemodule->showdescription) {
+        // Convert intro to html. Do not filter cached version, filters run at display time.
+        $result->content = format_module_intro('chat', $chat, $coursemodule->id, false);
+    }
+
+    // Populate some other values that can be used in calendar or on dashboard.
+    if ($chat->chattime) {
+        $result->customdata['chattime'] = $chat->chattime;
+        $result->customdata['schedule'] = $chat->schedule;
+    }
+
+    return $result;
+}
diff --git a/mod/chat/tests/behat/behat_mod_chat.php b/mod/chat/tests/behat/behat_mod_chat.php
new file mode 100644
index 00000000000..53c9eead539
--- /dev/null
+++ b/mod/chat/tests/behat/behat_mod_chat.php
@@ -0,0 +1,76 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Steps definitions related to mod_chat.
+ *
+ * @package   mod_chat
+ * @category  test
+ * @copyright 2021 Dongsheng Cai
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
+
+/**
+ * Steps definitions related to mod_chat.
+ *
+ */
+class behat_mod_chat extends behat_base {
+    /**
+     * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'.
+     *
+     * Recognised page names are:
+     * | pagetype          | name meaning | description                   |
+     * | View              | Chat name    | The chat info page (view.php) |
+     *
+     * @param string $type identifies which type of page this is, e.g. 'View'.
+     * @param string $name chat instance name
+     * @return moodle_url the corresponding URL.
+     * @throws Exception with a meaningful error message if the specified page cannot be found.
+     */
+    protected function resolve_page_instance_url(string $type, string $name): moodle_url {
+        switch ($type) {
+            case 'View':
+                $cm = $this->get_cm_by_chat_name($name);
+                return new moodle_url('/mod/chat/view.php', ['id' => $cm->id]);
+            default:
+                throw new Exception('Unrecognised chat page type "' . $type . '."');
+        }
+    }
+
+    /**
+     * Get a chat by name.
+     *
+     * @param string $name chat name.
+     * @return stdClass the corresponding DB row.
+     */
+    protected function get_chat_by_name(string $name): stdClass {
+        global $DB;
+        return $DB->get_record('chat', ['name' => $name], '*', MUST_EXIST);
+    }
+
+    /**
+     * Get a chat coursemodule object from the name.
+     *
+     * @param string $name chat name.
+     * @return stdClass cm from get_coursemodule_from_instance.
+     */
+    protected function get_cm_by_chat_name(string $name): stdClass {
+        $chat = $this->get_chat_by_name($name);
+        return get_coursemodule_from_instance('chat', $chat->id, $chat->course);
+    }
+}
diff --git a/mod/chat/tests/dates_test.php b/mod/chat/tests/dates_test.php
new file mode 100644
index 00000000000..dd8d3407612
--- /dev/null
+++ b/mod/chat/tests/dates_test.php
@@ -0,0 +1,127 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains unit tests for mod_chat\dates.
+ *
+ * @package   mod_chat
+ * @category  test
+ * @copyright 2021 Dongsheng Cai
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+declare(strict_types=1);
+
+namespace mod_chat;
+
+use advanced_testcase;
+use cm_info;
+use core\activity_dates;
+
+/**
+ * Class for unit testing mod_chat\dates.
+ */
+class dates_test extends advanced_testcase {
+
+    /**
+     * Data provider for get_dates_for_module().
+     * @return array[]
+     */
+    public function get_dates_for_module_provider(): array {
+        global $CFG;
+        require_once($CFG->dirroot . '/mod/chat/lib.php');
+
+        $now = time();
+        $past = $now - DAYSECS;
+        $future = $now + DAYSECS;
+
+        $dailynextchattime = $past + 2 * DAYSECS;
+        $weeklynextchattime = $past + 7 * DAYSECS;
+        $label = get_string('nextchattime', 'mod_chat');
+        return [
+            'chattime in the past' => [
+                $past, CHAT_SCHEDULE_NONE, []
+            ],
+            'chattime in the past' => [
+                $past, CHAT_SCHEDULE_SINGLE, []
+            ],
+            'chattime in the future' => [
+                $future, CHAT_SCHEDULE_SINGLE, [
+                    [
+                        'label' => $label,
+                        'timestamp' => $future
+                    ],
+                ]
+            ],
+            'future chattime weekly' => [
+                $future, CHAT_SCHEDULE_WEEKLY, [
+                    [
+                        'label' => $label,
+                        'timestamp' => $future
+                    ]
+                ]
+            ],
+            'future chattime daily' => [
+                $future, CHAT_SCHEDULE_DAILY, [
+                    [
+                        'label' => $label,
+                        'timestamp' => $future
+                    ]
+                ]
+            ],
+            'past chattime daily' => [
+                $past, CHAT_SCHEDULE_DAILY, [
+                    [
+                        'label' => $label,
+                        'timestamp' => $dailynextchattime
+                    ],
+                ]
+            ],
+            'past chattime weekly' => [
+                $past, CHAT_SCHEDULE_WEEKLY, [
+                    [
+                        'label' => $label,
+                        'timestamp' => $weeklynextchattime
+                    ],
+                ]
+            ],
+        ];
+    }
+
+    /**
+     * Test for get_dates_for_module().
+     *
+     * @dataProvider get_dates_for_module_provider
+     * @param int|null $chattime
+     * @param int|null $schedule
+     * @param array $expected The expected value of calling get_dates_for_module()
+     */
+    public function test_get_dates_for_module(?int $chattime, ?int $schedule, array $expected) {
+        $this->resetAfterTest();
+        $course = $this->getDataGenerator()->create_course();
+        $user = $this->getDataGenerator()->create_user();
+        $this->getDataGenerator()->enrol_user($user->id, $course->id);
+        $chat = ['course' => $course->id];
+        $chat['chattime'] = $chattime;
+        $chat['schedule'] = $schedule;
+        $modchat = $this->getDataGenerator()->create_module('chat', $chat);
+        $this->setUser($user);
+        $cm = get_coursemodule_from_instance('chat', $modchat->id);
+        $cminfo = cm_info::create($cm);
+        $dates = activity_dates::get_dates_for_module($cminfo, (int) $user->id);
+        $this->assertEquals($expected, $dates);
+    }
+}
diff --git a/mod/chat/view.php b/mod/chat/view.php
index 6daf4d0b7ef..ad98f546fdc 100644
--- a/mod/chat/view.php
+++ b/mod/chat/view.php
@@ -122,14 +122,10 @@ if (has_capability('mod/chat:chat', $context)) {
     echo $OUTPUT->box_start('generalbox', 'enterlink');
 
     $now = time();
-    $span = $chat->chattime - $now;
-    if ($chat->chattime and $chat->schedule and ($span > 0)) {  // A chat is scheduled.
-        echo '<p>';
-        $chatinfo = new stdClass();
-        $chatinfo->date = userdate($chat->chattime);
-        $chatinfo->fromnow = format_time($span);
-        echo get_string('sessionstart', 'chat', $chatinfo);
-        echo '</p>';
+    $chattime = $chat->chattime ?? 0;
+    $span = $chattime - $now;
+    if (!empty($chat->schedule) && $span > 0) {
+        echo html_writer::tag('p', get_string('sessionstartsin', 'chat', format_time($span)));
     }
 
     $params['id'] = $chat->id;