Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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