MDL-66299 mod_forum: Created new behat steps for bulk post creation
[moodle.git] / mod / forum / tests / behat / behat_mod_forum.php
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/>.
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  */
26 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
28 require_once(__DIR__ . '/../../../../lib/behat/behat_base.php');
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 {
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     }
52     /**
53      * 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.
54      *
55      * @Given /^I add a new discussion to "(?P<forum_name_string>(?:[^"]|\\")*)" forum with:$/
56      * @param string $forumname
57      * @param TableNode $table
58      */
59     public function i_add_a_forum_discussion_to_forum_with($forumname, TableNode $table) {
60         $this->add_new_discussion($forumname, $table, get_string('addanewdiscussion', 'forum'));
61     }
63     /**
64      * Adds a discussion to the forum specified by it's name with the provided table data (usually Subject and Message).
65      * The step begins from the forum's course page.
66      *
67      * @Given /^I add a new discussion to "(?P<forum_name_string>(?:[^"]|\\")*)" forum inline with:$/
68      * @param string $forumname
69      * @param TableNode $table
70      */
71     public function i_add_a_forum_discussion_to_forum_inline_with($forumname, TableNode $table) {
72         $this->add_new_discussion_inline($forumname, $table, get_string('addanewdiscussion', 'forum'));
73     }
75     /**
76      * 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.
77      *
78      * @Given /^I reply "(?P<post_subject_string>(?:[^"]|\\")*)" post from "(?P<forum_name_string>(?:[^"]|\\")*)" forum with:$/
79      * @param string $postname The subject of the post
80      * @param string $forumname The forum name
81      * @param TableNode $table
82      */
83     public function i_reply_post_from_forum_with($postsubject, $forumname, TableNode $table) {
85         // Navigate to forum.
86         $this->goto_main_post_reply($postsubject);
88         // Fill form and post.
89         $this->execute('behat_forms::i_set_the_following_fields_to_these_values', $table);
91         $this->execute('behat_forms::press_button', get_string('posttoforum', 'forum'));
92         $this->execute('behat_general::i_wait_to_be_redirected');
93     }
95     /**
96      * 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.
97      *
98      * @Given /^I reply "(?P<post_subject_string>(?:[^"]|\\")*)" post from "(?P<forum_name_string>(?:[^"]|\\")*)" forum using an inpage reply with:$/
99      * @param string $postsubject The subject of the post
100      * @param string $forumname The forum name
101      * @param TableNode $table
102      */
103     public function i_reply_post_from_forum_using_an_inpage_reply_with($postsubject, $forumname, TableNode $table) {
105         // Navigate to forum.
106         $this->execute('behat_general::click_link', $this->escape($forumname));
107         $this->execute('behat_general::click_link', $this->escape($postsubject));
108         $this->execute('behat_general::click_link', get_string('reply', 'forum'));
110         // Fill form and post.
111         $this->execute('behat_forms::i_set_the_following_fields_to_these_values', $table);
113         $this->execute('behat_forms::press_button', get_string('submit', 'core'));
114     }
116     /**
117      * Navigates to a particular discussion page
118      *
119      * @Given /^I navigate to post "(?P<post_subject_string>(?:[^"]|\\")*)" in "(?P<forum_name_string>(?:[^"]|\\")*)" forum$/
120      * @param string $postsubject The subject of the post
121      * @param string $forumname The forum name
122      */
123     public function i_navigate_to_post_in_forum($postsubject, $forumname) {
125         // Navigate to forum discussion.
126         $this->execute('behat_general::click_link', $this->escape($forumname));
127         $this->execute('behat_general::click_link', $this->escape($postsubject));
128     }
130     /**
131      * Opens up the action menu for the discussion
132      *
133      * @Given /^I click on "(?P<post_subject_string>(?:[^"]|\\")*)" action menu$/
134      * @param string $discussion The subject of the discussion
135      */
136     public function i_click_on_action_menu($discussion) {
137         $this->execute('behat_general::i_click_on_in_the', [
138             "[data-container='discussion-tools'] [data-toggle='dropdown']", "css_element",
139             "//tr[contains(concat(' ', normalize-space(@class), ' '), ' discussion ') and contains(.,'$discussion')]",
140             "xpath_element"
141         ]);
142     }
144     /**
145      * Creates new discussions within forums of a given course.
146      *
147      * @Given the following forum discussions exist in course :coursename:
148      * @param string $coursename The full name of the course where the forums exist.
149      * @param TableNode $discussionsdata The discussion posts to be created.
150      */
151     public function the_following_forum_discussions_exist(string $coursename, TableNode $discussionsdata) {
152         global $DB;
154         $courseid = $this->get_course_id($coursename);
155         $forumgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_forum');
157         // Add the discussions to the relevant forum.
158         foreach ($discussionsdata->getHash() as $discussioninfo) {
159             $discussioninfo['course'] = $courseid;
160             $discussioninfo['forum'] = $this->get_forum_id($courseid, $discussioninfo['forum']);
161             $discussioninfo['userid'] = $this->get_user_id($discussioninfo['user']);
163             // Prepare data for any attachments.
164             if (!empty($discussioninfo['attachments']) || !empty($discussioninfo['inlineattachments'])) {
165                 $discussioninfo['attachment'] = 1;
166                 $cm = get_coursemodule_from_instance('forum', $discussioninfo['forum']);
167             }
169             // Create the discussion post.
170             $discussion = $forumgenerator->create_discussion($discussioninfo);
171             $postid = $DB->get_field('forum_posts', 'id', ['discussion' => $discussion->id]);
173             // Override the creation and modified timestamps as required.
174             if (!empty($discussioninfo['created']) || !empty($discussioninfo['modified'])) {
175                 $discussiondata = [
176                     'id' => $discussion->id,
177                     'timemodified' => empty($discussioninfo['modified']) ? $discussioninfo['created'] : $discussioninfo['modified'],
178                 ];
180                 $DB->update_record('forum_discussions', $discussiondata);
182                 $postdata = [
183                     'id' => $postid,
184                     'modified' => empty($discussioninfo['modified']) ? $discussioninfo['created'] : $discussioninfo['modified'],
185                 ];
187                 if (!empty($discussioninfo['created'])) {
188                     $postdata['created'] = $discussioninfo['created'];
189                 }
191                 $DB->update_record('forum_posts', $postdata);
192             }
194             // Create attachments to the discussion post if required.
195             if (!empty($discussioninfo['attachments'])) {
196                 $attachments = array_map('trim', explode(',', $discussioninfo['attachments']));
197                 $this->create_post_attachments($postid, $discussioninfo['userid'], $attachments, $cm, 'attachment');
198             }
200             // Create inline attachments to the discussion post if required.
201             if (!empty($discussioninfo['inlineattachments'])) {
202                 $inlineattachments = array_map('trim', explode(',', $discussioninfo['inlineattachments']));
203                 $this->create_post_attachments($postid, $discussioninfo['userid'], $inlineattachments, $cm, 'post');
204             }
205         }
206     }
208     /**
209      * Creates replies to discussions within forums of a given course.
210      *
211      * @Given the following forum replies exist in course :coursename:
212      * @param string $coursename The full name of the course where the forums exist.
213      * @param TableNode $repliesdata The reply posts to be created.
214      */
215     public function the_following_forum_replies_exist(string $coursename, TableNode $repliesdata) {
216         global $DB;
218         $courseid = $this->get_course_id($coursename);
219         $forumgenerator = behat_util::get_data_generator()->get_plugin_generator('mod_forum');
221         // Add the replies to the relevant discussions.
222         foreach ($repliesdata->getHash() as $replyinfo) {
223             $replyinfo['course'] = $courseid;
224             $replyinfo['forum'] = $this->get_forum_id($courseid, $replyinfo['forum']);
225             $replyinfo['userid'] = $this->get_user_id($replyinfo['user']);
227             [
228                 'discussionid' => $replyinfo['discussion'],
229                 'parentid' => $replyinfo['parent'],
230             ] = $this->get_base_discussion($replyinfo['forum'], $replyinfo['discussion']);
232             // Prepare data for any attachments.
233             if (!empty($replyinfo['attachments']) || !empty($replyinfo['inlineattachments'])) {
234                 $replyinfo['attachment'] = 1;
235                 $cm = get_coursemodule_from_instance('forum', $replyinfo['forum']);
236             }
238             // Create the reply post.
239             $reply = $forumgenerator->create_post($replyinfo);
241             // Create attachments to the post if required.
242             if (!empty($replyinfo['attachments'])) {
243                 $attachments = array_map('trim', explode(',', $replyinfo['attachments']));
244                 $this->create_post_attachments($reply->id, $replyinfo['userid'], $attachments, $cm, 'attachment');
245             }
247             // Create inline attachments to the post if required.
248             if (!empty($replyinfo['inlineattachments'])) {
249                 $inlineattachments = array_map('trim', explode(',', $replyinfo['inlineattachments']));
250                 $this->create_post_attachments($reply->id, $replyinfo['userid'], $inlineattachments, $cm, 'post');
251             }
252         }
253     }
255     /**
256      * Fetch user ID from its username.
257      *
258      * @param string $username The username.
259      * @return int The user ID.
260      * @throws Exception
261      */
262     protected function get_user_id($username) {
263         global $DB;
265         if (!$userid = $DB->get_field('user', 'id', ['username' => $username])) {
266             throw new Exception("A user with username '{$username}' does not exist");
267         }
268         return $userid;
269     }
271     /**
272      * Fetch course ID using course name.
273      *
274      * @param string $coursename The name of the course.
275      * @return int The course ID.
276      * @throws Exception
277      */
278     protected function get_course_id(string $coursename): int {
279         global $DB;
281         if (!$courseid = $DB->get_field('course', 'id', ['fullname' => $coursename])) {
282             throw new Exception("A course with name '{$coursename}' does not exist");
283         }
285         return $courseid;
286     }
288     /**
289      * Fetch forum ID using forum name.
290      *
291      * @param int $courseid The course ID the forum exists within.
292      * @param string $forumname The name of the forum.
293      * @return int The forum ID.
294      * @throws Exception
295      */
296     protected function get_forum_id(int $courseid, string $forumname): int {
297         global $DB;
299         $conditions = [
300             'course' => $courseid,
301             'name' => $forumname,
302         ];
304         if (!$forumid = $DB->get_field('forum', 'id', $conditions)) {
305             throw new Exception("A forum with name '{$forumname}' does not exist in the provided course");
306         }
308         return $forumid;
309     }
311     /**
312      * Fetch discussion ID and first post ID by discussion name.
313      *
314      * @param int $forumid The forum ID where the discussion resides.
315      * @param string $name The name of the discussion.
316      * @return array The discussion ID and first post ID.
317      * @throws dml_exception If the discussion name is not unique within the forum (or doesn't exist).
318      */
319     protected function get_base_discussion(int $forumid, string $name): array {
320         global $DB;
322         $conditions = [
323             'name' => $name,
324             'forum' => $forumid,
325         ];
327         $result = $DB->get_record("forum_discussions", $conditions, 'id, firstpost', MUST_EXIST);
329         return [
330             'discussionid' => $result->id,
331             'parentid' => $result->firstpost,
332         ];
333     }
335     /**
336      * Create one or more attached or inline attachments to a forum post.
337      *
338      * @param int $postid The ID of the forum post.
339      * @param int $userid The user ID creating the attachment.
340      * @param array $attachmentnames Names of all attachments to be created.
341      * @param stdClass $cm The context module of the forum.
342      * @param string $filearea The file area being written to, eg 'attachment' or 'post' (inline).
343      */
344     protected function create_post_attachments(int $postid, int $userid, array $attachmentnames, stdClass $cm, string $filearea): void {
345         $filestorage = get_file_storage();
347         foreach ($attachmentnames as $attachmentname) {
348             $filestorage->create_file_from_string(
349                 [
350                     'contextid' => context_module::instance($cm->id)->id,
351                     'component' => 'mod_forum',
352                     'filearea'  => $filearea,
353                     'itemid'    => $postid,
354                     'filepath'  => '/',
355                     'filename'  => $attachmentname,
356                     'userid'    => $userid,
357                 ],
358                 "File content {$attachmentname}"
359             );
360         }
361     }
363     /**
364      * Returns the steps list to add a new discussion to a forum.
365      *
366      * Abstracts add a new topic and add a new discussion, as depending
367      * on the forum type the button string changes.
368      *
369      * @param string $forumname
370      * @param TableNode $table
371      * @param string $buttonstr
372      */
373     protected function add_new_discussion($forumname, TableNode $table, $buttonstr) {
375         // Navigate to forum.
376         $this->execute('behat_general::click_link', $this->escape($forumname));
377         $this->execute('behat_general::click_link', $buttonstr);
378         $this->execute('behat_forms::press_button', get_string('advanced'));
380         $this->fill_new_discussion_form($table);
381     }
383     /**
384      * Returns the steps list to add a new discussion to a forum inline.
385      *
386      * Abstracts add a new topic and add a new discussion, as depending
387      * on the forum type the button string changes.
388      *
389      * @param string $forumname
390      * @param TableNode $table
391      * @param string $buttonstr
392      */
393     protected function add_new_discussion_inline($forumname, TableNode $table, $buttonstr) {
395         // Navigate to forum.
396         $this->execute('behat_general::click_link', $this->escape($forumname));
397         $this->execute('behat_general::click_link', $buttonstr);
398         $this->fill_new_discussion_form($table);
399     }
401     /**
402      * Fill in the forum's post form and submit. It assumes you've already navigated and enabled the form for view.
403      *
404      * @param TableNode $table
405      * @throws coding_exception
406      */
407     protected function fill_new_discussion_form(TableNode $table) {
408         // Fill form and post.
409         $this->execute('behat_forms::i_set_the_following_fields_to_these_values', $table);
410         $this->execute('behat_forms::press_button', get_string('posttoforum', 'forum'));
411         $this->execute('behat_general::i_wait_to_be_redirected');
412     }
414     /**
415      * Go to the default reply to post page.
416      * This is used instead of navigating through 4-5 different steps and to solve issues where JS would be required to click
417      * on the advanced button
418      *
419      * @param $postsubject
420      * @throws coding_exception
421      * @throws dml_exception
422      * @throws moodle_exception
423      */
424     protected function goto_main_post_reply($postsubject) {
425         global $DB;
426         $post = $DB->get_record("forum_posts", array("subject" => $postsubject), 'id', MUST_EXIST);
427         $url = new moodle_url('/mod/forum/post.php', ['reply' => $post->id]);
428         $this->getSession()->visit($this->locate_path($url->out_as_local_url(false)));
429     }