From 498293e266ac75cf5b0d05abe335555fee1197cb Mon Sep 17 00:00:00 2001 From: Stefan Hanauska Date: Wed, 5 Nov 2025 14:08:15 +0100 Subject: [PATCH] MDL-86995 mod_forum: Allow ignoring maxeditingtime for qanda forum --- .../backup/moodle2/backup_forum_stepslib.php | 36 +++++++-- .../local/data_mappers/legacy/forum.php | 3 +- .../forum/classes/local/entities/forum.php | 16 +++- .../forum/classes/local/factories/entity.php | 3 +- public/mod/forum/db/install.xml | 3 +- public/mod/forum/db/upgrade.php | 14 ++++ public/mod/forum/externallib.php | 10 ++- public/mod/forum/lang/en/forum.php | 2 + public/mod/forum/lib.php | 7 +- public/mod/forum/mod_form.php | 5 ++ .../mod/forum/tests/behat/qanda_type.feature | 80 +++++++++++++++++++ .../mod/forum/tests/entities_forum_test.php | 4 +- public/mod/forum/version.php | 2 +- 13 files changed, 167 insertions(+), 18 deletions(-) create mode 100644 public/mod/forum/tests/behat/qanda_type.feature diff --git a/public/mod/forum/backup/moodle2/backup_forum_stepslib.php b/public/mod/forum/backup/moodle2/backup_forum_stepslib.php index 8255dd81f74..8ec7dd014c6 100644 --- a/public/mod/forum/backup/moodle2/backup_forum_stepslib.php +++ b/public/mod/forum/backup/moodle2/backup_forum_stepslib.php @@ -38,13 +38,35 @@ class backup_forum_activity_structure_step extends backup_activity_structure_ste // Define each element separated - $forum = new backup_nested_element('forum', array('id'), array( - 'type', 'name', 'intro', 'introformat', 'duedate', 'cutoffdate', - 'assessed', 'assesstimestart', 'assesstimefinish', 'scale', - 'maxbytes', 'maxattachments', 'forcesubscribe', 'trackingtype', - 'rsstype', 'rssarticles', 'timemodified', 'warnafter', - 'blockafter', 'blockperiod', 'completiondiscussions', 'completionreplies', - 'completionposts', 'displaywordcount', 'lockdiscussionafter', 'grade_forum')); + $forum = new backup_nested_element('forum', ['id'], [ + 'type', + 'name', + 'intro', + 'introformat', + 'duedate', + 'cutoffdate', + 'assessed', + 'assesstimestart', + 'assesstimefinish', + 'scale', + 'maxbytes', + 'maxattachments', + 'forcesubscribe', + 'trackingtype', + 'rsstype', + 'rssarticles', + 'timemodified', + 'warnafter', + 'blockafter', + 'blockperiod', + 'completiondiscussions', + 'completionreplies', + 'completionposts', + 'displaywordcount', + 'lockdiscussionafter', + 'grade_forum', + 'showimmediately', + ]); $discussions = new backup_nested_element('discussions'); diff --git a/public/mod/forum/classes/local/data_mappers/legacy/forum.php b/public/mod/forum/classes/local/data_mappers/legacy/forum.php index a55dcbff127..62243acdac1 100644 --- a/public/mod/forum/classes/local/data_mappers/legacy/forum.php +++ b/public/mod/forum/classes/local/data_mappers/legacy/forum.php @@ -73,7 +73,8 @@ class forum { 'displaywordcount' => $forum->should_display_word_count(), 'lockdiscussionafter' => $forum->get_lock_discussions_after(), 'duedate' => $forum->get_due_date(), - 'cutoffdate' => $forum->get_cutoff_date() + 'cutoffdate' => $forum->get_cutoff_date(), + 'showimmediately' => $forum->get_showimmediately(), ]; }, $forums); } diff --git a/public/mod/forum/classes/local/entities/forum.php b/public/mod/forum/classes/local/entities/forum.php index 9d089298077..6e0c187a136 100644 --- a/public/mod/forum/classes/local/entities/forum.php +++ b/public/mod/forum/classes/local/entities/forum.php @@ -107,6 +107,8 @@ class forum { private $duedate; /** @var int $cutoffdate Timestamp after which forum posts will no longer be accepted */ private $cutoffdate; + /** @var bool $showimmediately Whether to show replies in a qanda forum immediately */ + private bool $showimmediately; /** * Constructor @@ -144,6 +146,7 @@ class forum { * @param int $lockdiscussionafter Timestamp after which discussions should be locked * @param int $duedate Timestamp that represents the due date for forum posts * @param int $cutoffdate Timestamp after which forum posts will no longer be accepted + * @param bool $showimmediately Whether to show replies in a qanda forum immediately */ public function __construct( context $context, @@ -178,7 +181,8 @@ class forum { bool $displaywordcount, int $lockdiscussionafter, int $duedate, - int $cutoffdate + int $cutoffdate, + bool $showimmediately ) { $this->context = $context; $this->coursemodule = $coursemodule; @@ -213,6 +217,7 @@ class forum { $this->lockdiscussionafter = $lockdiscussionafter; $this->duedate = $duedate; $this->cutoffdate = $cutoffdate; + $this->showimmediately = $showimmediately; } /** @@ -669,4 +674,13 @@ class forum { return $this->is_discussion_time_locked($discussion); } + + /** + * Get the value of showimmediately + * + * @return bool + */ + public function get_showimmediately(): bool { + return $this->showimmediately; + } } diff --git a/public/mod/forum/classes/local/factories/entity.php b/public/mod/forum/classes/local/factories/entity.php index 0f63dea2875..100af4945a0 100644 --- a/public/mod/forum/classes/local/factories/entity.php +++ b/public/mod/forum/classes/local/factories/entity.php @@ -104,7 +104,8 @@ class entity { $record->displaywordcount, $record->lockdiscussionafter, $record->duedate, - $record->cutoffdate + $record->cutoffdate, + $record->showimmediately ); } diff --git a/public/mod/forum/db/install.xml b/public/mod/forum/db/install.xml index 7e4b9edc843..5ecb8a5b79b 100644 --- a/public/mod/forum/db/install.xml +++ b/public/mod/forum/db/install.xml @@ -1,5 +1,5 @@ - @@ -35,6 +35,7 @@ + diff --git a/public/mod/forum/db/upgrade.php b/public/mod/forum/db/upgrade.php index 33e18af2f7c..a7c25cb9c14 100644 --- a/public/mod/forum/db/upgrade.php +++ b/public/mod/forum/db/upgrade.php @@ -75,5 +75,19 @@ function xmldb_forum_upgrade($oldversion) { // Automatically generated Moodle v5.1.0 release upgrade line. // Put any upgrade step following this. + if ($oldversion < 2025110500) { + // Define field showimmediately to be added to forum. + $table = new xmldb_table('forum'); + $field = new xmldb_field('showimmediately', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, '0', 'lockdiscussionafter'); + + // Conditionally launch add field showimmediately. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Forum savepoint reached. + upgrade_mod_savepoint(true, 2025110500, 'forum'); + } + return true; } diff --git a/public/mod/forum/externallib.php b/public/mod/forum/externallib.php index 26e632e7aac..a4a8f4090eb 100644 --- a/public/mod/forum/externallib.php +++ b/public/mod/forum/externallib.php @@ -139,7 +139,7 @@ class mod_forum_external extends external_api { // Also, the return type declaration is wrong, but I am not changing it now because I don't want ot break things. return new external_multiple_structure( new external_single_structure( - array( + [ 'id' => new external_value(PARAM_INT, 'Forum id'), 'course' => new external_value(PARAM_INT, 'Course id'), 'type' => new external_value(PARAM_TEXT, 'The forum type'), @@ -176,7 +176,13 @@ class mod_forum_external extends external_api { 'istracked' => new external_value(PARAM_BOOL, 'If the user is tracking the forum', VALUE_OPTIONAL), 'unreadpostscount' => new external_value(PARAM_INT, 'The number of unread posts for tracked forums', VALUE_OPTIONAL), - ), 'forum' + 'showimmediately' => new external_value( + PARAM_BOOL, + 'Whether to show replies immediately in a Q&A forum', + VALUE_OPTIONAL + ), + ], + 'forum' ) ); } diff --git a/public/mod/forum/lang/en/forum.php b/public/mod/forum/lang/en/forum.php index c6da4339a79..a30a714d025 100644 --- a/public/mod/forum/lang/en/forum.php +++ b/public/mod/forum/lang/en/forum.php @@ -370,6 +370,8 @@ $string['indicator:socialbreadth_help'] = 'This indicator is based on the social $string['indicator:socialbreadthdef'] = 'Forum social'; $string['indicator:socialbreadthdef_help'] = 'The participant has reached this percentage of the social engagement offered by the Forum activities during this analysis interval (Levels = No participation, Participant alone, Participant with others)'; $string['indicator:socialbreadthdef_link'] = 'Learning_analytics_indicators#Social_breadth'; +$string['showimmediately'] = 'Show other replies immediately after posting'; +$string['showimmediately_help'] = 'This will show the replies of other participants before reaching the maximum editing time ({$a} minutes). Participants could still change their post during this time.'; $string['starredonly'] = 'Search starred discussions only'; $string['indexoutoftotal'] = '{$a->index} out of {$a->total}'; $string['inforum'] = 'in {$a}'; diff --git a/public/mod/forum/lib.php b/public/mod/forum/lib.php index bfec579be4d..806f455ef1c 100644 --- a/public/mod/forum/lib.php +++ b/public/mod/forum/lib.php @@ -580,7 +580,7 @@ function forum_print_recent_activity($course, $viewfullnames, $timestart) { f.scale, f.grade_forum, f.maxbytes, f.maxattachments, f.forcesubscribe, f.trackingtype, f.rsstype, f.rssarticles, f.timemodified, f.warnafter, f.blockafter, f.blockperiod, f.completiondiscussions, f.completionreplies, f.completionposts, - f.displaywordcount, f.lockdiscussionafter, f.grade_forum_notify, + f.displaywordcount, f.lockdiscussionafter, f.grade_forum_notify, f.showimmediately, d.name AS discussionname, d.firstpost, d.userid AS discussionstarter, d.assessed AS discussionassessed, d.timemodified, d.usermodified, d.forum, d.groupid, d.timestart, d.timeend, d.pinned, d.timelocked, @@ -672,7 +672,8 @@ function forum_print_recent_activity($course, $viewfullnames, $timestart) { 'completionposts' => $post->completionposts, 'displaywordcount' => $post->displaywordcount, 'lockdiscussionafter' => $post->lockdiscussionafter, - 'grade_forum_notify' => $post->grade_forum_notify + 'grade_forum_notify' => $post->grade_forum_notify, + 'showimmediately' => $post->showimmediately, ]; // Build the forum entity from the factory. $forumentity = $entityfactory->get_forum_from_stdclass($forumrecord, $context, $coursemodule, $course); @@ -3895,7 +3896,7 @@ function forum_user_can_see_post($forum, $discussion, $post, $user = null, $cm = return true; } $userfirstpost = forum_get_user_posted_time($discussion->id, $user->id); - return (($userfirstpost !== false && (time() - $userfirstpost >= $CFG->maxeditingtime))); + return ($userfirstpost !== false && (time() - $userfirstpost >= $CFG->maxeditingtime || $forum->showimmediately)); } return true; } diff --git a/public/mod/forum/mod_form.php b/public/mod/forum/mod_form.php index 0a7bb5aaa67..b0ab344ceb3 100644 --- a/public/mod/forum/mod_form.php +++ b/public/mod/forum/mod_form.php @@ -56,6 +56,11 @@ class mod_forum_mod_form extends moodleform_mod { $mform->addHelpButton('type', 'forumtype', 'forum'); $mform->setDefault('type', 'general'); + $mform->addElement('advcheckbox', 'showimmediately', get_string('showimmediately', 'forum')); + $mform->addHelpButton('showimmediately', 'showimmediately', 'forum', '', false, $CFG->maxeditingtime / 60); + $mform->setDefault('showimmediately', 0); + $mform->hideIf('showimmediately', 'type', 'neq', 'qanda'); + $mform->addElement('header', 'availability', get_string('availability', 'forum')); $name = get_string('duedate', 'forum'); diff --git a/public/mod/forum/tests/behat/qanda_type.feature b/public/mod/forum/tests/behat/qanda_type.feature new file mode 100644 index 00000000000..fd59e0ade54 --- /dev/null +++ b/public/mod/forum/tests/behat/qanda_type.feature @@ -0,0 +1,80 @@ +@mod @mod_forum +Feature: QandA forum discussion type + In order to let students first see other replies to a post after replying themselves + As a teacher + I need to create a forum of qand a type + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + | student2 | Student | 2 | student2@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + | student2 | C1 | student | + And the following "activities" exist: + | activity | name | intro | type | course | idnumber | showimmediately | + | forum | Q and A forum | Q and A forum description | qanda | C1 | forum | 0 | + And the following "mod_forum > discussions" exist: + | forum | name | subject | message | user | + | forum | Discussion 1 | Discussion 1 | Discussion contents 1, first message | student1 | + | forum | Discussion 2 | Discussion 2 | Discussion contents 2, first message | teacher1 | + And the following "mod_forum > posts" exist: + | parentsubject | subject | message | user | + | Discussion 1 | Reply 1 to discussion 1 | Discussion contents 1, second message | student2 | + | Discussion 2 | Reply 1 to discussion 2 | Discussion contents 2, second message | student2 | + + Scenario: Students can see all replies to own discussions + When I am on the "Q and A forum" "forum activity" page logged in as student1 + And I follow "Discussion 1" + Then I should see "Reply 1 to discussion 1" + + Scenario: Teachers can see all replies + When I am on the "Q and A forum" "forum activity" page logged in as teacher1 + And I follow "Discussion 1" + Then I should see "Reply 1 to discussion 1" + + Scenario: Students can't see replies to discussions from other users they didn't reply to yet + When I am on the "Q and A forum" "forum activity" page logged in as student1 + And I follow "Discussion 2" + Then I should not see "Reply 1 to discussion 2" + + Scenario: Students can't see replies to discussions from other users when they have replied but they are within the maximum editing time + Given the following "mod_forum > posts" exist: + | parentsubject | subject | message | user | + | Discussion 2 | Reply 2 to discussion 1 | Discussion contents 2, third message | student1 | + When I am on the "Q and A forum" "forum activity" page logged in as student1 + And I follow "Discussion 2" + Then I should not see "Reply 1 to discussion 2" + + Scenario: Students can see replies to discussions from other users when they have replied and they can't edit the post anymore + Given the following "mod_forum > posts" exist: + | parentsubject | subject | message | user | created | + | Discussion 2 | Reply 2 to discussion 2 | Discussion contents 2, third message | student1 | ##now +1 second## | + And the following config values are set as admin: + | maxeditingtime | 1 | + And I wait "2" seconds + When I am on the "Q and A forum" "forum activity" page logged in as student1 + And I follow "Discussion 2" + Then I should see "Reply 1 to discussion 2" + + Scenario: Students can see replies to discussions from other users when they have replied regardless of editing time when showimmediately option is set + Given the following "activities" exist: + | activity | name | intro | type | course | idnumber | showimmediately | + | forum | Q and A forum showimmediately | Q and A forum description | qanda | C1 | forum2 | 1 | + And the following "mod_forum > discussions" exist: + | forum | name | subject | message | user | + | forum2 | Discussion 3 | Discussion 3 | Discussion contents 3, first message | teacher1 | + And the following "mod_forum > posts" exist: + | parentsubject | subject | message | user | + | Discussion 3 | Reply 1 to discussion 3 | Discussion contents 3, second message | student2 | + | Discussion 3 | Reply 2 to discussion 3 | Discussion contents 3, second message | student1 | + When I am on the "Q and A forum showimmediately" "forum activity" page logged in as student1 + And I follow "Discussion 3" + Then I should see "Reply 1 to discussion 3" diff --git a/public/mod/forum/tests/entities_forum_test.php b/public/mod/forum/tests/entities_forum_test.php index a582a69d776..d63d414b875 100644 --- a/public/mod/forum/tests/entities_forum_test.php +++ b/public/mod/forum/tests/entities_forum_test.php @@ -91,6 +91,7 @@ final class entities_forum_test extends \advanced_testcase { $duedate = 0; $cutoffdate = 0; $sendnotification = false; + $showimmediately = false; $forum = new forum_entity( $context, $coursemodule, @@ -124,7 +125,8 @@ final class entities_forum_test extends \advanced_testcase { $displaywordcount, $lockdiscussionafter, $duedate, - $cutoffdate + $cutoffdate, + $showimmediately ); $this->assertEquals($context, $forum->get_context()); diff --git a/public/mod/forum/version.php b/public/mod/forum/version.php index 49651baea9e..5032484de8c 100644 --- a/public/mod/forum/version.php +++ b/public/mod/forum/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2025100600; // The current module version (Date: YYYYMMDDXX). +$plugin->version = 2025110500; // The current module version (Date: YYYYMMDDXX). $plugin->requires = 2025092600; // Requires this Moodle version. $plugin->component = 'mod_forum'; // Full name of the plugin (used for diagnostics)