Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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('addanewtopic', '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('addanewquestion', '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          if ($this->running_javascript()) {
 282              $this->execute('behat_general::i_click_on', [get_string('actionsmenu'), 'link']);
 283          }
 284  
 285          $this->execute('behat_general::assert_page_contains_text', [get_string('subscribe', 'mod_forum')]);
 286  
 287          if ($this->running_javascript()) {
 288              $this->execute('behat_general::i_click_on', [get_string('actionsmenu'), 'link']);
 289          }
 290      }
 291  
 292      /**
 293       * Checks if the user can unsubscribe from the forum.
 294       *
 295       * @Given /^I can unsubscribe from this forum$/
 296       */
 297      public function i_can_unsubscribe_from_this_forum() {
 298          if ($this->running_javascript()) {
 299              $this->execute('behat_general::i_click_on', [get_string('actionsmenu'), 'link']);
 300          }
 301  
 302          $this->execute('behat_general::assert_page_contains_text', [get_string('unsubscribe', 'mod_forum')]);
 303  
 304          if ($this->running_javascript()) {
 305              $this->execute('behat_general::i_click_on', [get_string('actionsmenu'), 'link']);
 306          }
 307      }
 308  
 309      /**
 310       * Subscribes to the forum.
 311       *
 312       * @Given /^I subscribe to this forum$/
 313       */
 314      public function i_subscribe_to_this_forum() {
 315          if ($this->running_javascript()) {
 316              $this->execute('behat_general::i_click_on', [get_string('actionsmenu'), 'link']);
 317          }
 318  
 319          $this->execute('behat_general::click_link', [get_string('subscribe', 'mod_forum')]);
 320      }
 321  
 322      /**
 323       * Unsubscribes from the forum.
 324       *
 325       * @Given /^I unsubscribe from this forum$/
 326       */
 327      public function i_unsubscribe_from_this_forum() {
 328          if ($this->running_javascript()) {
 329              $this->execute('behat_general::i_click_on', [get_string('actionsmenu'), 'link']);
 330          }
 331  
 332          $this->execute('behat_general::click_link', [get_string('unsubscribe', 'mod_forum')]);
 333      }
 334  
 335      /**
 336       * Fetch user ID from its username.
 337       *
 338       * @param string $username The username.
 339       * @return int The user ID.
 340       * @throws Exception
 341       */
 342      protected function get_user_id($username) {
 343          global $DB;
 344  
 345          if (!$userid = $DB->get_field('user', 'id', ['username' => $username])) {
 346              throw new Exception("A user with username '{$username}' does not exist");
 347          }
 348          return $userid;
 349      }
 350  
 351      /**
 352       * Fetch course ID using course name.
 353       *
 354       * @param string $coursename The name of the course.
 355       * @return int The course ID.
 356       * @throws Exception
 357       */
 358      protected function get_course_id(string $coursename): int {
 359          global $DB;
 360  
 361          if (!$courseid = $DB->get_field('course', 'id', ['fullname' => $coursename])) {
 362              throw new Exception("A course with name '{$coursename}' does not exist");
 363          }
 364  
 365          return $courseid;
 366      }
 367  
 368      /**
 369       * Fetch forum ID using forum name.
 370       *
 371       * @param int $courseid The course ID the forum exists within.
 372       * @param string $forumname The name of the forum.
 373       * @return int The forum ID.
 374       * @throws Exception
 375       */
 376      protected function get_forum_id(int $courseid, string $forumname): int {
 377          global $DB;
 378  
 379          $conditions = [
 380              'course' => $courseid,
 381              'name' => $forumname,
 382          ];
 383  
 384          if (!$forumid = $DB->get_field('forum', 'id', $conditions)) {
 385              throw new Exception("A forum with name '{$forumname}' does not exist in the provided course");
 386          }
 387  
 388          return $forumid;
 389      }
 390  
 391      /**
 392       * Fetch Group ID using group name.
 393       *
 394       * @param int $courseid The course ID the forum exists within.
 395       * @param string $groupname The short name of the group.
 396       * @return int The group ID.
 397       * @throws Exception
 398       */
 399      protected function get_group_id(int $courseid, string $groupname): int {
 400          global $DB;
 401  
 402          if ($groupname === 'All participants') {
 403              return -1;
 404          }
 405  
 406          $conditions = [
 407              'courseid' => $courseid,
 408              'idnumber' => $groupname,
 409          ];
 410  
 411          if (!$groupid = $DB->get_field('groups', 'id', $conditions)) {
 412              throw new Exception("A group with name '{$groupname}' does not exist in the provided course");
 413          }
 414  
 415          return $groupid;
 416      }
 417  
 418      /**
 419       * Fetch discussion ID and first post ID by discussion name.
 420       *
 421       * @param int $forumid The forum ID where the discussion resides.
 422       * @param string $name The name of the discussion.
 423       * @return array The discussion ID and first post ID.
 424       * @throws dml_exception If the discussion name is not unique within the forum (or doesn't exist).
 425       */
 426      protected function get_base_discussion(int $forumid, string $name): array {
 427          global $DB;
 428  
 429          $conditions = [
 430              'name' => $name,
 431              'forum' => $forumid,
 432          ];
 433  
 434          $result = $DB->get_record("forum_discussions", $conditions, 'id, firstpost', MUST_EXIST);
 435  
 436          return [
 437              'discussionid' => $result->id,
 438              'parentid' => $result->firstpost,
 439          ];
 440      }
 441  
 442      /**
 443       * Create one or more attached or inline attachments to a forum post.
 444       *
 445       * @param int $postid The ID of the forum post.
 446       * @param int $userid The user ID creating the attachment.
 447       * @param array $attachmentnames Names of all attachments to be created.
 448       * @param stdClass $cm The context module of the forum.
 449       * @param string $filearea The file area being written to, eg 'attachment' or 'post' (inline).
 450       */
 451      protected function create_post_attachments(int $postid, int $userid, array $attachmentnames, stdClass $cm, string $filearea): void {
 452          $filestorage = get_file_storage();
 453  
 454          foreach ($attachmentnames as $attachmentname) {
 455              $filestorage->create_file_from_string(
 456                  [
 457                      'contextid' => context_module::instance($cm->id)->id,
 458                      'component' => 'mod_forum',
 459                      'filearea'  => $filearea,
 460                      'itemid'    => $postid,
 461                      'filepath'  => '/',
 462                      'filename'  => $attachmentname,
 463                      'userid'    => $userid,
 464                  ],
 465                  "File content {$attachmentname}"
 466              );
 467          }
 468      }
 469  
 470      /**
 471       * Returns the steps list to add a new discussion to a forum.
 472       *
 473       * Abstracts add a new topic and add a new discussion, as depending
 474       * on the forum type the button string changes.
 475       *
 476       * @param string $forumname
 477       * @param TableNode $table
 478       * @param string $buttonstr
 479       */
 480      protected function add_new_discussion($forumname, TableNode $table, $buttonstr) {
 481          // Navigate to forum.
 482          $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']);
 483          $this->execute('behat_general::click_link', $buttonstr);
 484          $this->execute('behat_forms::press_button', get_string('showadvancededitor'));
 485  
 486          $this->fill_new_discussion_form($table);
 487      }
 488  
 489      /**
 490       * Returns the steps list to add a new discussion to a forum inline.
 491       *
 492       * Abstracts add a new topic and add a new discussion, as depending
 493       * on the forum type the button string changes.
 494       *
 495       * @param string $forumname
 496       * @param TableNode $table
 497       * @param string $buttonstr
 498       */
 499      protected function add_new_discussion_inline($forumname, TableNode $table, $buttonstr) {
 500          // Navigate to forum.
 501          $this->execute('behat_navigation::i_am_on_page_instance', [$this->escape($forumname), 'forum activity']);
 502          $this->execute('behat_general::click_link', $buttonstr);
 503          $this->fill_new_discussion_form($table);
 504      }
 505  
 506      /**
 507       * Fill in the forum's post form and submit. It assumes you've already navigated and enabled the form for view.
 508       *
 509       * @param TableNode $table
 510       * @throws coding_exception
 511       */
 512      protected function fill_new_discussion_form(TableNode $table) {
 513          // Fill form and post.
 514          $this->execute('behat_forms::i_set_the_following_fields_to_these_values', $table);
 515          $this->execute('behat_forms::press_button', get_string('posttoforum', 'forum'));
 516          $this->execute('behat_general::i_wait_to_be_redirected');
 517      }
 518  
 519      /**
 520       * Go to the default reply to post page.
 521       * This is used instead of navigating through 4-5 different steps and to solve issues where JS would be required to click
 522       * on the advanced button
 523       *
 524       * @param $postsubject
 525       * @throws coding_exception
 526       * @throws dml_exception
 527       * @throws moodle_exception
 528       */
 529      protected function goto_main_post_reply($postsubject) {
 530          global $DB;
 531          $post = $DB->get_record("forum_posts", array("subject" => $postsubject), 'id', MUST_EXIST);
 532          $url = new moodle_url('/mod/forum/post.php', ['reply' => $post->id]);
 533          $this->execute('behat_general::i_visit', [$url]);
 534      }
 535  }