diff --git a/.github/workflows/moodle-ci.yml b/.github/workflows/moodle-ci.yml index cc16e5483b..41e4b21d56 100644 --- a/.github/workflows/moodle-ci.yml +++ b/.github/workflows/moodle-ci.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: php: ['8.3'] - moodle-branch: ['MOODLE_404_STABLE'] + moodle-branch: ['MOODLE_405_STABLE'] database: ['pgsql'] steps: @@ -111,17 +111,17 @@ jobs: fail-fast: false matrix: php: ['8.0', '8.1', '8.2', '8.3'] - moodle-branch: ['MOODLE_401_STABLE', 'MOODLE_402_STABLE', 'MOODLE_403_STABLE', 'MOODLE_404_STABLE'] + moodle-branch: ['MOODLE_401_STABLE', 'MOODLE_403_STABLE', 'MOODLE_404_STABLE', 'MOODLE_405_STABLE'] database: ['mariadb', 'pgsql'] exclude: - php: '8.0' moodle-branch: 'MOODLE_404_STABLE' + - php: '8.0' + moodle-branch: 'MOODLE_405_STABLE' - php: '8.2' moodle-branch: 'MOODLE_401_STABLE' - php: '8.3' moodle-branch: 'MOODLE_401_STABLE' - - php: '8.3' - moodle-branch: 'MOODLE_402_STABLE' - php: '8.3' moodle-branch: 'MOODLE_403_STABLE' include: diff --git a/backup/moodle2/backup_moodleoverflow_stepslib.php b/backup/moodle2/backup_moodleoverflow_stepslib.php index aa56787799..391f6754ef 100644 --- a/backup/moodle2/backup_moodleoverflow_stepslib.php +++ b/backup/moodle2/backup_moodleoverflow_stepslib.php @@ -39,14 +39,16 @@ class backup_moodleoverflow_activity_structure_step extends backup_activity_stru * @return backup_nested_element */ protected function define_structure() { + // To know if we are including userinfo. $userinfo = $this->get_setting_value('userinfo'); // Define the root element describing the moodleoverflow instance. $moodleoverflow = new backup_nested_element('moodleoverflow', ['id'], [ - 'name', 'intro', 'introformat', 'maxbytes', 'maxattachments', 'timecreated', 'timemodified', 'forcesubscribe', - 'trackingtype', 'ratingpreference', 'coursewidereputation', 'allowrating', 'allowreputation', 'allownegativereputation', - 'grademaxgrade', 'gradescalefactor', 'gradecat', 'anonymous', 'allowmultiplemarks', ]); + 'name', 'intro', 'introformat', 'maxbytes', 'maxattachments', 'timecreated', 'timemodified', + 'forcesubscribe', 'trackingtype', 'ratingpreference', 'coursewidereputation', 'allowrating', + 'allowreputation', 'allownegativereputation', 'grademaxgrade', 'gradescalefactor', 'gradecat', + 'anonymous', 'allowmultiplemarks', 'la_starttime', 'la_endtime', ]); // Define each element separated. $discussions = new backup_nested_element('discussions'); @@ -54,20 +56,24 @@ protected function define_structure() { 'name', 'firstpost', 'userid', 'timestart', 'timemodified', 'usermodified', ]); $posts = new backup_nested_element('posts'); - $post = new backup_nested_element('post', ['id'], ['parent', 'userid', 'created', 'modified', 'message', - 'messageformat', 'attachment', 'mailed', 'reviewed', 'timereviewed', ]); + $post = new backup_nested_element('post', ['id'], [ + 'parent', 'userid', 'created', 'modified', + 'message', 'messageformat', 'attachment', 'mailed', 'reviewed', 'timereviewed', ]); $ratings = new backup_nested_element('ratings'); - $rating = new backup_nested_element('rating', ['id'], ['userid', 'rating', 'firstrated', 'lastchanged']); + $rating = new backup_nested_element('rating', ['id'], [ + 'userid', 'rating', 'firstrated', 'lastchanged', ]); $discussionsubs = new backup_nested_element('discuss_subs'); - $discussionsub = new backup_nested_element('discuss_sub', ['id'], ['userid', 'preference']); + $discussionsub = new backup_nested_element('discuss_sub', ['id'], [ + 'userid', 'preference', ]); $subscriptions = new backup_nested_element('subscriptions'); $subscription = new backup_nested_element('subscription', ['id'], ['userid']); $readposts = new backup_nested_element('readposts'); - $read = new backup_nested_element('read', ['id'], ['userid', 'discussionid', 'postid', 'firstread', 'lastread']); + $read = new backup_nested_element('read', ['id'], [ + 'userid', 'discussionid', 'postid', 'firstread', 'lastread', ]); $grades = new backup_nested_element('grades'); $grade = new backup_nested_element('grade', ['id'], ['userid', 'grade']); @@ -94,9 +100,6 @@ protected function define_structure() { $moodleoverflow->add_child($readposts); $readposts->add_child($read); - $moodleoverflow->add_child($grades); - $grades->add_child($grade); - $moodleoverflow->add_child($tracking); $tracking->add_child($track); diff --git a/classes/output/helpicon.php b/classes/output/helpicon.php new file mode 100644 index 0000000000..b1867f850f --- /dev/null +++ b/classes/output/helpicon.php @@ -0,0 +1,72 @@ +. + +/** + * 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' => '

' . $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/tables/userstats_table.php b/classes/tables/userstats_table.php index d88fa7ce21..eed18f636a 100644 --- a/classes/tables/userstats_table.php +++ b/classes/tables/userstats_table.php @@ -30,6 +30,7 @@ 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 @@ -151,27 +152,11 @@ public function get_usertable() { * Setup the help icon for amount of activity */ public function set_helpactivity() { - global $CFG; + $htmlclass = 'helpactivityclass btn btn-link'; + $content = get_string('helpamountofactivity', 'moodleoverflow'); + $helpobject = new helpicon($htmlclass, $content); $this->helpactivity = new \stdClass(); - $this->helpactivity->iconurl = $CFG->wwwroot . '/pix/a/help.png'; - $this->helpactivity->icon = \html_writer::img($this->helpactivity->iconurl, - get_string('helpamountofactivity', 'moodleoverflow')); - $this->helpactivity->class = 'helpactivityclass btn btn-link'; - $this->helpactivity->iconattributes = ['role' => 'button', - 'data-container' => 'body', - 'data-toggle' => 'popover', - 'data-placement' => 'right', - 'data-action' => 'showhelpicon', - 'data-html' => 'true', - 'data-trigger' => 'focus', - 'tabindex' => '0', - 'data-content' => '

' . - get_string('helpamountofactivity', 'moodleoverflow') . - '

', ]; - - $this->helpactivity->object = \html_writer::span($this->helpactivity->icon, - $this->helpactivity->class, - $this->helpactivity->iconattributes); + $this->helpactivity->object = $helpobject->get_helpicon(); } // Functions that show the data. diff --git a/db/install.xml b/db/install.xml index f7f28d27fb..9d3f323f5f 100644 --- a/db/install.xml +++ b/db/install.xml @@ -28,6 +28,8 @@ + + diff --git a/db/upgrade.php b/db/upgrade.php index dc813f23a9..070183b1d1 100644 --- a/db/upgrade.php +++ b/db/upgrade.php @@ -300,5 +300,22 @@ function xmldb_moodleoverflow_upgrade($oldversion) { upgrade_mod_savepoint(true, 2024072600, 'moodleoverflow'); } + if ($oldversion < 2025031200) { + // Define table moodleoverflow to be edited. + $table = new xmldb_table('moodleoverflow'); + + // Create the field fot the start time for the limited answer mode. + $field = new xmldb_field('la_starttime', XMLDB_TYPE_INTEGER, '10', null, null, null, 0, 'allowmultiplemarks'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + // Create the field for the end time for the limited answer mode. + $field = new xmldb_field('la_endtime', XMLDB_TYPE_INTEGER, '10', null, null, null, 0, 'la_starttime'); + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + upgrade_mod_savepoint(true, 2025031200, 'moodleoverflow'); + } + return true; } diff --git a/discussion.php b/discussion.php index bb0cfec030..488e34e14a 100644 --- a/discussion.php +++ b/discussion.php @@ -55,6 +55,8 @@ if ($marksetting->allowmultiplemarks == 1) { $multiplemarks = true; } +// Setting of limitedanswer. Limitedanswertime saves the timestamp, until the limitedanswer is on (0 if off). +$limitedanswersetting = $DB->get_record('moodleoverflow', ['id' => $moodleoverflow->id], 'la_starttime, la_endtime'); // Get the related coursemodule and its context. if (!$cm = get_coursemodule_from_instance('moodleoverflow', $moodleoverflow->id, $course->id)) { @@ -156,7 +158,7 @@ echo '
'; -moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discussion, $post, $multiplemarks); +moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discussion, $post, $multiplemarks, $limitedanswersetting); echo '
'; echo $OUTPUT->footer(); diff --git a/lang/en/moodleoverflow.php b/lang/en/moodleoverflow.php index f2c65c5235..4a0876e485 100644 --- a/lang/en/moodleoverflow.php +++ b/lang/en/moodleoverflow.php @@ -172,7 +172,21 @@ $string['invalidpostid'] = 'Invalid post ID - {$a}'; $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_ruleerror'] = 'End time must be in the future'; +$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_ruleerror'] = 'Start time must be in the future'; $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 375da72dfd..8829da045d 100644 --- a/locallib.php +++ b/locallib.php @@ -27,6 +27,8 @@ 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; @@ -255,7 +257,7 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = - } // Check if the question owner marked the question as helpful. - $markedhelpful = \mod_moodleoverflow\ratings::moodleoverflow_discussion_is_solved($discussion->discussion, false); + $markedhelpful = ratings::moodleoverflow_discussion_is_solved($discussion->discussion, false); $preparedarray[$i]['starterlink'] = null; if ($markedhelpful) { $link = '/mod/moodleoverflow/discussion.php?d='; @@ -266,7 +268,7 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = - } // Check if a teacher marked a post as solved. - $markedsolution = \mod_moodleoverflow\ratings::moodleoverflow_discussion_is_solved($discussion->discussion, true); + $markedsolution = ratings::moodleoverflow_discussion_is_solved($discussion->discussion, true); $preparedarray[$i]['teacherlink'] = null; if ($markedsolution) { $link = '/mod/moodleoverflow/discussion.php?d='; @@ -285,7 +287,7 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = - } // Get the amount of votes for the discussion. - $votes = \mod_moodleoverflow\ratings::moodleoverflow_get_ratings_by_discussion($discussion->discussion, $discussion->id); + $votes = ratings::moodleoverflow_get_ratings_by_discussion($discussion->discussion, $discussion->id); $votes = $votes->upvotes - $votes->downvotes; $preparedarray[$i]['votetext'] = ($votes == 1) ? 'vote' : 'votes'; @@ -397,13 +399,13 @@ function moodleoverflow_print_latest_discussions($moodleoverflow, $cm, $page = - $preparedarray[$i]['votes'] = $votes; // Did the user rated this post? - $rating = \mod_moodleoverflow\ratings::moodleoverflow_user_rated($discussion->firstpost); + $rating = ratings::moodleoverflow_user_rated($discussion->firstpost); $firstpost = moodleoverflow_get_post_full($discussion->firstpost); $preparedarray[$i]['userupvoted'] = ($rating->rating ?? null) == RATING_UPVOTE; $preparedarray[$i]['userdownvoted'] = ($rating->rating ?? null) == RATING_DOWNVOTE; - $preparedarray[$i]['canchange'] = \mod_moodleoverflow\ratings::moodleoverflow_user_can_rate($firstpost, $context) && + $preparedarray[$i]['canchange'] = ratings::moodleoverflow_user_can_rate($firstpost, $context) && $startuser->id != $USER->id; $preparedarray[$i]['postid'] = $discussion->firstpost; @@ -926,14 +928,16 @@ function moodleoverflow_user_can_post($modulecontext, $posttoreplyto, $considerr /** * Prints a moodleoverflow discussion. * - * @param stdClass $course The course object + * @param stdClass $course The course object * @param object $cm - * @param stdClass $moodleoverflow The moodleoverflow object - * @param stdClass $discussion The discussion object - * @param stdClass $post The post object - * @param bool $multiplemarks The setting of multiplemarks (default: multiplemarks are not allowed) + * @param stdClass $moodleoverflow The moodleoverflow object + * @param stdClass $discussion The discussion object + * @param stdClass $post The post object + * @param bool $multiplemarks The setting of multiplemarks (default: multiplemarks are not allowed) + * @param stdClass|null $limitedanswersetting Two Unix timestamp wrapped in a stdClass, upper and lower label for answering. */ -function moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discussion, $post, $multiplemarks = false) { +function moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discussion, $post, + $multiplemarks = false, ?stdClass $limitedanswersetting = null) { global $USER; // Check if the current is the starter of the discussion. $ownpost = (isloggedin() && ($USER->id == $post->userid)); @@ -985,7 +989,7 @@ function moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discuss // Print the starting post. echo moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $course, - $ownpost, false, '', '', $postread, true, $istracked, 0, $usermapping, 0, $multiplemarks); + $ownpost, false, '', '', $postread, true, $istracked, 0, $usermapping, 0, $multiplemarks, $limitedanswersetting); // Print answer divider. if ($answercount == 1) { @@ -999,7 +1003,7 @@ function moodleoverflow_print_discussion($course, $cm, $moodleoverflow, $discuss // Print the other posts. echo moodleoverflow_print_posts_nested($course, $cm, $moodleoverflow, $discussion, $post, $istracked, $posts, - null, $usermapping, $multiplemarks); + null, $usermapping, $multiplemarks, $limitedanswersetting); echo ''; } @@ -1060,7 +1064,7 @@ function moodleoverflow_get_all_discussion_posts($discussionid, $tracking, $modc } // Load all ratings. - $discussionratings = \mod_moodleoverflow\ratings::moodleoverflow_get_ratings_by_discussion($discussionid); + $discussionratings = ratings::moodleoverflow_get_ratings_by_discussion($discussionid); // Assign ratings to the posts. foreach ($posts as $postid => $post) { @@ -1074,7 +1078,7 @@ function moodleoverflow_get_all_discussion_posts($discussionid, $tracking, $modc } // Order the answers by their ratings. - $posts = \mod_moodleoverflow\ratings::moodleoverflow_sort_answers_by_ratings($posts); + $posts = ratings::moodleoverflow_sort_answers_by_ratings($posts); // Find all children of this post. foreach ($posts as $postid => $post) { @@ -1118,17 +1122,18 @@ function moodleoverflow_get_all_discussion_posts($discussionid, $tracking, $modc * @param object $moodleoverflow * @param object $cm * @param object $course - * @param object $ownpost + * @param bool $ownpost * @param bool $link * @param string $footer * @param string $highlight - * @param bool $postisread + * @param null $postisread * @param bool $dummyifcantsee * @param bool $istracked * @param bool $iscomment * @param array $usermapping * @param int $level - * @param bool $multiplemarks setting of multiplemarks + * @param bool $multiplemarks The setting of multiplemarks (default: multiplemarks are not allowed) + * @param stdClass|null $limitedanswersetting Two Unix timestamp wrapped in a stdClass, upper and lower label for answering. * @return void|null * @throws coding_exception * @throws dml_exception @@ -1138,8 +1143,9 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co $ownpost = false, $link = false, $footer = '', $highlight = '', $postisread = null, $dummyifcantsee = true, $istracked = false, - $iscomment = false, $usermapping = [], $level = 0, $multiplemarks = false) { - global $USER, $CFG, $OUTPUT, $PAGE; + $iscomment = false, $usermapping = [], $level = 0, + $multiplemarks = false, ?stdClass $limitedanswersetting = null) { + global $USER, $CFG, $OUTPUT, $PAGE, $DB; // Require the filelib. require_once($CFG->libdir . '/filelib.php'); @@ -1240,8 +1246,8 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co $helpfulposts = false; $solvedposts = false; if ($multiplemarks) { - $helpfulposts = \mod_moodleoverflow\ratings::moodleoverflow_discussion_is_solved($discussion->id, false); - $solvedposts = \mod_moodleoverflow\ratings::moodleoverflow_discussion_is_solved($discussion->id, true); + $helpfulposts = ratings::moodleoverflow_discussion_is_solved($discussion->id, false); + $solvedposts = ratings::moodleoverflow_discussion_is_solved($discussion->id, true); } // If the user has started the discussion, he can mark the answer as helpful. @@ -1310,16 +1316,34 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co // Give the option to reply to a post. if (moodleoverflow_user_can_post($modulecontext, $post, false)) { - $attributes = [ 'class' => 'onlyifreviewed', ]; - // Answer to the parent post. if (empty($post->parent)) { - $replyurl = new moodle_url('/mod/moodleoverflow/post.php#mformmoodleoverflow', ['reply' => $post->id]); - $commands[] = ['url' => $replyurl, 'text' => $str->replyfirst, 'attributes' => $attributes]; - + // 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', + 'moodleoverflow', ['limitedanswerdate' => date('d.m.Y H:i', $limitedanswersetting->la_starttime)]) : ''; + $infolimited .= $limitedanswersetting->la_endtime ? " " . get_string('limitedanswer_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); + } 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); + } + } else { + $replyurl = new moodle_url('/mod/moodleoverflow/post.php#mformmoodleoverflow', ['reply' => $post->id]); + $commands[] = ['url' => $replyurl, 'text' => $str->replyfirst, 'attributes' => $attributes]; + } // If the post is a comment, answer to the parent post. } else if (!$iscomment) { $replyurl = new moodle_url('/mod/moodleoverflow/post.php#mformmoodleoverflow', ['reply' => $post->id]); @@ -1349,7 +1373,7 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co $mustachedata->markedsolution = $post->markedsolution; // Did the user rated this post? - $rating = \mod_moodleoverflow\ratings::moodleoverflow_user_rated($post->id); + $rating = ratings::moodleoverflow_user_rated($post->id); // Initiate the variables. $mustachedata->userupvoted = false; @@ -1423,7 +1447,7 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co if (anonymous::is_post_anonymous($discussion, $moodleoverflow, $post->userid)) { $postuserrating = null; } else { - $postuserrating = \mod_moodleoverflow\ratings::moodleoverflow_get_reputation($moodleoverflow->id, $postinguser->id); + $postuserrating = ratings::moodleoverflow_get_reputation($moodleoverflow->id, $postinguser->id); } // The name of the user and the date modified. @@ -1470,7 +1494,11 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co $commandhtml = []; foreach ($commands as $command) { if (is_array($command)) { - $commandhtml[] = html_writer::link($command['url'], $command['text'], $command['attributes'] ?? null); + if (array_key_exists('limitedanswer', $command)) { + $commandhtml[] = html_writer::tag('span', $command['text'], $command['attributes'] ?? null); + } else { + $commandhtml[] = html_writer::link($command['url'], $command['text'], $command['attributes'] ?? null); + } } else { $commandhtml[] = $command; } @@ -1494,27 +1522,66 @@ function moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $co return $renderer->render_post($mustachedata); } +/** + * Check if the limited answer setting is currently disabling answers. + * @param stdClass $limitedanswersetting Two Unix timestamp wrapped in a stdClass, upper and lower label for answering. + * @return bool + */ +function is_currently_time_limited($limitedanswersetting): bool { + return ($limitedanswersetting->la_starttime != 0 && $limitedanswersetting->la_starttime > time()) + || ($limitedanswersetting->la_endtime != 0 && $limitedanswersetting->la_endtime < time()); +} + +/** + * 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 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. + * @return void + * @throws coding_exception + */ +function render_limited_answer($htmlattributes, &$commands, $infolimited, $role, $helpstring) { + $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(); + // 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, ]; +} /** * Prints all posts of the discussion in a nested form. * - * @param object $course The course object + * @param object $course The course object * @param object $cm - * @param object $moodleoverflow The moodleoverflow object - * @param object $discussion The discussion object - * @param object $parent The object of the parent post - * @param bool $istracked Whether the user tracks the discussion - * @param array $posts Array of posts within the discussion - * @param bool $iscomment Whether the current post is a comment - * @param array $usermapping - * @param bool $multiplemarks + * @param object $moodleoverflow The moodleoverflow object + * @param object $discussion The discussion object + * @param object $parent The object of the parent post + * @param bool $istracked Whether the user tracks the discussion + * @param array $posts Array of posts within the discussion + * @param bool $iscomment Whether the current post is a comment + * @param array $usermapping + * @param bool $multiplemarks The setting of multiplemarks (default: multiplemarks are not allowed) + * @param stdClass|null $limitedanswersetting Two Unix timestamp wrapped in a stdClass, upper and lower label for answering. * @return string * @throws coding_exception * @throws dml_exception * @throws moodle_exception */ function moodleoverflow_print_posts_nested($course, &$cm, $moodleoverflow, $discussion, $parent, - $istracked, $posts, $iscomment = null, $usermapping = [], $multiplemarks = false) { + $istracked, $posts, $iscomment = null, $usermapping = [], + $multiplemarks = false, ?stdClass $limitedanswersetting = null) { global $USER; // Prepare the output. @@ -1555,12 +1622,13 @@ function moodleoverflow_print_posts_nested($course, &$cm, $moodleoverflow, $disc $postread = !empty($post->postread); // Print the answer. - $output .= moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $course, - $ownpost, false, '', '', $postread, true, $istracked, $parentid, $usermapping, $level, $multiplemarks); + $output .= moodleoverflow_print_post($post, $discussion, $moodleoverflow, $cm, $course, $ownpost, false, '', '', + $postread, true, $istracked, $parentid, $usermapping, $level, + $multiplemarks, $limitedanswersetting); // Print its children. $output .= moodleoverflow_print_posts_nested($course, $cm, $moodleoverflow, - $discussion, $post, $istracked, $posts, $parentid, $usermapping, $multiplemarks); + $discussion, $post, $istracked, $posts, $parentid, $usermapping, $multiplemarks, $limitedanswersetting); // End the div. $output .= "\n"; @@ -2135,7 +2203,7 @@ function moodleoverflow_update_all_grades_for_cm($moodleoverflowid) { } // Get user reputation. - $userrating = \mod_moodleoverflow\ratings::moodleoverflow_get_reputation($moodleoverflow->id, $userid, true); + $userrating = ratings::moodleoverflow_get_reputation($moodleoverflow->id, $userid, true); // Calculate the posting user's updated grade. moodleoverflow_update_user_grade_on_db($moodleoverflow, $userrating, $userid); diff --git a/mod_form.php b/mod_form.php index 30cdef4367..57256565c5 100644 --- a/mod_form.php +++ b/mod_form.php @@ -45,7 +45,7 @@ class mod_moodleoverflow_mod_form extends moodleform_mod { * Defines forms elements. */ public function definition() { - global $CFG, $COURSE, $PAGE; + global $CFG, $COURSE, $PAGE, $DB; // Define the modform. $mform = $this->_form; @@ -229,6 +229,50 @@ public function definition() { $mform->addHelpButton('allowmultiplemarks', 'allowmultiplemarks', 'moodleoverflow'); $mform->setDefault('allowmultiplemarks', 0); + // Limited answer options. + $mform->addElement('header', 'limitedanswerheading', get_string('limitedanswerheading', 'moodleoverflow')); + + $answersfound = false; + if (!empty($this->current->id)) { + + $limiteddate = $DB->get_record('moodleoverflow', ['id' => $this->current->id], 'la_starttime, la_endtime'); + // Check if limitedanswermode was already set up and place a warning in case the starttime has already expired ... + // ... or the endtime has already expired. + + // Check if there are already answered posts in this moodleoverflow and place a warning if so. + $sql = 'SELECT COUNT(*) AS answerposts + FROM {moodleoverflow_discussions} discuss JOIN {moodleoverflow_posts} posts + ON discuss.id = posts.discussion + WHERE posts.parent != 0 + AND discuss.moodleoverflow = ' . $this->current->id . ';'; + $answerpostscount = $DB->get_records_sql($sql); + $answerpostscount = $answerpostscount[array_key_first($answerpostscount)]->answerposts; + $answersfound = $answerpostscount > 0; + if ($answersfound) { + $warningstring = get_string('limitedanswerwarning_answers', 'moodleoverflow'); + $warningstring .= '
' . get_string('limitedanswerwarning_conclusion', 'moodleoverflow'); + $htmlwarning = html_writer::div($warningstring, 'alert alert-warning', ['role' => 'alert']); + $mform->addElement('html', $htmlwarning); + } + } + + // Limited answer setting elements.. + $mform->addElement('hidden', 'la_answersfound', $answersfound); + $mform->setType('la_answersfound', PARAM_BOOL); + $mform->addElement('date_time_selector', 'la_starttime', get_string('la_starttime', 'moodleoverflow'), + ['optional' => true]); + + $mform->addHelpButton('la_starttime', 'la_starttime', 'moodleoverflow'); + $mform->disabledIf('la_starttime', 'la_answersfound', 'eq', true); + + $mform->addElement('date_time_selector', 'la_endtime', get_string('la_endtime', 'moodleoverflow'), + ['optional' => true]); + + $mform->addHelpButton('la_endtime', 'la_endtime', 'moodleoverflow'); + + $mform->addElement('hidden', 'la_error'); + $mform->setType('la_error', PARAM_TEXT); + // Add standard elements, common to all modules. $this->standard_coursemodule_elements(); @@ -249,4 +293,39 @@ public function data_postprocessing($data) { $data->coursewidereputation = false; } } + + /** + * Validates set data in mod_form + * @param $data + * @param $files + * @return array + * @throws coding_exception + */ + public function validation($data, $files) { + $errors = parent::validation($data, $files); + + // Validate that the limited answer settings. + $currenttime = time(); + $isstarttime = !empty($data['la_starttime']); + $isendtime = !empty($data['la_endtime']); + + if ($isstarttime && $data['la_starttime'] < $currenttime) { + $errors['la_starttime'] = get_string('la_starttime_ruleerror', 'moodleoverflow'); + } + if ($isendtime) { + if ($data['la_endtime'] < $currenttime) { + $errors['la_endtime'] = get_string('la_endtime_ruleerror', 'moodleoverflow'); + } + + if ($isstarttime && $data['la_endtime'] <= $data['la_starttime']) { + if (isset($errors['la_endtime'])) { + $errors['la_endtime'] .= '
' . get_string('la_sequence_error', 'moodleoverflow'); + } else { + $errors['la_endtime'] = get_string('la_sequence_error', 'moodleoverflow'); + } + } + } + + return $errors; + } } diff --git a/post.php b/post.php index 86bf67b23c..23d3f9e586 100644 --- a/post.php +++ b/post.php @@ -219,6 +219,24 @@ $modulecontext = context_module::instance($cm->id); $coursecontext = context_course::instance($course->id); + // Check if Limitedanswertime is on. If so, replies are not possible. + $limitedanswersetting = $DB->get_record('moodleoverflow', ['id' => $moodleoverflow->id], 'la_starttime, la_endtime'); + $lastarttime = $limitedanswersetting->la_starttime; + $laendtime = $limitedanswersetting->la_endtime; + + $roleid = $DB->get_field('role', 'id', ['shortname' => 'editingteacher']); + $iseditteacher = $DB->record_exists('role_assignments', ['userid' => $USER->id, 'roleid' => $roleid]); + + $roleidteacher = $DB->get_field('role', 'id', ['shortname' => 'teacher']); + $isteacher = $DB->record_exists('role_assignments', ['userid' => $USER->id, 'roleid' => $roleidteacher]); + + if (($lastarttime > time() || $laendtime != 0 && $laendtime < time()) && + (!has_capability('mod/moodleoverflow:addinstance', $modulecontext))) { + // Redirect to the moodleoverflow. + $link = new \moodle_url('/mod/moodleoverflow/view.php', ['id' => $cm->id]); + redirect($link); + } + // Check whether the user is allowed to post. if (!moodleoverflow_user_can_post($modulecontext, $parent)) { diff --git a/tests/behat/behat_mod_moodleoverflow.php b/tests/behat/behat_mod_moodleoverflow.php index 9fd2f80806..6f430092f2 100644 --- a/tests/behat/behat_mod_moodleoverflow.php +++ b/tests/behat/behat_mod_moodleoverflow.php @@ -241,4 +241,23 @@ public function should_not_exist_in_the_moodleoverflow_discussion_card($element, $this->getSession() ); } + + /** + * Sets the limited answer starttime attribute of a moodleoverflow to the current time. + * + * @Given I set the :activity moodleoverflow limitedanswerstarttime to now + * @param $activity + * @param $value + * @return void + */ + public function i_set_the_moodleoverflow_limitedanswerstarttime_to_now($activity): void { + global $DB; + + if (!$activityrecord = $DB->get_record('moodleoverflow', ['name' => $activity])) { + throw new Exception("Activity '$activity' not found"); + } + // Update the specified field. + $activityrecord->la_starttime = time(); + $DB->update_record('moodleoverflow', $activityrecord); + } } diff --git a/tests/behat/limitedanswer.feature b/tests/behat/limitedanswer.feature new file mode 100644 index 0000000000..2663591990 --- /dev/null +++ b/tests/behat/limitedanswer.feature @@ -0,0 +1,97 @@ +@mod @mod_moodleoverflow @javascript + Feature: Moodleoverflows can start in a limited answer mode, where answers from + students are not enabled until a set date. + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@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 | + + Scenario: With limited answer mode on, a teacher can answer a post that a student can not. When the teacher changes the + limitedanswer starttime to now, the student can now answer the post. + Given the following "activities" exist: + | activity | name | intro | course | idnumber | la_starttime | + | moodleoverflow | Test Moodleoverflow | Test moodleoverflow description | C1 | 1 | ##now +1 day## | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test Moodleoverflow" + And I add a new discussion to "Test Moodleoverflow" moodleoverflow with: + | Subject | Forum post 1 | + | Message | This is the question message | + And I log out + And I log in as "student1" + And I am on "Course 1" course homepage + And I follow "Test Moodleoverflow" + And I follow "Forum post 1" + And I click on "Answer" "text" + Then I should not see "Your reply" + When I set the "Test Moodleoverflow" moodleoverflow limitedanswerstarttime to now + And I am on "Course 1" course homepage + And I follow "Test Moodleoverflow" + And I follow "Forum post 1" + And I click on "Answer" "text" + Then I should see "Your reply" + And I set the following fields to these values: + | Subject | Re: Forum post 1 | + | Message | This is the answer message | + And I press "Post to forum" + Then I should see "This is the answer message" + And I should see "This is the question message" + + Scenario: Setting up the limited answer mode, the times need to be in the right order + Given the following "activities" exist: + | activity | name | intro | course | idnumber | + | moodleoverflow | Test Moodleoverflow | Test moodleoverflow description | C1 | 1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test Moodleoverflow" + And I navigate to "Settings" in current page administration + And I follow "Limited Answer Mode" + And I click on "la_starttime[enabled]" "checkbox" + And I set the following fields to these values: + | id_la_starttime_day | ##tomorrow##%d## | + | id_la_starttime_month | ##tomorrow##%B## | + | id_la_starttime_year | ##tomorrow##%Y## | + | id_la_starttime_hour | 12 | + | id_la_starttime_minute | 30 | + And I click on "la_endtime[enabled]" "checkbox" + And I set the following fields to these values: + | id_la_endtime_day | ##yesterday##%d## | + | id_la_endtime_month | ##yesterday##%B## | + | id_la_endtime_year | ##yesterday##%Y## | + | id_la_endtime_hour | 12 | + | id_la_endtime_minute | 30 | + When I press "Save and display" + And I follow "Limited Answer Mode" + And I click on "#collapseElement-5" "css_element" + Then I should see "End time must be in the future" + And I should see "The end time must be after the start time" + + Scenario: Setting up the limited answer mode, the start times need to be in the future + Given the following "activities" exist: + | activity | name | intro | course | idnumber | + | moodleoverflow | Test Moodleoverflow | Test moodleoverflow description | C1 | 1 | + And I log in as "teacher1" + And I am on "Course 1" course homepage + And I follow "Test Moodleoverflow" + And I navigate to "Settings" in current page administration + And I follow "Limited Answer Mode" + And I click on "la_starttime[enabled]" "checkbox" + And I set the following fields to these values: + | id_la_starttime_day | ##yesterday##%d## | + | id_la_starttime_month | ##yesterday##%B## | + | id_la_starttime_year | ##yesterday##%Y## | + | id_la_starttime_hour | 12 | + | id_la_starttime_minute | 30 | + When I press "Save and display" + And I follow "Limited Answer Mode" + And I click on "#collapseElement-5" "css_element" + Then I should see "Start time must be in the future" diff --git a/tests/dailymail_test.php b/tests/dailymail_test.php index c546cea291..d0a32a3ee2 100644 --- a/tests/dailymail_test.php +++ b/tests/dailymail_test.php @@ -67,6 +67,7 @@ final class dailymail_test extends \advanced_testcase { * Test setUp. */ public function setUp(): void { + parent::setUp(); $this->resetAfterTest(); set_config('maxeditingtime', -10, 'moodleoverflow'); @@ -88,6 +89,7 @@ public function tearDown(): void { // Clear all caches. \mod_moodleoverflow\subscriptions::reset_moodleoverflow_cache(); \mod_moodleoverflow\subscriptions::reset_discussion_cache(); + parent::tearDown(); } // Helper functions. diff --git a/tests/locallib_test.php b/tests/locallib_test.php index 30745209a5..dbbf58d11c 100644 --- a/tests/locallib_test.php +++ b/tests/locallib_test.php @@ -39,11 +39,13 @@ final class locallib_test extends advanced_testcase { public function setUp(): void { + parent::setUp(); \mod_moodleoverflow\subscriptions::reset_moodleoverflow_cache(); } public function tearDown(): void { \mod_moodleoverflow\subscriptions::reset_moodleoverflow_cache(); + parent::tearDown(); } /** diff --git a/tests/post_test.php b/tests/post_test.php index 2906298ff8..a4727b0b3f 100644 --- a/tests/post_test.php +++ b/tests/post_test.php @@ -61,6 +61,7 @@ final class post_test extends \advanced_testcase { public function setUp(): void { + parent::setUp(); $this->resetAfterTest(); $this->helper_course_set_up(); } @@ -69,6 +70,7 @@ public function tearDown(): void { // Clear all caches. \mod_moodleoverflow\subscriptions::reset_moodleoverflow_cache(); \mod_moodleoverflow\subscriptions::reset_discussion_cache(); + parent::tearDown(); } /** diff --git a/tests/privacy_provider_test.php b/tests/privacy_provider_test.php index a9e7490712..b02612c3e8 100644 --- a/tests/privacy_provider_test.php +++ b/tests/privacy_provider_test.php @@ -54,6 +54,7 @@ final class privacy_provider_test extends \core_privacy\tests\provider_testcase * Test setUp. */ public function setUp(): void { + parent::setUp(); $this->resetAfterTest(true); $this->generator = $this->getDataGenerator()->get_plugin_generator('mod_moodleoverflow'); } diff --git a/tests/ratings_test.php b/tests/ratings_test.php index 479b133674..54c829cee7 100644 --- a/tests/ratings_test.php +++ b/tests/ratings_test.php @@ -39,7 +39,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ final class ratings_test extends \advanced_testcase { - /** @var stdClass a post from the teacher*/ private $post; @@ -65,6 +64,7 @@ final class ratings_test extends \advanced_testcase { * Test setUp. */ public function setUp(): void { + parent::setUp(); $this->resetAfterTest(); $this->helper_course_set_up(); } @@ -76,6 +76,7 @@ public function tearDown(): void { // Clear all caches. subscriptions::reset_moodleoverflow_cache(); subscriptions::reset_discussion_cache(); + parent::tearDown(); } // Begin of test functions. diff --git a/tests/review_test.php b/tests/review_test.php index 261acafba5..4ef36a3f70 100644 --- a/tests/review_test.php +++ b/tests/review_test.php @@ -72,6 +72,7 @@ final class review_test extends \advanced_testcase { * @return void */ protected function setUp(): void { + parent::setUp(); $this->resetAfterTest(); set_config('reviewpossibleaftertime', -10, 'moodleoverflow'); @@ -102,6 +103,7 @@ protected function tearDown(): void { $this->messagesink->clear(); $this->messagesink->close(); unset($this->messagesink); + parent::tearDown(); } /** diff --git a/tests/subscriptions_test.php b/tests/subscriptions_test.php index 26d653b7a9..9e5a76e47e 100644 --- a/tests/subscriptions_test.php +++ b/tests/subscriptions_test.php @@ -44,6 +44,7 @@ final class subscriptions_test extends advanced_testcase { * Test setUp. */ public function setUp(): void { + parent::setUp(); // Clear all caches. subscriptions::reset_moodleoverflow_cache(); subscriptions::reset_discussion_cache(); @@ -56,6 +57,7 @@ public function tearDown(): void { // Clear all caches. subscriptions::reset_moodleoverflow_cache(); subscriptions::reset_discussion_cache(); + parent::tearDown(); } /** diff --git a/tests/userstats_test.php b/tests/userstats_test.php index 037b1db1f6..61a670129c 100644 --- a/tests/userstats_test.php +++ b/tests/userstats_test.php @@ -84,6 +84,7 @@ final class userstats_test extends \advanced_testcase { * Test setUp. */ public function setUp(): void { + parent::setUp(); $this->resetAfterTest(); $this->helper_course_set_up(); } @@ -95,6 +96,7 @@ public function tearDown(): void { // Clear all caches. \mod_moodleoverflow\subscriptions::reset_moodleoverflow_cache(); \mod_moodleoverflow\subscriptions::reset_discussion_cache(); + parent::tearDown(); } // Begin of test functions. diff --git a/version.php b/version.php index ce8bd8157d..18f30005a9 100644 --- a/version.php +++ b/version.php @@ -28,7 +28,7 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'mod_moodleoverflow'; -$plugin->version = 2024082700; +$plugin->version = 2025031200; $plugin->release = 'v4.4-r3'; $plugin->requires = 2022112800; // Requires 4.1+ Moodle version. $plugin->maturity = MATURITY_STABLE;