diff --git a/.github/workflows/config.json b/.github/workflows/config.json
deleted file mode 100644
index 7a643bc495..0000000000
--- a/.github/workflows/config.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "moodle-plugin-ci": "4.5.7",
- "main-moodle": "MOODLE_500_STABLE",
- "main-php": "8.3",
- "main-db": "pgsql",
- "moodle-testmatrix": {
- "MOODLE_401_STABLE": {
- "php": ["8.0", "8.1"]
- },
- "MOODLE_404_STABLE": {
- "php": ["8.1", "8.2", "8.3"]
- },
- "MOODLE_405_STABLE": {
- "php": ["8.1", "8.2", "8.3"],
- "db": ["pgsql", "mariadb", "mysqli"]
- },
- "MOODLE_500_STABLE": {
- "php": ["8.2", "8.3", "8.4"],
- "db": ["pgsql", "mariadb", "mysqli"]
- }
- }
-}
\ No newline at end of file
diff --git a/CHANGES.md b/CHANGES.md
index e490f2344c..de63c4d35d 100755
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,9 +1,12 @@
CHANGELOG
=========
-5.0 (2025-06-24)
+v5.0-r1 (2025-08-01)
------------------
-
+- Fixes Issues [#216](https://github.com/learnweb/moodle-mod_moodleoverflow/issues/216),
+ [#211](https://github.com/learnweb/moodle-mod_moodleoverflow/issues/211),
+ [#202](https://github.com/learnweb/moodle-mod_moodleoverflow/issues/202)
+- Adaption to Moodle 5.0
4.5.1 (2025-05-19)
------------------
diff --git a/classes/discussion/discussion.php b/classes/discussion/discussion.php
index 68b2d7a03d..bbc99a9777 100644
--- a/classes/discussion/discussion.php
+++ b/classes/discussion/discussion.php
@@ -81,7 +81,7 @@ class discussion {
// Not Database-related attributes.
- /** @var array an Array of posts that belong to this discussion */
+ /** @var post[] an Array of posts that belong to this discussion */
public $posts;
/** @var bool a variable for checking if this instance has all its posts */
@@ -126,7 +126,7 @@ public function __construct($id, $course, $moodleoverflow, $name, $firstpost,
* Builds a Discussion from a DB record.
*
* @param object $record Data object.
- * @return object discussion instance
+ * @return discussion discussion instance
*/
public static function from_record($record) {
$id = null;
@@ -258,9 +258,8 @@ public function moodleoverflow_delete_discussion($prepost) {
$transaction = $DB->start_delegated_transaction();
// Delete every post of this discussion.
- foreach ($this->posts as $post) {
- $post->moodleoverflow_delete_post(false);
- }
+ $firstpost = $this->posts[$this->firstpost];
+ $firstpost->moodleoverflow_delete_post(true);
// Delete the read-records for the discussion.
readtracking::moodleoverflow_delete_read_records(-1, -1, $this->id);
diff --git a/classes/output/helpicon.php b/classes/output/helpicon.php
deleted file mode 100644
index b1867f850f..0000000000
--- a/classes/output/helpicon.php
+++ /dev/null
@@ -1,72 +0,0 @@
-.
-
-/**
- * Use of the Helpicon from Moodle core.
- * @package mod_moodleoverflow
- * @copyright 2023 Tamaro Walter
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-namespace mod_moodleoverflow\output;
-
-/**
- * Builds a Helpicon, that shows a String when hovering over it.
- * @package mod_moodleoverflow
- * @copyright 2023 Tamaro Walter
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-class helpicon {
-
- /** @var object The Helpicon*/
- private $helpobject;
-
- /**
- * Builds a Helpicon and stores it in helpobject.
- *
- * @param string $htmlclass The classname in which the icon will be.
- * @param string $content A string that shows the information that the icon has.
- */
- public function __construct($htmlclass, $content) {
- global $CFG;
- $iconurl = $CFG->wwwroot . '/pix/a/help.svg';
- $iconstyle = ['style' =>
- 'max-width: 20px; max-height: 20px; margin: 0; padding: 0; box-sizing: content-box; margin-right: .5rem;'];
- $icon = \html_writer::img($iconurl, $content, $iconstyle);
-
- $class = $htmlclass;
- $iconattributes = ['role' => 'button',
- 'style' => 'display: inline;',
- 'data-container' => 'body',
- 'data-toggle' => 'popover',
- 'data-placement' => 'right',
- 'data-action' => 'showhelpicon',
- 'data-html' => 'true',
- 'data-trigger' => 'focus',
- 'tabindex' => '0',
- 'data-content' => '
', ];
- $this->helpobject = \html_writer::span($icon, $class, $iconattributes);
- }
-
- /**
- * Returns the Helpicon, so that it can be used.
- *
- * @return object The Helpicon
- */
- public function get_helpicon() {
- return $this->helpobject;
- }
-}
diff --git a/classes/post/post.php b/classes/post/post.php
index ae9d122875..942d5ddbd8 100644
--- a/classes/post/post.php
+++ b/classes/post/post.php
@@ -31,6 +31,7 @@
use mod_moodleoverflow\review;
use mod_moodleoverflow\readtracking;
use mod_moodleoverflow\discussion\discussion;
+use mod_moodleoverflow_post_form;
use moodle_exception;
defined('MOODLE_INTERNAL') || die();
@@ -103,8 +104,8 @@ class post {
/** @var string The subject/title of the Discussion */
public $subject;
- /** @var object The discussion where the post is located */
- public $discussionobject;
+ /** @var discussion The discussion where the post is located */
+ public discussion $discussionobject;
/** @var object The Moodleoverflow where the post is located*/
public $moodleoverflowobject;
@@ -154,9 +155,9 @@ public function __construct($id, $discussion, $parent, $userid, $created, $modif
* Builds a Post from a DB record.
* Look up database structure for standard values.
* @param object $record Data object.
- * @return object post instance
+ * @return post post instance
*/
- public static function from_record($record) {
+ public static function from_record($record): post {
$id = null;
if (object_property_exists($record, 'id') && $record->id) {
$id = $record->id;
@@ -259,6 +260,15 @@ public function moodleoverflow_add_new_post() {
// Add post to the database.
$this->id = $DB->insert_record('moodleoverflow_posts', $this->build_db_object());
+
+ // Save draft files to permanent file area.
+ $context = \context_module::instance($this->get_coursemodule()->id);
+ $draftid = file_get_submitted_draft_itemid('introeditor');
+ $this->message = file_save_draft_area_files($draftid, $context->id, 'mod_moodleoverflow', 'post',
+ $this->id, mod_moodleoverflow_post_form::editor_options($context, $this->id), $this->message);
+ $DB->update_record('moodleoverflow_posts', $this->build_db_object());
+
+ // Update the attachments. This happens after the DB update call, as this function changes the DB record as well.
$this->moodleoverflow_add_attachment();
if ($this->reviewed) {
@@ -375,10 +385,15 @@ public function moodleoverflow_edit_post($time, $postmessage, $messageformat, $f
// Update the attributes.
$this->modified = $time;
- $this->message = $postmessage;
$this->messageformat = $messageformat;
$this->formattachments = $formattachments;
+ // Update the message and save draft files to permanent file area.
+ $context = \context_module::instance($this->get_coursemodule()->id);
+ $draftid = file_get_submitted_draft_itemid('introeditor');
+ $this->message = file_save_draft_area_files($draftid, $context->id, 'mod_moodleoverflow', 'post',
+ $this->id, mod_moodleoverflow_post_form::editor_options($context, $this->id), $postmessage);
+
// Update the record in the database.
$DB->update_record('moodleoverflow_posts', $this->build_db_object());
@@ -546,9 +561,9 @@ public function get_moodleoverflow() {
/**
* Returns the discussion where the post is located.
*
- * @return object $discussionobject.
+ * @return discussion $discussionobject.
*/
- public function get_discussion() {
+ public function get_discussion(): discussion {
global $DB;
$this->existence_check();
@@ -631,7 +646,7 @@ public function get_db_object() {
public function moodleoverflow_get_post_ratings() {
$this->existence_check();
- $discussionid = $this->get_discussion()->id;
+ $discussionid = $this->get_discussion()->get_id();
$postratings = \mod_moodleoverflow\ratings::moodleoverflow_get_ratings_by_discussion($discussionid, $this->id);
$ratingsobject = new \stdClass();
diff --git a/classes/post/post_control.php b/classes/post/post_control.php
index c32b0a8e91..d2fb43be86 100644
--- a/classes/post/post_control.php
+++ b/classes/post/post_control.php
@@ -796,11 +796,11 @@ private function check_moodleoverflow_exists(int $moodleoverflowid): object {
/**
* Checks if the related discussion exists.
* @param int $discussionid
- * @return object $discussion
+ * @return discussion $discussion
* @throws dml_exception
* @throws moodle_exception
*/
- private function check_discussion_exists(int $discussionid): object {
+ private function check_discussion_exists(int $discussionid): discussion {
global $DB;
if (!$discussionrecord = $DB->get_record('moodleoverflow_discussions', ['id' => $discussionid])) {
throw new moodle_exception('invaliddiscussionid', 'moodleoverflow');
@@ -811,11 +811,11 @@ private function check_discussion_exists(int $discussionid): object {
/**
* Checks if a post exists.
* @param int $postid
- * @return object $post
+ * @return post $post
* @throws dml_exception
* @throws moodle_exception
*/
- private function check_post_exists(int $postid): object {
+ private function check_post_exists(int $postid): post {
global $DB;
if (!$postrecord = $DB->get_record('moodleoverflow_posts', ['id' => $postid])) {
throw new moodle_exception('invalidpostid', 'moodleoverflow');
diff --git a/classes/post_form.php b/classes/post_form.php
index 2e1c66e2b5..58212feee8 100644
--- a/classes/post_form.php
+++ b/classes/post_form.php
@@ -159,7 +159,7 @@ public static function editor_options(context_module $context, $postid) {
'maxbytes' => $maxbytes,
'trusttext' => true,
'return_types' => FILE_INTERNAL | FILE_EXTERNAL,
- 'subdirs' => file_area_contains_subdirs($context, 'mod_forum', 'post', $postid),
+ 'subdirs' => file_area_contains_subdirs($context, 'mod_moodleoverflow', 'post', $postid),
];
}
}
diff --git a/classes/tables/userstats_table.php b/classes/tables/userstats_table.php
index dc2db1e610..4e4bd348a0 100644
--- a/classes/tables/userstats_table.php
+++ b/classes/tables/userstats_table.php
@@ -30,7 +30,6 @@
require_once($CFG->dirroot . '/mod/moodleoverflow/lib.php');
require_once($CFG->dirroot . '/mod/moodleoverflow/locallib.php');
require_once($CFG->libdir . '/tablelib.php');
-use mod_moodleoverflow\output\helpicon;
/**
* Table listing all user statistics of a course
@@ -50,9 +49,6 @@ class userstats_table extends \flexible_table {
/** @var array table that will have objects with every user and his statistics. */
private $userstatsdata = [];
- /** @var \stdClass Help icon for amountofactivity-column.*/
- private $helpactivity;
-
/**
* Constructor for workflow_table.
*
@@ -62,13 +58,13 @@ class userstats_table extends \flexible_table {
* @param string $url The url of the table
*/
public function __construct($uniqueid, $courseid, $moodleoverflow, $url) {
- global $PAGE;
+ global $PAGE, $OUTPUT;
parent::__construct($uniqueid);
$PAGE->requires->js_call_amd('mod_moodleoverflow/activityhelp', 'init');
$this->courseid = $courseid;
$this->moodleoverflowid = $moodleoverflow;
- $this->set_helpactivity();
+ $helpactivity = $OUTPUT->help_icon('helpamountofactivity', 'moodleoverflow');
$this->set_attribute('class', 'moodleoverflow-statistics-table');
$this->set_attribute('id', $uniqueid);
@@ -78,8 +74,8 @@ public function __construct($uniqueid, $courseid, $moodleoverflow, $url) {
$this->define_headers([get_string('fullnameuser'),
get_string('userstatsupvotes', 'moodleoverflow'),
get_string('userstatsdownvotes', 'moodleoverflow'),
- (get_string('userstatsforumactivity', 'moodleoverflow') . $this->helpactivity->object),
- (get_string('userstatscourseactivity', 'moodleoverflow') . $this->helpactivity->object),
+ (get_string('userstatsforumactivity', 'moodleoverflow') . $helpactivity),
+ (get_string('userstatscourseactivity', 'moodleoverflow') . $helpactivity),
get_string('userstatsforumreputation', 'moodleoverflow'),
get_string('userstatscoursereputation', 'moodleoverflow'), ]);
$this->get_table_data();
@@ -148,17 +144,6 @@ public function get_usertable() {
return $this->userstatsdata;
}
- /**
- * Setup the help icon for amount of activity
- */
- public function set_helpactivity() {
- $htmlclass = 'helpactivityclass btn btn-link';
- $content = get_string('helpamountofactivity', 'moodleoverflow');
- $helpobject = new helpicon($htmlclass, $content);
- $this->helpactivity = new \stdClass();
- $this->helpactivity->object = $helpobject->get_helpicon();
- }
-
// Functions that show the data.
/**
diff --git a/lang/en/moodleoverflow.php b/lang/en/moodleoverflow.php
index dd3cbdd861..9e2371ec9c 100644
--- a/lang/en/moodleoverflow.php
+++ b/lang/en/moodleoverflow.php
@@ -167,7 +167,8 @@
$string['grademaxgradeerror'] = 'Maximum grade must be a positive integer different than 0';
$string['gradesreport'] = 'Grades report';
$string['gradesupdated'] = 'Grades updated';
-$string['helpamountofactivity'] = 'Each activity like writing a post, starting a discussion or giving a rating gives 1 point';
+$string['helpamountofactivity'] = 'Help icon for activity';
+$string['helpamountofactivity_help'] = 'Each activity like writing a post, starting a discussion or giving a rating gives 1 point';
$string['hiddenmoodleoverflowpost'] = 'Hidden forum post';
$string['invaliddiscussionid'] = 'Discussion ID was incorrect';
$string['invalidforcesubscribe'] = 'Invalid force subscription mode';
@@ -177,20 +178,24 @@
$string['invalidratingid'] = 'The submitted rating is neither an upvote nor a downvote.';
$string['jump_to_next_post_needing_review'] = 'Jump to next post needing to be reviewed.';
$string['la_endtime'] = 'Time at which students can no longer answer';
-$string['la_endtime_help'] = 'Students can not answer to qustions after the set up date';
+$string['la_endtime_help'] = 'Students can not answer questions after the set up date';
$string['la_endtime_ruleerror'] = 'End time must be in the future';
+$string['la_heading'] = 'Limited Answer Mode';
+$string['la_helpicon_teacher'] = 'This can be changed in the settings of the Moodleoverflow.';
+$string['la_info_endtime'] = 'Posts can not be answered after {$a->limitedanswerdate}.';
+$string['la_info_start'] = 'This Moodleoverflow is in a limited answer mode.';
+$string['la_info_starttime'] = 'Posts can not be answered until {$a->limitedanswerdate}.';
$string['la_sequence_error'] = 'The end time must be after the start time';
$string['la_starttime'] = 'Time at which students can start to answer';
-$string['la_starttime_help'] = 'Students can not answer to questions until the set up date';
+$string['la_starttime_help'] = 'Students can start to answer questions after the set up date';
$string['la_starttime_ruleerror'] = 'Start time must be in the future';
+$string['la_student_helpicon'] = "limited answer help icon";
+$string['la_student_helpicon_help'] = "This moodleoveroverflow is currently in a restricted mode. You can answer as soon as the teacher allows it.";
+$string['la_teacher_helpicon'] = "limited answer help icon";
+$string['la_teacher_helpicon_help'] = "This moodleoverflow is currently in a restricted mode. This can be changed in the settings of this acticity.";
+$string['la_warning_answers'] = 'There are already answered posts in this Moodleoverflow.';
+$string['la_warning_conclusion'] = 'You can only set a time until students are able to answer';
$string['lastpost'] = 'Last post';
-$string['limitedanswer_helpicon_teacher'] = 'This can be changed in the settings of the Moodleoverflow.';
-$string['limitedanswer_info_endtime'] = 'Posts can not be answered after {$a->limitedanswerdate}.';
-$string['limitedanswer_info_start'] = 'This Moodleoverflow is in a limited answer mode.';
-$string['limitedanswer_info_starttime'] = 'Posts can not be answered until {$a->limitedanswerdate}.';
-$string['limitedanswerheading'] = 'Limited Answer Mode';
-$string['limitedanswerwarning_answers'] = 'There are already answered posts in this Moodleoverflow.';
-$string['limitedanswerwarning_conclusion'] = 'You can only set a time until students are able to answer';
$string['mailindexlink'] = 'Change your forum preferences: {$a}';
$string['manydiscussions'] = 'Discussions per page';
$string['markallread'] = 'Mark all posts in this discussion as read';
diff --git a/locallib.php b/locallib.php
index 79fde129e7..f049731883 100644
--- a/locallib.php
+++ b/locallib.php
@@ -27,7 +27,6 @@
use mod_moodleoverflow\anonymous;
use mod_moodleoverflow\capabilities;
use mod_moodleoverflow\event\post_deleted;
-use mod_moodleoverflow\output\helpicon;
use mod_moodleoverflow\ratings;
use mod_moodleoverflow\readtracking;
use mod_moodleoverflow\review;
@@ -1327,21 +1326,21 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co
// Check if limitedanswertime is on.
$settingexist = $limitedanswersetting->la_starttime != 0 || $limitedanswersetting->la_endtime != 0;
if ($settingexist) {
- $infolimited = $limitedanswersetting->la_starttime ? " " . get_string('limitedanswer_info_starttime',
+ $infolimited = $limitedanswersetting->la_starttime ? " " . get_string('la_info_starttime',
'moodleoverflow', ['limitedanswerdate' => date('d.m.Y H:i', $limitedanswersetting->la_starttime)]) : '';
- $infolimited .= $limitedanswersetting->la_endtime ? " " . get_string('limitedanswer_info_endtime', 'moodleoverflow',
+ $infolimited .= $limitedanswersetting->la_endtime ? " " . get_string('la_info_endtime', 'moodleoverflow',
['limitedanswerdate' => date('d.m.Y H:i', $limitedanswersetting->la_endtime)]) : '';
echo html_writer::div($infolimited, 'alert alert-warning', ['role' => 'alert']);
}
if (is_currently_time_limited($limitedanswersetting)) {
if (!has_capability('mod/moodleoverflow:addinstance', $modulecontext)) {
// In case the user can not change the limited answer time he/she can not answer.
- render_limited_answer('text-muted', $commands, $infolimited, 'student', $str->replyfirst);
+ render_limited_answer('text-muted', $commands, 'student', $str->replyfirst);
} else {
// The user is a teacher.
$replyurl = new moodle_url('/mod/moodleoverflow/post.php#mformmoodleoverflow', ['reply' => $post->id]);
$answerbutton = html_writer::link($replyurl, $str->replyfirst, ['class' => 'onlyifreviewed answerbutton']);
- render_limited_answer('', $commands, $infolimited, 'teacher', $answerbutton);
+ render_limited_answer('', $commands, 'teacher', $answerbutton);
}
} else {
$replyurl = new moodle_url('/mod/moodleoverflow/post.php#mformmoodleoverflow', ['reply' => $post->id]);
@@ -1537,30 +1536,27 @@ function is_currently_time_limited($limitedanswersetting): bool {
/**
* Renders the answer action in a post.
- * @param String $htmlattributes additional attributes passed to the html class and the command (either 'text-muted' or empty).
+ * @param string $htmlattributes additional attributes passed to the html class and the command (either 'text-muted' or empty).
* @param array $commands array of actions available to the user in a post.
- * @param String $infolimited information about the limited answer setting.
- * @param String $role either 'student' or 'teacher'.
- * @param String $helpstring content for the tag specifing the helpicon for the answer button.
+ * @param string $role either 'student' or 'teacher'.
+ * @param string $helpstring content for the tag specifing the helpicon for the answer button.
* @return void
* @throws coding_exception
*/
-function render_limited_answer($htmlattributes, &$commands, $infolimited, $role, $helpstring) {
+function render_limited_answer(string $htmlattributes, array &$commands, string $role, string $helpstring): void {
+ global $OUTPUT;
$limitedanswerattributes = ['class' => 'onlyifreviewed ' . $htmlattributes];
- $htmlclass = 'onlyifreviewed helpicon ' . $htmlattributes;
- $content = get_string('limitedanswer_info_start', 'moodleoverflow');
- $content .= $infolimited;
- $htmlattributes == '' ? $content .= " " . get_string('limitedanswer_helpicon_teacher', 'moodleoverflow') : $content .= '';
- $helpobject = new helpicon($htmlclass, $content);
- $helpicon = $helpobject->get_helpicon();
+ $helpicon = match ($htmlattributes) {
+ 'text-muted' => $OUTPUT->help_icon('la_student_helpicon', 'moodleoverflow'),
+ '' => $OUTPUT->help_icon('la_teacher_helpicon', 'moodleoverflow'),
+ };
+
// Build a html span that has the answer button and the help icon.
$limitedanswerobject = html_writer::tag('span', $helpstring . ' ' . $helpicon);
// Save the span in the commands with an extra value.
- $commands[] = ['text' => $limitedanswerobject,
- 'attributes' => $limitedanswerattributes,
- 'limitedanswer' => $role, ];
+ $commands[] = ['text' => $limitedanswerobject, 'attributes' => $limitedanswerattributes, 'limitedanswer' => $role];
}
/**
diff --git a/mod_form.php b/mod_form.php
index 987b98f23f..81e5d35b3a 100644
--- a/mod_form.php
+++ b/mod_form.php
@@ -230,7 +230,7 @@ public function definition() {
$mform->setDefault('allowmultiplemarks', 0);
// Limited answer options.
- $mform->addElement('header', 'limitedanswerheading', get_string('limitedanswerheading', 'moodleoverflow'));
+ $mform->addElement('header', 'limitedanswerheading', get_string('la_heading', 'moodleoverflow'));
$answersfound = false;
if (!empty($this->current->id)) {
@@ -244,8 +244,8 @@ public function definition() {
$answerpostscount = $answerpostscount[array_key_first($answerpostscount)]->answerposts;
$answersfound = $answerpostscount > 0;
if ($answersfound) {
- $warningstring = get_string('limitedanswerwarning_answers', 'moodleoverflow');
- $warningstring .= '
' . get_string('limitedanswerwarning_conclusion', 'moodleoverflow');
+ $warningstring = get_string('la_warning_answers', 'moodleoverflow');
+ $warningstring .= '
' . get_string('la_warning_conclusion', 'moodleoverflow');
$htmlwarning = html_writer::div($warningstring, 'alert alert-warning', ['role' => 'alert']);
$mform->addElement('html', $htmlwarning);
}
diff --git a/tests/behat/behat_mod_moodleoverflow.php b/tests/behat/behat_mod_moodleoverflow.php
index 9965c0e557..2d129a760d 100644
--- a/tests/behat/behat_mod_moodleoverflow.php
+++ b/tests/behat/behat_mod_moodleoverflow.php
@@ -41,6 +41,142 @@
*/
class behat_mod_moodleoverflow extends behat_base {
+
+ /**
+ * Build basic background for moodleoverflow tests.
+ * Builds:
+ * - A course
+ * - One teacher and one student, both enrolled in the course
+ * - A moodleoverflow activity
+ * - A discussion started by the teacher
+ * - A reply to the discussion by the student
+ *
+ * @Given /^I add a moodleoverflow discussion with posts from different users$/
+ * @return void
+ */
+ public function i_add_a_moodleoverflow_discussion_with_posts_from_different_users(): void {
+ global $DB;
+ $time = time();
+ $starttime = $time - 31556926;
+ $endtime = $time + 31556926;
+
+ // Create users.
+ $this->execute('behat_data_generators::the_following_entities_exist', ['users', new TableNode([
+ ['username', 'firstname', 'lastname'],
+ ['teacher1', 'Tamaro', 'Walter'],
+ ['student1', 'John', 'Smith'],
+ ])]);
+
+ $teacher = $DB->get_record('user', ['username' => 'teacher1']);
+ $student = $DB->get_record('user', ['username' => 'student1']);
+
+ // Create course.
+ $this->execute('behat_data_generators::the_following_entities_exist', ['courses', new TableNode([
+ ['fullname', 'shortname', 'category', 'startdate', 'enddate'],
+ ['Course 1', 'C1', '0', $starttime, $endtime],
+ ])]);
+ $course = $DB->get_record('course', ['shortname' => 'C1']);
+
+ // Enroll users.
+ $this->execute('behat_data_generators::the_following_entities_exist', ['course enrolments', new TableNode([
+ ['user', 'course', 'role'],
+ ['teacher1', 'C1', 'teacher'],
+ ['student1', 'C1', 'student'],
+ ])]);
+
+ // Create activity.
+ $this->execute('behat_data_generators::the_following_entities_exist', ['activities', new TableNode([
+ ['activity', 'course', 'name'],
+ ['moodleoverflow', 'C1', 'Moodleoverflow 1'],
+ ])]);
+ $moodleoverflow = $DB->get_record('moodleoverflow', ['name' => 'Moodleoverflow 1']);
+
+ // Create discussion.
+ $discussionid = $DB->insert_record('moodleoverflow_discussions', ['course' => $course->id,
+ 'moodleoverflow' => $moodleoverflow->id, 'name' => 'Discussion 1', 'firstpost' => 1, 'userid' => $teacher->id,
+ 'timemodified' => $time, 'timestart' => $time, 'usermodified' => $teacher->id,
+ ]);
+
+ // Create teacher's post.
+ $teacherpostid = $DB->insert_record('moodleoverflow_posts', ['discussion' => $discussionid,
+ 'moodleoverflow' => $moodleoverflow->id, 'parent' => 0, 'userid' => $teacher->id, 'created' => $time,
+ 'modified' => $time, 'message' => 'Message from teacher', 'messageformat' => 1, 'attachment' => '', 'mailed' => 1,
+ 'reviewed' => '1', 'timereviewed' => null,
+ ]);
+
+ // Update firstpost field in discussion.
+ $record = $DB->get_record('moodleoverflow_discussions', ['id' => $discussionid]);
+ $record->firstpost = $teacherpostid;
+ $DB->update_record('moodleoverflow_discussions', $record);
+
+ // Create student reply.
+ $DB->insert_record('moodleoverflow_posts', ['discussion' => $discussionid, 'moodleoverflow' => $moodleoverflow->id,
+ 'parent' => $teacherpostid, 'userid' => $student->id, 'created' => $time, 'modified' => $time,
+ 'message' => 'Answer from student', 'messageformat' => 1, 'attachment' => '', 'mailed' => 1, 'reviewed' => '1',
+ 'timereviewed' => null,
+ ]);
+ }
+
+ /**
+ * Logs in as a user and navigates in a course to dedicated moodleoverflow discussion.
+ *
+ * @Given /^I navigate as "(?P[^"]*)" to "(?P[^"]*)" "(?P[^"]*)" "(?P[^"]*)"$/
+ *
+ * @param string $user The username to log in as
+ * @param string $course The full name of the course
+ * @param string $moodleoverflow The name of the moodleoverflow activity
+ * @param string $discussion The name of the discussion
+ * @return void
+ * @throws Exception
+ */
+ public function i_navigate_as_user_to_the_discussion(string $user, string $course, string $moodleoverflow,
+ string $discussion): void {
+ $this->execute('behat_auth::i_log_in_as', $user);
+ $this->execute('behat_navigation::i_am_on_course_homepage', $course);
+ $this->execute('behat_general::click_link', $this->escape($moodleoverflow));
+ $this->execute('behat_general::click_link', $this->escape($discussion));
+ }
+
+ /**
+ * Clicks on the delete button of a moodleoverflow post.
+ *
+ * @Given /^I try to delete moodleoverflow post "(?P(?:[^"]|\\")*)"$/
+ * @param string $postmessage
+ * @return void
+ * @throws Exception
+ */
+ public function i_try_to_delete_moodleoverflow_post(string $postmessage): void {
+ // Find the div containing the post message and click the delete link within it.
+ $this->execute('behat_general::i_click_on', [
+ "//div[contains(@class, 'moodleoverflowpost')][contains(., '" . $this->escape($postmessage) . "')]//a[text()='Delete']",
+ "xpath_element",
+ ]);
+ $this->execute('behat_general::i_click_on', ['Continue', 'button']);
+ }
+
+ /**
+ * Clicks on the comment button of a moodleoverflow post
+ *
+ * @Given /^I comment "(?P(?:[^"]|\\")*)" with "(?P(?:[^"]|\\")*)"$/
+ * @param string $postmessage
+ * @param string $replymessage
+ * @return void
+ * @throws Exception
+ */
+ public function i_comment_moodleoverflow_post_with_message(string $postmessage, string $replymessage): void {
+ $this->execute('behat_general::i_click_on', [
+ "//div[contains(@class, 'moodleoverflowpost')][contains(., '" . $this->escape($postmessage) .
+ "')]//a[text()='Comment']",
+ "xpath_element",
+ ]);
+ $table = new TableNode([
+ ['Message', $replymessage],
+ ]);
+ $this->execute('behat_forms::i_set_the_following_fields_to_these_values', $table);
+ $this->execute('behat_forms::press_button', get_string('posttomoodleoverflow', 'moodleoverflow'));
+ $this->execute('behat_general::i_wait_to_be_redirected');
+ }
+
/**
* Adds a new moodleoverflow to the specified course and section.
*
@@ -104,7 +240,7 @@ public function i_add_a_moodleoverflow_discussion_to_moodleoverflow_with($moodle
}
/**
- * Adds a reply to the specified post of the specified moodleoverflow.
+ * Adds a reply to the starter post of the specified moodleoverflow.
* The step begins from the moodleoverflow's page or from the moodleoverflow's course page.
*
* @Given /^I reply "(?P(?:[^"]|\\")*)" post
diff --git a/tests/behat/delete_post.feature b/tests/behat/delete_post.feature
new file mode 100644
index 0000000000..6d90d2a17f
--- /dev/null
+++ b/tests/behat/delete_post.feature
@@ -0,0 +1,30 @@
+@mod @mod_moodleoverflow @javascript @mod_moodleoverflow_delete @javascript
+Feature: Teachers and students can delete posts
+
+ Background:
+ Given I add a moodleoverflow discussion with posts from different users
+
+ Scenario: Teacher deletes the whole discussion
+ Given I navigate as "teacher1" to "Course 1" "Moodleoverflow 1" "Discussion 1"
+ And I try to delete moodleoverflow post "Message from teacher"
+ Then I should not see "Discussion 1"
+
+ Scenario: Teacher only deletes reply post
+ Given I navigate as "teacher1" to "Course 1" "Moodleoverflow 1" "Discussion 1"
+ And I try to delete moodleoverflow post "Answer from student"
+ Then I should see "Discussion 1"
+ And I should not see "Answer from student"
+
+ Scenario: Student deletes own reply post
+ Given I navigate as "student1" to "Course 1" "Moodleoverflow 1" "Discussion 1"
+ And I try to delete moodleoverflow post "Answer from student"
+ Then I should see "Discussion 1"
+ And I should not see "Answer from student"
+
+ Scenario: Student tries to delete his post which the teacher already replied
+ Given I navigate as "teacher1" to "Course 1" "Moodleoverflow 1" "Discussion 1"
+ And I comment "Answer from student" with "comment from teacher"
+ And I log out
+ And I navigate as "student1" to "Course 1" "Moodleoverflow 1" "Discussion 1"
+ And I try to delete moodleoverflow post "Answer from student"
+ Then I should see "You are not allowed to delete this post because it already has replies."
\ No newline at end of file
diff --git a/tests/discussion_test.php b/tests/discussion_test.php
index e5ee1c6c31..a69926e95e 100644
--- a/tests/discussion_test.php
+++ b/tests/discussion_test.php
@@ -91,6 +91,9 @@ public function test_create_discussion(): void {
$prepost->formattachments = '';
$prepost->modulecontext = $this->modulecontext;
+ // Log in as the teacher.
+ $this->setUser($this->teacher);
+
// Build a new discussion object.
$discussion = discussion::construct_without_id($this->course->id, $this->moodleoverflow->id, 'Discussion Topic',
0, $this->teacher->id, $time, $time, $this->teacher->id);
diff --git a/tests/post_test.php b/tests/post_test.php
index 8b16739bbd..ff6729482a 100644
--- a/tests/post_test.php
+++ b/tests/post_test.php
@@ -85,6 +85,10 @@ public function tearDown(): void {
*/
public function test_create_post(): void {
global $DB;
+
+ // Log in as the teacher.
+ $this->setUser($this->teacher);
+
// Build a new post object.
$time = time();
$message = 'a unique message';
@@ -103,6 +107,9 @@ public function test_create_post(): void {
public function test_edit_post(): void {
global $DB;
+ // Log in as the teacher.
+ $this->setUser($this->teacher);
+
// The post and the attachment should exist.
$numberofattachments = count($DB->get_records('files', ['itemid' => $this->post->get_id()]));
$this->assertEquals(2, $numberofattachments); // One Attachment is saved twice in 'files'.
diff --git a/version.php b/version.php
index 15e7ad3556..aacba4b21e 100644
--- a/version.php
+++ b/version.php
@@ -26,10 +26,10 @@
defined('MOODLE_INTERNAL') || die();
-$plugin->version = 2025072500;
+$plugin->version = 2025080100;
$plugin->requires = 2022112819; // Require Moodle 4.1.
$plugin->supported = [401, 500];
$plugin->component = 'mod_moodleoverflow';
-$plugin->maturity = MATURITY_RC;
-$plugin->release = 'v5.0-rc1';
+$plugin->maturity = MATURITY_STABLE;
+$plugin->release = 'v5.0-r1';
$plugin->dependencies = [];