Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Steps definitions related with the forum activity.
  19   *
  20   * @package    mod_forum
  21   * @category   test
  22   * @copyright  2013 David MonllaĆ³
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
  27  
  28  require_once (__DIR__ . '/../../../../lib/behat/behat_base.php');
  29  
  30  use Behat\Gherkin\Node\TableNode as TableNode;
  31  /**
  32   * Forum-related steps definitions.
  33   *
  34   * @package    mod_forum
  35   * @category   test
  36   * @copyright  2013 David MonllaĆ³
  37   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  38   */
  39  class behat_mod_forum extends behat_base {
  40  
  41      /**
  42       * Adds a topic to the forum specified by it's name. Useful for the Announcements and blog-style forums.
  43       *
  44       * @Given /^I add a new topic to "(?P<forum_name_string>(?:[^"]|\\")*)" forum with:$/
  45       * @param string $forumname
  46       * @param TableNode $table
  47       */
  48      public function i_add_a_new_topic_to_forum_with($forumname, TableNode $table) {
  49          $this->add_new_discussion($forumname, $table, get_string('addanewdiscussion', 'forum'));
  50      }
  51  
  52      /**
  53       * Adds a Q&A discussion to the Q&A-type forum specified by it's name with the provided table data.
  54       *
  55       * @Given /^I add a new question to "(?P<forum_name_string>(?:[^"]|\\")*)" forum with:$/
  56       * @param string $forumname
  57       * @param TableNode $table
  58       */
  59      public function i_add_a_new_question_to_forum_with($forumname, TableNode $table) {
  60          $this->add_new_discussion($forumname, $table, get_string('addanewdiscussion', 'forum'));
  61      }
  62  
  63      /**
  64       * Adds a discussion to the forum specified by it's name with the provided table data (usually Subject and Message). The step begins from the forum's course page.
  65       *
  66       * @Given /^I add a new discussion to "(?P<forum_name_string>(?:[^"]|\\")*)" forum with:$/
  67       * @param string $forumname
  68       * @param TableNode $table
  69       */
  70      public function i_add_a_forum_discussion_to_forum_with($forumname, TableNode $table) {
  71          $this->add_new_discussion($forumname, $table, get_string('addanewdiscussion', 'forum'));
  72      }
  73  
  74      /**
  75       * Adds a discussion to the forum specified by it's name with the provided table data (usually Subject and Message).
  76       * The step begins from the forum's course page.
  77       *
  78       * @Given /^I add a new discussion to "(?P<forum_name_string>(?:[^"]|\\")*)" forum inline with:$/
  79       * @param string $forumname
  80       * @param TableNode $table
  81       */
  82      public function i_add_a_forum_discussion_to_forum_inline_with($forumname, TableNode $table) {
  83          $this->add_new_discussion_inline($forumname, $table, get_string('addanewdiscussion', 'forum'));
  84      }
  85  
  86      /**
  87       * Adds a reply to the specified post of the specified forum. The step begins from the forum's page or from the forum's course page.
  88       *
  89       * @Given /^I reply "(?P<post_subject_string>(?:[^"]|\\")*)" post from "(?P<forum_name_string>(?:[^"]|\\")*)" forum with:$/
  90       * @param string $postname The subject of the post
  91       * @param string $forumname The forum name
  92       * @param TableNode $table
  93       */
  94      public function i_reply_post_from_forum_with($postsubject, $forumname, TableNode $table) {
  95  
  96          // Navigate to forum.
  97          $this->goto_main_post_reply($postsubject);
  98  
  99          // Fill form and post.
 100          $this->execute('behat_forms::i_set_the_following_fields_to_these_values', $table);
 101  
 102          $this->execute('behat_forms::press_button', get_string('posttoforum', 'forum'));
 103          $this->execute('behat_general::i_wait_to_be_redirected');
 104      }
 105  
 106      /**
 107       * Inpage Reply - adds a reply to the specified post of the specified forum. The step begins from the forum's page or from the forum's course page.
 108       *
 109       * @Given /^I reply "(?P<post_subject_string>(?:[^"]|\\")*)" post from "(?P<forum_name_string>(?:[^"]|\\")*)" forum using an inpage reply with:$/
 110       * @param string $postsubject The subject of the post
 111       * @param string $forumname The forum name
 112       * @param TableNode $table
 113       */
 114      public function i_reply_post_from_forum_using_an_inpage_reply_with($postsubject, $forumname, TableNode $table) {
 115          // Navigate to forum.
 116          $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']);
 117          $this->execute('behat_general::click_link', $this->escape($postsubject));
 118          $this->execute('behat_general::click_link', get_string('reply', 'forum'));
 119  
 120          // Fill form and post.
 121          $this->execute('behat_forms::i_set_the_following_fields_to_these_values', $table);
 122  
 123          $this->execute('behat_forms::press_button', get_string('posttoforum', 'mod_forum'));
 124      }
 125  
 126      /**
 127       * Navigates to a particular discussion page
 128       *
 129       * @Given /^I navigate to post "(?P<post_subject_string>(?:[^"]|\\")*)" in "(?P<forum_name_string>(?:[^"]|\\")*)" forum$/
 130       * @param string $postsubject The subject of the post
 131       * @param string $forumname The forum name
 132       */
 133      public function i_navigate_to_post_in_forum($postsubject, $forumname) {
 134          // Navigate to forum discussion.
 135          $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']);
 136          $this->execute('behat_general::click_link', $this->escape($postsubject));
 137      }
 138  
 139      /**
 140       * Opens up the action menu for the discussion
 141       *
 142       * @Given /^I click on "(?P<post_subject_string>(?:[^"]|\\")*)" action menu$/
 143       * @param string $discussion The subject of the discussion
 144       */
 145      public function i_click_on_action_menu($discussion) {
 146          $this->execute('behat_general::i_click_on_in_the', [
 147              "[data-container='discussion-tools'] [data-toggle='dropdown']", "css_element",
 148              "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ') and contains(.,'$discussion')]",
 149              "xpath_element"
 150          ]);
 151      }
 152  
 153      /**
 154       * Creates new discussions within forums of a given course.
 155       *
 156       * @Given the following forum discussions exist in course :coursename:
 157       * @param string $coursename The full name of the course where the forums exist.
 158       * @param TableNode $discussionsdata The discussion posts to be created.
 159       */
 160      public function the_following_forum_discussions_exist(string $coursename, TableNode $discussionsdata) {
 161          global $DB;
 162  
 163          $courseid = $this->get_course_id($coursename);
 164          $forumgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_forum');
 165  
 166          // Add the discussions to the relevant forum.
 167          foreach ($discussionsdata->getHash() as $discussioninfo) {
 168              $discussioninfo['course'] = $courseid;
 169              $discussioninfo['forum'] = $this->get_forum_id($courseid, $discussioninfo['forum']);
 170              $discussioninfo['userid'] = $this->get_user_id($discussioninfo['user']);
 171  
 172              // Prepare data for any attachments.
 173              if (!empty($discussioninfo['attachments']) || !empty($discussioninfo['inlineattachments'])) {
 174                  $discussioninfo['attachment'] = 1;
 175                  $cm = get_coursemodule_from_instance('forum', $discussioninfo['forum']);
 176              }
 177  
 178              // Prepare data for groups if needed.
 179              if (!empty($discussioninfo['group'])) {
 180                  $discussioninfo['groupid'] = $this->get_group_id($courseid, $discussioninfo['group']);
 181                  unset($discussioninfo['group']);
 182              }
 183  
 184              // Create the discussion post.
 185              $discussion = $forumgenerator->create_discussion($discussioninfo);
 186              $postid = $DB->get_field('forum_posts', 'id', ['discussion' => $discussion->id]);
 187  
 188              // Override the creation and modified timestamps as required.
 189              if (!empty($discussioninfo['created']) || !empty($discussioninfo['modified'])) {
 190                  $discussiondata = [
 191                      'id' => $discussion->id,
 192                      'timemodified' => empty($discussioninfo['modified']) ? $discussioninfo['created'] : $discussioninfo['modified'],
 193                  ];
 194  
 195                  $DB->update_record('forum_discussions', $discussiondata);
 196  
 197                  $postdata = [
 198                      'id' => $postid,
 199                      'modified' => empty($discussioninfo['modified']) ? $discussioninfo['created'] : $discussioninfo['modified'],
 200                  ];
 201  
 202                  if (!empty($discussioninfo['created'])) {
 203                      $postdata['created'] = $discussioninfo['created'];
 204                  }
 205  
 206                  $DB->update_record('forum_posts', $postdata);
 207              }
 208  
 209              // Create attachments to the discussion post if required.
 210              if (!empty($discussioninfo['attachments'])) {
 211                  $attachments = array_map('trim', explode(',', $discussioninfo['attachments']));
 212                  $this->create_post_attachments($postid, $discussioninfo['userid'], $attachments, $cm, 'attachment');
 213              }
 214  
 215              // Create inline attachments to the discussion post if required.
 216              if (!empty($discussioninfo['inlineattachments'])) {
 217                  $inlineattachments = array_map('trim', explode(',', $discussioninfo['inlineattachments']));
 218                  $this->create_post_attachments($postid, $discussioninfo['userid'], $inlineattachments, $cm, 'post');
 219              }
 220          }
 221      }
 222  
 223      /**
 224       * Creates replies to discussions within forums of a given course.
 225       *
 226       * @Given the following forum replies exist in course :coursename:
 227       * @param string $coursename The full name of the course where the forums exist.
 228       * @param TableNode $repliesdata The reply posts to be created.
 229       */
 230      public function the_following_forum_replies_exist(string $coursename, TableNode $repliesdata) {
 231          global $DB;
 232  
 233          $courseid = $this->get_course_id($coursename);
 234          $forumgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_forum');
 235  
 236          // Add the replies to the relevant discussions.
 237          foreach ($repliesdata->getHash() as $replyinfo) {
 238              $replyinfo['course'] = $courseid;
 239              $replyinfo['forum'] = $this->get_forum_id($courseid, $replyinfo['forum']);
 240              $replyinfo['userid'] = $this->get_user_id($replyinfo['user']);
 241  
 242              [
 243                  'discussionid' => $replyinfo['discussion'],
 244                  'parentid' => $replyinfo['parent'],
 245              ] = $this->get_base_discussion($replyinfo['forum'], $replyinfo['discussion']);
 246  
 247              // Prepare data for any attachments.
 248              if (!empty($replyinfo['attachments']) || !empty($replyinfo['inlineattachments'])) {
 249                  $replyinfo['attachment'] = 1;
 250                  $cm = get_coursemodule_from_instance('forum', $replyinfo['forum']);
 251              }
 252  
 253              // Get the user id of the user to whom the reply is private.
 254              if (!empty($replyinfo['privatereplyto'])) {
 255                  $replyinfo['privatereplyto'] = $this->get_user_id($replyinfo['privatereplyto']);
 256              }
 257  
 258              // Create the reply post.
 259              $reply = $forumgenerator->create_post($replyinfo);
 260  
 261              // Create attachments to the post if required.
 262              if (!empty($replyinfo['attachments'])) {
 263                  $attachments = array_map('trim', explode(',', $replyinfo['attachments']));
 264                  $this->create_post_attachments($reply->id, $replyinfo['userid'], $attachments, $cm, 'attachment');
 265              }
 266  
 267              // Create inline attachments to the post if required.
 268              if (!empty($replyinfo['inlineattachments'])) {
 269                  $inlineattachments = array_map('trim', explode(',', $replyinfo['inlineattachments']));
 270                  $this->create_post_attachments($reply->id, $replyinfo['userid'], $inlineattachments, $cm, 'post');
 271              }
 272          }
 273      }
 274  
 275      /**
 276       * Checks if the user can subscribe to the forum.
 277       *
 278       * @Given /^I can subscribe to this forum$/
 279       */
 280      public function i_can_subscribe_to_this_forum() {
 281          $this->execute('behat_general::assert_page_contains_text', [get_string('subscribe', 'mod_forum')]);
 282      }
 283  
 284      /**
 285       * Checks if the user can unsubscribe from the forum.
 286       *
 287       * @Given /^I can unsubscribe from this forum$/
 288       */
 289      public function i_can_unsubscribe_from_this_forum() {
 290          $this->execute('behat_general::assert_page_contains_text', [get_string('unsubscribe', 'mod_forum')]);
 291      }
 292  
 293      /**
 294       * Subscribes to the forum.
 295       *
 296       * @Given /^I subscribe to this forum$/
 297       */
 298      public function i_subscribe_to_this_forum() {
 299          $this->execute('behat_general::click_link', [get_string('subscribe', 'mod_forum')]);
 300      }
 301  
 302      /**
 303       * Unsubscribes from the forum.
 304       *
 305       * @Given /^I unsubscribe from this forum$/
 306       */
 307      public function i_unsubscribe_from_this_forum() {
 308          $this->execute('behat_general::click_link', [get_string('unsubscribe', 'mod_forum')]);
 309      }
 310  
 311      /**
 312       * Fetch user ID from its username.
 313       *
 314       * @param string $username The username.
 315       * @return int The user ID.
 316       * @throws Exception
 317       */
 318      protected function get_user_id($username) {
 319          global $DB;
 320  
 321          if (!$userid = $DB->get_field('user', 'id', ['username' => $username])) {
 322              throw new Exception("A user with username '{$username}' does not exist");
 323          }
 324          return $userid;
 325      }
 326  
 327      /**
 328       * Fetch course ID using course name.
 329       *
 330       * @param string $coursename The name of the course.
 331       * @return int The course ID.
 332       * @throws Exception
 333       */
 334      protected function get_course_id(string $coursename): int {
 335          global $DB;
 336  
 337          if (!$courseid = $DB->get_field('course', 'id', ['fullname' => $coursename])) {
 338              throw new Exception("A course with name '{$coursename}' does not exist");
 339          }
 340  
 341          return $courseid;
 342      }
 343  
 344      /**
 345       * Fetch forum ID using forum name.
 346       *
 347       * @param int $courseid The course ID the forum exists within.
 348       * @param string $forumname The name of the forum.
 349       * @return int The forum ID.
 350       * @throws Exception
 351       */
 352      protected function get_forum_id(int $courseid, string $forumname): int {
 353          global $DB;
 354  
 355          $conditions = [
 356              'course' => $courseid,
 357              'name' => $forumname,
 358          ];
 359  
 360          if (!$forumid = $DB->get_field('forum', 'id', $conditions)) {
 361              throw new Exception("A forum with name '{$forumname}' does not exist in the provided course");
 362          }
 363  
 364          return $forumid;
 365      }
 366  
 367      /**
 368       * Fetch Group ID using group name.
 369       *
 370       * @param int $courseid The course ID the forum exists within.
 371       * @param string $groupname The short name of the group.
 372       * @return int The group ID.
 373       * @throws Exception
 374       */
 375      protected function get_group_id(int $courseid, string $groupname): int {
 376          global $DB;
 377  
 378          if ($groupname === 'All participants') {
 379              return -1;
 380          }
 381  
 382          $conditions = [
 383              'courseid' => $courseid,
 384              'idnumber' => $groupname,
 385          ];
 386  
 387          if (!$groupid = $DB->get_field('groups', 'id', $conditions)) {
 388              throw new Exception("A group with name '{$groupname}' does not exist in the provided course");
 389          }
 390  
 391          return $groupid;
 392      }
 393  
 394      /**
 395       * Fetch discussion ID and first post ID by discussion name.
 396       *
 397       * @param int $forumid The forum ID where the discussion resides.
 398       * @param string $name The name of the discussion.
 399       * @return array The discussion ID and first post ID.
 400       * @throws dml_exception If the discussion name is not unique within the forum (or doesn't exist).
 401       */
 402      protected function get_base_discussion(int $forumid, string $name): array {
 403          global $DB;
 404  
 405          $conditions = [
 406              'name' => $name,
 407              'forum' => $forumid,
 408          ];
 409  
 410          $result = $DB->get_record("forum_discussions", $conditions, 'id, firstpost', MUST_EXIST);
 411  
 412          return [
 413              'discussionid' => $result->id,
 414              'parentid' => $result->firstpost,
 415          ];
 416      }
 417  
 418      /**
 419       * Create one or more attached or inline attachments to a forum post.
 420       *
 421       * @param int $postid The ID of the forum post.
 422       * @param int $userid The user ID creating the attachment.
 423       * @param array $attachmentnames Names of all attachments to be created.
 424       * @param stdClass $cm The context module of the forum.
 425       * @param string $filearea The file area being written to, eg 'attachment' or 'post' (inline).
 426       */
 427      protected function create_post_attachments(int $postid, int $userid, array $attachmentnames, stdClass $cm, string $filearea): void {
 428          $filestorage = get_file_storage();
 429  
 430          foreach ($attachmentnames as $attachmentname) {
 431              $filestorage->create_file_from_string(
 432                  [
 433                      'contextid' => context_module::instance($cm->id)->id,
 434                      'component' => 'mod_forum',
 435                      'filearea'  => $filearea,
 436                      'itemid'    => $postid,
 437                      'filepath'  => '/',
 438                      'filename'  => $attachmentname,
 439                      'userid'    => $userid,
 440                  ],
 441                  "File content {$attachmentname}"
 442              );
 443          }
 444      }
 445  
 446      /**
 447       * Returns the steps list to add a new discussion to a forum.
 448       *
 449       * Abstracts add a new topic and add a new discussion, as depending
 450       * on the forum type the button string changes.
 451       *
 452       * @param string $forumname
 453       * @param TableNode $table
 454       * @param string $buttonstr
 455       */
 456      protected function add_new_discussion($forumname, TableNode $table, $buttonstr) {
 457          // Navigate to forum.
 458          $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']);
 459          $this->execute('behat_general::click_link', $buttonstr);
 460          $this->execute('behat_forms::press_button', get_string('showadvancededitor'));
 461  
 462          $this->fill_new_discussion_form($table);
 463      }
 464  
 465      /**
 466       * Returns the steps list to add a new discussion to a forum inline.
 467       *
 468       * Abstracts add a new topic and add a new discussion, as depending
 469       * on the forum type the button string changes.
 470       *
 471       * @param string $forumname
 472       * @param TableNode $table
 473       * @param string $buttonstr
 474       */
 475      protected function add_new_discussion_inline($forumname, TableNode $table, $buttonstr) {
 476          // Navigate to forum.
 477          $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']);
 478          $this->execute('behat_general::click_link', $buttonstr);
 479          $this->fill_new_discussion_form($table);
 480      }
 481  
 482      /**
 483       * Fill in the forum's post form and submit. It assumes you've already navigated and enabled the form for view.
 484       *
 485       * @param TableNode $table
 486       * @throws coding_exception
 487       */
 488      protected function fill_new_discussion_form(TableNode $table) {
 489          // Fill form and post.
 490          $this->execute('behat_forms::i_set_the_following_fields_to_these_values', $table);
 491          $this->execute('behat_forms::press_button', get_string('posttoforum', 'forum'));
 492          $this->execute('behat_general::i_wait_to_be_redirected');
 493      }
 494  
 495      /**
 496       * Go to the default reply to post page.
 497       * This is used instead of navigating through 4-5 different steps and to solve issues where JS would be required to click
 498       * on the advanced button
 499       *
 500       * @param $postsubject
 501       * @throws coding_exception
 502       * @throws dml_exception
 503       * @throws moodle_exception
 504       */
 505      protected function goto_main_post_reply($postsubject) {
 506          global $DB;
 507          $post = $DB->get_record("forum_posts", array("subject" => $postsubject), 'id', MUST_EXIST);
 508          $url = new moodle_url('/mod/forum/post.php', ['reply' => $post->id]);
 509          $this->execute('behat_general::i_visit', [$url]);
 510      }
 511  }