MDL-46929 mod_forum: Implement tagging
authorAndrew Hancox <andrewdchancox@googlemail.com>
Fri, 24 Mar 2017 15:41:07 +0000 (15:41 +0000)
committerAndrew Hancox <andrewdchancox@googlemail.com>
Tue, 11 Apr 2017 07:30:09 +0000 (08:30 +0100)
20 files changed:
lib/searchlib.php
mod/forum/backup/moodle2/backup_forum_stepslib.php
mod/forum/backup/moodle2/restore_forum_stepslib.php
mod/forum/classes/output/big_search_form.php
mod/forum/classes/post_form.php
mod/forum/db/tag.php [new file with mode: 0644]
mod/forum/lang/en/forum.php
mod/forum/lib.php
mod/forum/locallib.php
mod/forum/post.php
mod/forum/search.php
mod/forum/templates/big_search_form.mustache
mod/forum/tests/behat/advanced_search.feature
mod/forum/tests/behat/behat_mod_forum.php
mod/forum/tests/behat/edit_tags.feature [new file with mode: 0644]
mod/forum/tests/generator/lib.php
mod/forum/tests/generator_test.php
mod/forum/tests/lib_test.php
mod/forum/version.php
theme/boost/templates/mod_forum/big_search_form.mustache

index 1393cd4..7921783 100644 (file)
@@ -38,6 +38,7 @@ define("TOKEN_USERID","5");
 define("TOKEN_DATEFROM","6");
 define("TOKEN_DATETO","7");
 define("TOKEN_INSTANCE","8");
+define("TOKEN_TAGS","9");
 
 /**
  * Class to hold token/value pairs after they're parsed.
@@ -110,6 +111,14 @@ class search_lexer extends Lexer{
     $this->addExitPattern("\s","indatefrom");
 
 
+    // If we see the string tags: while in the base accept state, start
+    // parsing tags and go to the intags state.
+    $this->addEntryPattern("tags:\S+","accept","intags");
+
+    // Snarf everything into the tags until we see whitespace, then exit
+    // back to the base accept state.
+    $this->addExitPattern("\s","intags");
+
     // Patterns to handle strings  of the form dateto:foo
 
     // If we see the string dateto: while in the base accept state, start
@@ -268,6 +277,17 @@ class search_parser {
         return true;
     }
 
+    // State for handling tags:tagname,tagname constructs. Potentially emits a token.
+    function intags($content){
+        if (strlen($content) < 5) { // State exit or missing parameter.
+            return true;
+        }
+        // Strip off the tags: part and add the reminder to the parsed token array
+        $param = trim(substr($content,5));
+        $this->tokens[] = new search_token(TOKEN_TAGS,$param);
+        return true;
+    }
+
     // State for handling instance:foo constructs. Potentially emits a token.
     function ininstance($content){
         if (strlen($content) < 10) { // State exit or missing parameter.
@@ -390,7 +410,8 @@ function search_generate_text_SQL($parsetree, $datafield, $metafield, $mainidfie
  * @global object
  */
 function search_generate_SQL($parsetree, $datafield, $metafield, $mainidfield, $useridfield,
-                             $userfirstnamefield, $userlastnamefield, $timefield, $instancefield) {
+                             $userfirstnamefield, $userlastnamefield, $timefield, $instancefield,
+                             $tagfields = []) {
     global $CFG, $DB;
     static $p = 0;
 
@@ -407,7 +428,7 @@ function search_generate_SQL($parsetree, $datafield, $metafield, $mainidfield, $
     }
 
     $SQLString = '';
-
+    $nexttagfield = 0;
     for ($i=0; $i<$ntokens; $i++){
         if ($i > 0) {// We have more than one clause, need to tack on AND
             $SQLString .= ' AND ';
@@ -465,6 +486,19 @@ function search_generate_SQL($parsetree, $datafield, $metafield, $mainidfield, $
                 $SQLString .= "($timefield >= :$name1)";
                 $params[$name1] =  $value;
                 break;
+            case TOKEN_TAGS:
+                $sqlstrings = [];
+                foreach (explode(',', $value) as $tag) {
+                    $paramname = $name1 . '_' . $nexttagfield;
+                    if (!isset($tagfields[$nexttagfield])) {
+                        throw new coding_exception('Not enough tag fields supplied for search.');
+                    }
+                    $sqlstrings[] = "($tagfields[$nexttagfield] = :$paramname)";
+                    $params[$paramname] = $tag;
+                    $nexttagfield++;
+                }
+                $SQLString .= implode(' AND ', $sqlstrings);
+                break;
             case TOKEN_NEGATE:
                 $SQLString .= "(NOT ((".$DB->sql_like($datafield, ":$name1", false).") OR (".$DB->sql_like($metafield, ":$name2", false).")))";
                 $params[$name1] =  "%$value%";
index c01e33a..50a6b07 100644 (file)
@@ -60,6 +60,9 @@ class backup_forum_activity_structure_step extends backup_activity_structure_ste
             'mailed', 'subject', 'message', 'messageformat',
             'messagetrust', 'attachment', 'totalscore', 'mailnow'));
 
+        $tags = new backup_nested_element('tags');
+        $tag = new backup_nested_element('tag', array('id'), array('name', 'rawname'));
+
         $ratings = new backup_nested_element('ratings');
 
         $rating = new backup_nested_element('rating', array('id'), array(
@@ -113,6 +116,9 @@ class backup_forum_activity_structure_step extends backup_activity_structure_ste
         $discussion->add_child($posts);
         $posts->add_child($post);
 
+        $posts->add_child($tags);
+        $tags->add_child($tag);
+
         $post->add_child($ratings);
         $ratings->add_child($rating);
 
@@ -147,6 +153,16 @@ class backup_forum_activity_structure_step extends backup_activity_structure_ste
                                                       'ratingarea' => backup_helper::is_sqlparam('post'),
                                                       'itemid'     => backup::VAR_PARENTID));
             $rating->set_source_alias('rating', 'value');
+
+            $tag->set_source_sql('SELECT t.id, t.name, t.rawname
+                                    FROM {tag} t
+                                    JOIN {tag_instance} ti ON ti.tagid = t.id
+                                   WHERE ti.itemtype = ?
+                                     AND ti.component = ?
+                                     AND ti.itemid = ?', array(
+                backup_helper::is_sqlparam('forum_posts'),
+                backup_helper::is_sqlparam('mod_forum'),
+                backup::VAR_PARENTID));
         }
 
         // Define id annotations
index de8b038..26595d6 100644 (file)
@@ -40,6 +40,7 @@ class restore_forum_activity_structure_step extends restore_activity_structure_s
         if ($userinfo) {
             $paths[] = new restore_path_element('forum_discussion', '/activity/forum/discussions/discussion');
             $paths[] = new restore_path_element('forum_post', '/activity/forum/discussions/discussion/posts/post');
+            $paths[] = new restore_path_element('forum_tag', '/activity/forum/discussions/discussion/posts/post/tags/tag');
             $paths[] = new restore_path_element('forum_discussion_sub', '/activity/forum/discussions/discussion/discussion_subs/discussion_sub');
             $paths[] = new restore_path_element('forum_rating', '/activity/forum/discussions/discussion/posts/post/ratings/rating');
             $paths[] = new restore_path_element('forum_subscription', '/activity/forum/subscriptions/subscription');
@@ -117,6 +118,20 @@ class restore_forum_activity_structure_step extends restore_activity_structure_s
         }
     }
 
+    protected function process_forum_tag($data) {
+        $data = (object)$data;
+
+        if (!core_tag_tag::is_enabled('mod_forum', 'forum_posts')) { // Tags disabled in server, nothing to process.
+            return;
+        }
+
+        $tag = $data->rawname;
+        $itemid = $this->get_new_parentid('forum_post');
+
+        $context = context_module::instance($this->task->get_moduleid());
+        core_tag_tag::add_item_tag('mod_forum', 'forum_posts', $itemid, $context, $tag);
+    }
+
     protected function process_forum_rating($data) {
         global $DB;
 
index 8145103..b4ae556 100644 (file)
@@ -52,6 +52,7 @@ class big_search_form implements renderable, templatable {
     public $subject;
     public $user;
     public $words;
+    public $tags;
     /** @var string The URL of the search form. */
     public $actionurl;
 
@@ -64,6 +65,7 @@ class big_search_form implements renderable, templatable {
     public function __construct($course) {
         global $DB;
         $this->course = $course;
+        $this->tags = [];
         $this->showfullwords = $DB->get_dbfamily() == 'mysql' || $DB->get_dbfamily() == 'postgres';
         $this->actionurl = new moodle_url('/mod/forum/search.php');
 
@@ -148,6 +150,15 @@ class big_search_form implements renderable, templatable {
         $this->words = $value;
     }
 
+    /**
+     * Set tags.
+     *
+     * @param mixed $value Tags.
+     */
+    public function set_tags($value) {
+        $this->tags = $value;
+    }
+
     /**
      * Forum ID setter search criteria.
      *
@@ -158,6 +169,7 @@ class big_search_form implements renderable, templatable {
     }
 
     public function export_for_template(renderer_base $output) {
+        global $DB, $CFG, $PAGE;
         $data = new stdClass();
 
         $data->courseid = $this->course->id;
@@ -172,6 +184,26 @@ class big_search_form implements renderable, templatable {
         $data->showfullwords = $this->showfullwords;
         $data->actionurl = $this->actionurl->out(false);
 
+        $tagtypestoshow = \core_tag_area::get_showstandard('mod_forum', 'forum_posts');
+        $showstandard = ($tagtypestoshow != \core_tag_tag::HIDE_STANDARD);
+        $typenewtags = ($tagtypestoshow != \core_tag_tag::STANDARD_ONLY);
+
+        $PAGE->requires->js_call_amd('core/form-autocomplete', 'enhance', $params = array('#tags', $typenewtags, '',
+                              get_string('entertags', 'tag'), false, $showstandard, get_string('noselection', 'form')));
+
+        $data->tagsenabled = \core_tag_tag::is_enabled('mod_forum', 'forum_posts');
+        $namefield = empty($CFG->keeptagnamecase) ? 'name' : 'rawname';
+        $tags = $DB->get_records('tag',
+            array('isstandard' => 1, 'tagcollid' => \core_tag_area::get_collection('mod_forum', 'forum_posts')),
+            $namefield, 'rawname,' . $namefield . ' as fieldname');
+        $data->tags = [];
+        foreach ($tags as $tag) {
+            $data->tagoptions[] = ['value'    => $tag->rawname,
+                                   'text'     => $tag->fieldname,
+                                   'selected' => in_array($tag->rawname, $this->tags)
+            ];
+        }
+
         $datefrom = $this->datefrom;
         if (empty($datefrom)) {
             $datefrom = make_timestamp(2000, 1, 1, 0, 0, 0);
index a9e8e82..0ade6fb 100644 (file)
@@ -238,6 +238,13 @@ class mod_forum_post_form extends moodleform {
             $mform->setConstants(array('timestart' => 0, 'timeend' => 0));
         }
 
+        if (core_tag_tag::is_enabled('mod_forum', 'forum_posts')) {
+            $mform->addElement('header', 'tagshdr', get_string('tags', 'tag'));
+
+            $mform->addElement('tags', 'tags', get_string('tags'),
+                array('itemtype' => 'forum_posts', 'component' => 'mod_forum'));
+        }
+
         //-------------------------------------------------------------------------------
         // buttons
         if (isset($post->edit)) { // hack alert
diff --git a/mod/forum/db/tag.php b/mod/forum/db/tag.php
new file mode 100644 (file)
index 0000000..8496408
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Tag areas in component mod_forum
+ *
+ * @package   mod_forum
+ * @copyright 2017 Andrew Hancox <andrewdchancox@googlemail.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+
+$tagareas = array(
+    array(
+        'itemtype' => 'forum_posts',
+        'component' => 'mod_forum',
+        'callback' => 'mod_forum_get_tagged_posts',
+        'callbackfile' => '/mod/forum/locallib.php',
+    ),
+);
index 415c599..4ee9694 100644 (file)
@@ -430,6 +430,7 @@ $string['qandaforum'] = 'Q and A forum';
 $string['qandanotify'] = 'This is a question and answer forum. In order to see other responses to these questions, you must first post your answer';
 $string['re'] = 'Re:';
 $string['readtherest'] = 'Read the rest of this topic';
+$string['removeallforumtags'] = 'Remove all forum tags';
 $string['replies'] = 'Replies';
 $string['repliesmany'] = '{$a} replies so far';
 $string['repliesone'] = '{$a} reply so far';
@@ -464,6 +465,7 @@ $string['searcholderposts'] = 'Search older posts...';
 $string['searchphrase'] = 'This exact phrase must appear in the post';
 $string['searchresults'] = 'Search results';
 $string['searchsubject'] = 'These words should be in the subject';
+$string['searchtags'] = 'Is tagged with';
 $string['searchuser'] = 'This name should match the author';
 $string['searchuserid'] = 'The Moodle ID of the author';
 $string['searchwhichforums'] = 'Choose which forums to search';
@@ -503,6 +505,9 @@ $string['subscriptionforced'] = 'Forced subscription';
 $string['subscriptionauto'] = 'Auto subscription';
 $string['subscriptiondisabled'] = 'Subscription disabled';
 $string['subscriptions'] = 'Subscriptions';
+$string['tagarea_forum_posts'] = 'Forum posts';
+$string['tagsdeleted'] = 'Forum tags have been deleted';
+$string['tagtitle'] = 'See the "{$a}" tag';
 $string['thisforumisthrottled'] = 'This forum has a limit to the number of forum postings you can make in a given time period - this is currently set at {$a->blockafter} posting(s) in {$a->blockperiod}';
 $string['timedhidden'] = 'Timed status: Hidden from students';
 $string['timedposts'] = 'Timed posts';
index b66923e..3302aec 100644 (file)
@@ -2095,15 +2095,32 @@ function forum_search_posts($searchterms, $courseid=0, $limitfrom=0, $limitnum=5
 
     if ($lexer->parse($searchstring)) {
         $parsearray = $parser->get_parsed_array();
+
+        $tagjoins = '';
+        $tagfields = [];
+        foreach ($parsearray as $token) {
+            if ($token->getType() == TOKEN_TAGS) {
+                for ($i = substr_count($token->getValue(), ','); $i >= 0; $i--) {
+                    $tagjoins .= "\n LEFT JOIN {tag_instance} ti_$i
+                                        ON p.id = ti_$i.itemid
+                                            AND ti_$i.component='mod_forum'
+                                            AND ti_$i.itemtype = 'forum_posts'";
+                    $tagjoins .= "\n LEFT JOIN {tag} t_$i ON t_$i.id = ti_$i.tagid";
+                    $tagfields[] = "t_$i.rawname";
+
+                }
+            }
+        }
         list($messagesearch, $msparams) = search_generate_SQL($parsearray, 'p.message', 'p.subject',
                                                               'p.userid', 'u.id', 'u.firstname',
-                                                              'u.lastname', 'p.modified', 'd.forum');
+                                                              'u.lastname', 'p.modified', 'd.forum',
+                                                              $tagfields);
         $params = array_merge($params, $msparams);
     }
 
-    $fromsql = "{forum_posts} p,
-                  {forum_discussions} d,
-                  {user} u";
+    $fromsql = "{forum_posts} p
+                  INNER JOIN {forum_discussions} d ON d.id = p.discussion
+                  INNER JOIN {user} u ON u.id = p.userid $tagjoins";
 
     $selectsql = " $messagesearch
                AND p.discussion = d.id
@@ -3446,6 +3463,10 @@ function forum_print_post($post, $discussion, $forum, &$cm, $course, $ownpost=fa
         $postcontent .= html_writer::tag('div', $attachedimages, array('class'=>'attachedimages'));
     }
 
+    if (\core_tag_tag::is_enabled('mod_forum', 'forum_posts')) {
+        $postcontent .= $OUTPUT->tag_list(core_tag_tag::get_item_tags('mod_forum', 'forum_posts', $post->id), null, 'forum-tags');
+    }
+
     // Output the post content
     $output .= html_writer::tag('div', $postcontent, array('class'=>'posting '.$postclass));
     $output .= html_writer::end_tag('div'); // Content
@@ -5262,7 +5283,8 @@ function forum_user_can_see_post($forum, $discussion, $post, $user=NULL, $cm=NUL
         $user = $USER;
     }
 
-    $canviewdiscussion = !empty($cm->cache->caps['mod/forum:viewdiscussion']) || has_capability('mod/forum:viewdiscussion', $modcontext, $user->id);
+    $canviewdiscussion = (isset($cm->cache) && !empty($cm->cache->caps['mod/forum:viewdiscussion']))
+        || has_capability('mod/forum:viewdiscussion', $modcontext, $user->id);
     if (!$canviewdiscussion && !has_all_capabilities(array('moodle/user:viewdetails', 'moodle/user:readuserposts'), context_user::instance($post->userid))) {
         return false;
     }
index 057339a..527cba6 100644 (file)
@@ -566,3 +566,128 @@ class forum_file_info_container extends file_info {
         return $this->browser->get_file_info($this->context);
     }
 }
+
+/**
+ * Returns forum posts tagged with a specified tag.
+ *
+ * This is a callback used by the tag area mod_forum/forum_posts to search for forum posts
+ * tagged with a specific tag.
+ *
+ * @param core_tag_tag $tag
+ * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
+ *             are displayed on the page and the per-page limit may be bigger
+ * @param int $fromctx context id where the link was displayed, may be used by callbacks
+ *            to display items in the same context first
+ * @param int $ctx context id where to search for records
+ * @param bool $rec search in subcontexts as well
+ * @param int $page 0-based number of page being displayed
+ * @return \core_tag\output\tagindex
+ */
+function mod_forum_get_tagged_posts($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
+    global $OUTPUT;
+    $perpage = $exclusivemode ? 20 : 5;
+
+    // Build the SQL query.
+    $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
+    $query = "SELECT fp.id, fp.subject, fd.forum, fp.discussion, f.type, fd.timestart, fd.timeend, fd.groupid, fp.parent, fp.userid,
+                    cm.id AS cmid, c.id AS courseid, c.shortname, c.fullname, $ctxselect
+                FROM {forum_posts} fp
+                JOIN {forum_discussions} fd ON fp.discussion = fd.id
+                JOIN {forum} f ON f.id = fd.forum
+                JOIN {modules} m ON m.name='forum'
+                JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = f.id
+                JOIN {tag_instance} tt ON fp.id = tt.itemid
+                JOIN {course} c ON cm.course = c.id
+                JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
+               WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
+                 AND cm.deletioninprogress = 0
+                 AND fp.id %ITEMFILTER% AND c.id %COURSEFILTER%";
+
+    $params = array('itemtype' => 'forum_posts', 'tagid' => $tag->id, 'component' => 'mod_forum',
+                    'coursemodulecontextlevel' => CONTEXT_MODULE);
+
+    if ($ctx) {
+        $context = $ctx ? context::instance_by_id($ctx) : context_system::instance();
+        $query .= $rec ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
+        $params['contextid'] = $context->id;
+        $params['path'] = $context->path.'/%';
+    }
+
+    $query .= " ORDER BY ";
+    if ($fromctx) {
+        // In order-clause specify that modules from inside "fromctx" context should be returned first.
+        $fromcontext = context::instance_by_id($fromctx);
+        $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
+        $params['fromcontextid'] = $fromcontext->id;
+        $params['frompath'] = $fromcontext->path.'/%';
+    }
+    $query .= ' c.sortorder, cm.id, fp.id';
+
+    $totalpages = $page + 1;
+
+    // Use core_tag_index_builder to build and filter the list of items.
+    $builder = new core_tag_index_builder('mod_forum', 'forum_posts', $query, $params, $page * $perpage, $perpage + 1);
+    while ($item = $builder->has_item_that_needs_access_check()) {
+        context_helper::preload_from_record($item);
+        $courseid = $item->courseid;
+        if (!$builder->can_access_course($courseid)) {
+            $builder->set_accessible($item, false);
+            continue;
+        }
+        $modinfo = get_fast_modinfo($builder->get_course($courseid));
+        // Set accessibility of this item and all other items in the same course.
+        $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) {
+            if ($taggeditem->courseid == $courseid) {
+                $cm = $modinfo->get_cm($taggeditem->cmid);
+                $forum = (object)['id'     => $taggeditem->forum,
+                                  'course' => $taggeditem->courseid,
+                                  'type'   => $taggeditem->type
+                ];
+                $discussion = (object)['id'        => $taggeditem->discussion,
+                                       'timestart' => $taggeditem->timestart,
+                                       'timeend'   => $taggeditem->timeend,
+                                       'groupid'   => $taggeditem->groupid
+                ];
+                $post = (object)['id' => $taggeditem->id,
+                                       'parent' => $taggeditem->parent,
+                                       'userid'   => $taggeditem->userid,
+                                       'groupid'   => $taggeditem->groupid
+                ];
+
+                $accessible = forum_user_can_see_post($forum, $discussion, $post, null, $cm);
+                $builder->set_accessible($taggeditem, $accessible);
+            }
+        });
+    }
+
+    $items = $builder->get_items();
+    if (count($items) > $perpage) {
+        $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
+        array_pop($items);
+    }
+
+    // Build the display contents.
+    if ($items) {
+        $tagfeed = new core_tag\output\tagfeed();
+        foreach ($items as $item) {
+            context_helper::preload_from_record($item);
+            $modinfo = get_fast_modinfo($item->courseid);
+            $cm = $modinfo->get_cm($item->cmid);
+            $pageurl = new moodle_url('/mod/forum/discuss.php', array('d' => $item->discussion), 'p' . $item->id);
+            $pagename = format_string($item->subject, true, array('context' => context_module::instance($item->cmid)));
+            $pagename = html_writer::link($pageurl, $pagename);
+            $courseurl = course_get_url($item->courseid, $cm->sectionnum);
+            $cmname = html_writer::link($cm->url, $cm->get_formatted_name());
+            $coursename = format_string($item->fullname, true, array('context' => context_course::instance($item->courseid)));
+            $coursename = html_writer::link($courseurl, $coursename);
+            $icon = html_writer::link($pageurl, html_writer::empty_tag('img', array('src' => $cm->get_icon_url())));
+            $tagfeed->add($icon, $pagename, $cmname.'<br>'.$coursename);
+        }
+
+        $content = $OUTPUT->render_from_template('core_tag/tagfeed',
+            $tagfeed->export_for_template($OUTPUT));
+
+        return new core_tag\output\tagindex($tag, 'mod_forum', 'forum_posts', $content,
+            $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
+    }
+}
index 1933800..c87780e 100644 (file)
@@ -757,6 +757,8 @@ if ($mform_post->is_cancelled()) {
             $discussionurl = new moodle_url("/mod/forum/discuss.php", array('d' => $discussion->id), 'p' . $fromform->id);
         }
 
+        core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $fromform->id, $modcontext, $fromform->tags);
+
         $params = array(
             'context' => $modcontext,
             'objectid' => $fromform->id,
@@ -809,6 +811,10 @@ if ($mform_post->is_cancelled()) {
                 $discussionurl = new moodle_url("/mod/forum/discuss.php", array('d' => $discussion->id), 'p'.$fromform->id);
             }
 
+            if (core_tag_tag::is_enabled('mod_forum', 'forum_posts') && isset($data->tags)) {
+                core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $fromform->id, $modcontext, $fromform->tags);
+            }
+
             $params = array(
                 'context' => $modcontext,
                 'objectid' => $fromform->id,
@@ -936,6 +942,8 @@ if ($mform_post->is_cancelled()) {
             $completion->update_state($cm, COMPLETION_COMPLETE);
         }
 
+        core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $fromform->firstpost, $modcontext, $fromform->tags);
+
         // Redirect back to the discussion.
         redirect(
                 forum_go_back_to($redirectto->out()),
@@ -1052,6 +1060,13 @@ if (!empty($parent)) {
 if (!empty($formheading)) {
     echo $OUTPUT->heading($formheading, 2, array('class' => 'accesshide'));
 }
+
+$data = new StdClass();
+if (isset($postid)) {
+    $data->tags = core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $postid);
+    $mform_post->set_data($data);
+}
+
 $mform_post->display();
 
 echo $OUTPUT->footer();
index ec399f9..9cfed3b 100644 (file)
@@ -38,6 +38,7 @@ $phrase  = trim(optional_param('phrase', '', PARAM_NOTAGS));  // Phrase
 $words   = trim(optional_param('words', '', PARAM_NOTAGS));   // Words
 $fullwords = trim(optional_param('fullwords', '', PARAM_NOTAGS)); // Whole words
 $notwords = trim(optional_param('notwords', '', PARAM_NOTAGS));   // Words we don't want
+$tags = optional_param_array('tags', [], PARAM_TEXT);
 
 $timefromrestrict = optional_param('timefromrestrict', 0, PARAM_INT); // Use starting date
 $fromday = optional_param('fromday', 0, PARAM_INT);      // Starting date
@@ -101,6 +102,9 @@ if (empty($search)) {   // Check the other parameters instead
     if (!empty($dateto)) {
         $search .= ' dateto:'.$dateto;
     }
+    if (!empty($tags)) {
+        $search .= ' tags:' . implode(',', $tags);
+    }
     $individualparams = true;
 } else {
     $individualparams = false;
@@ -186,19 +190,27 @@ $PAGE->set_heading($course->fullname);
 $PAGE->set_button($searchform);
 echo $OUTPUT->header();
 echo '<div class="reportlink">';
-echo '<a href="search.php?id='.$course->id.
-                         '&amp;user='.urlencode($user).
-                         '&amp;userid='.$userid.
-                         '&amp;forumid='.$forumid.
-                         '&amp;subject='.urlencode($subject).
-                         '&amp;phrase='.urlencode($phrase).
-                         '&amp;words='.urlencode($words).
-                         '&amp;fullwords='.urlencode($fullwords).
-                         '&amp;notwords='.urlencode($notwords).
-                         '&amp;dateto='.$dateto.
-                         '&amp;datefrom='.$datefrom.
-                         '&amp;showform=1'.
-                         '">'.get_string('advancedsearch','forum').'...</a>';
+
+$params = [
+    'id'        => $course->id,
+    'user'      => $user,
+    'userid'    => $userid,
+    'forumid'   => $forumid,
+    'subject'   => $subject,
+    'phrase'    => $phrase,
+    'words'     => $words,
+    'fullwords' => $fullwords,
+    'notwords'  => $notwords,
+    'dateto'    => $dateto,
+    'datefrom'  => $datefrom,
+    'showform'  => 1
+];
+$url    = new moodle_url("/mod/forum/search.php", $params);
+foreach ($tags as $tag) {
+    $url .= "&tags[]=$tag";
+}
+echo html_writer::link($url, get_string('advancedsearch', 'forum').'...');
+
 echo '</div>';
 
 echo $OUTPUT->heading($strforums, 2);
@@ -318,7 +330,7 @@ echo $OUTPUT->footer();
   * @return void The function prints the form.
   */
 function forum_print_big_search_form($course) {
-    global $PAGE, $words, $subject, $phrase, $user, $fullwords, $notwords, $datefrom, $dateto, $forumid;
+    global $PAGE, $words, $subject, $phrase, $user, $fullwords, $notwords, $datefrom, $dateto, $forumid, $tags;
 
     $renderable = new \mod_forum\output\big_search_form($course, $user);
     $renderable->set_words($words);
@@ -330,6 +342,7 @@ function forum_print_big_search_form($course) {
     $renderable->set_subject($subject);
     $renderable->set_user($user);
     $renderable->set_forumid($forumid);
+    $renderable->set_tags($tags);
 
     $output = $PAGE->get_renderer('mod_forum');
     echo $output->render($renderable);
index f9371f2..4d26e5c 100644 (file)
                     <input type="text" size="35" name="user" id="user" value="{{user}}">
                 </td>
             </tr>
+            {{#tagsenabled}}
+            <tr>
+                <td class="c0">
+                    <label for="tags">{{#str}}searchtags, forum{{/str}}</label>
+                </td>
+                <td class="c1">
+                    <select class="custom-select" name="tags[]"
+                            id="tags" multiple>
+                        {{#tagoptions}}
+                            <option value="{{value}}" {{#selected}}selected{{/selected}}>{{{text}}}</option>
+                        {{/tagoptions}}
+                    </select>
+                </td>
+            </tr>
+            {{/tagsenabled}}
             <tr>
                 <td colspan="2" class="submit">
                     <div class="text-center">
index c9134a8..0f8a2b4 100644 (file)
@@ -18,6 +18,9 @@ Feature: The forum search allows users to perform advanced searches for forum po
       | teacher1 | C1 | editingteacher |
       | teacher2 | C1 | editingteacher |
       | student1 | C1 | student |
+    And the following "tags" exist:
+      | name         | isstandard  |
+      | SearchedTag  | 1           |
     And I log in as "teacher1"
     And I am on "Course 1" course homepage with editing mode on
     And I add the "Latest announcements" block
@@ -27,6 +30,7 @@ Feature: The forum search allows users to perform advanced searches for forum po
     And I add a new topic to "Announcements" forum with:
       | Subject | My subject |
       | Message | My message |
+      | Tags    | SearchedTag |
     And I am on "Course 1" course homepage
     And I add a new topic to "Announcements" forum with:
       | Subject | My subjective|
@@ -108,3 +112,16 @@ Feature: The forum search allows users to perform advanced searches for forum po
     When I press "Search forums"
     Then I should not see "My message"
     And I should see "My subjective"
+
+  @javascript
+  Scenario: Perform an advanced search using tags
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I follow "Announcements"
+    And I press "Search forums"
+    And I should see "Advanced search"
+    And I set the field "Is tagged with" to "SearchedTag"
+    And I click on "[data-value='SearchedTag']" "css_element"
+    When I press "Search forums"
+    Then I should see "My subject"
+    And I should not see "My subjective"
index 3a8cd70..b546457 100644 (file)
@@ -77,6 +77,7 @@ class behat_mod_forum extends behat_base {
 
         // Fill form and post.
         $this->execute('behat_forms::i_set_the_following_fields_to_these_values', $table);
+        $this->execute("behat_general::wait_until_the_page_is_ready");
 
         $this->execute('behat_forms::press_button', get_string('posttoforum', 'forum'));
         $this->execute('behat_general::i_wait_to_be_redirected');
diff --git a/mod/forum/tests/behat/edit_tags.feature b/mod/forum/tests/behat/edit_tags.feature
new file mode 100644 (file)
index 0000000..0ab3fa8
--- /dev/null
@@ -0,0 +1,69 @@
+@mod @mod_forum @core_tag @javascript
+Feature: Edited forum posts handle tags correctly
+  In order to get forum posts properly labelled
+  As a user
+  I need to introduce the tags while editing
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email |
+      | teacher1 | Teacher | 1 | teacher1@example.com |
+      | student1 | Student | 1 | student1@example.com |
+    And the following "courses" exist:
+      | fullname | shortname | format |
+      | Course 1 | C1 | topics |
+    And the following "course enrolments" exist:
+      | user | course | role |
+      | teacher1 | C1 | editingteacher |
+      | student1 | C1 | student |
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I turn editing mode on
+    And I add a "Forum" to section "1" and I fill the form with:
+      | Forum name | Test forum name |
+      | Description | Test forum description |
+    And I add a new discussion to "Test forum name" forum with:
+      | Subject | Teacher post subject |
+      | Message | Teacher post message |
+    And I log out
+
+  Scenario: Forum post edition of custom tags works as expected
+    Given I log in as "student1"
+    And I follow "Course 1"
+    And I reply "Teacher post subject" post from "Test forum name" forum with:
+      | Subject | Student post subject |
+      | Message | Student post message |
+      | Tags    | Tag1                 |
+    Then I should see "Tag1" in the ".forum-tags" "css_element"
+    And I click on "Edit" "link" in the "//div[@aria-label='Student post subject by Student 1']" "xpath_element"
+    Then I should see "Tag1" in the ".form-autocomplete-selection" "css_element"
+
+  @javascript
+  Scenario: Forum post edition of standard tags works as expected
+    Given I log in as "admin"
+    And I navigate to "Appearance > Manage tags" in site administration
+    And I follow "Default collection"
+    And I follow "Add standard tags"
+    And I set the field "Enter comma-separated list of new tags" to "OT1, OT2, OT3"
+    And I press "Continue"
+    And I log out
+    And I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Test forum"
+    And I click on "Add a new discussion topic" "button"
+    And I expand all fieldsets
+    And I click on ".form-autocomplete-downarrow" "css_element"
+    And I should see "OT1" in the ".form-autocomplete-suggestions" "css_element"
+    And I should see "OT2" in the ".form-autocomplete-suggestions" "css_element"
+    And I should see "OT3" in the ".form-autocomplete-suggestions" "css_element"
+    And I reply "Teacher post subject" post from "Test forum name" forum with:
+      | Subject | Student post subject |
+      | Message | Student post message |
+      | Tags | OT1, OT3 |
+    Then I should see "OT1" in the ".forum-tags" "css_element"
+    And I should see "OT3" in the ".forum-tags" "css_element"
+    And I should not see "OT2" in the ".forum-tags" "css_element"
+    And I click on "Edit" "link" in the "//div[@aria-label='Student post subject by Teacher 1']" "xpath_element"
+    And I should see "OT1" in the ".form-autocomplete-selection" "css_element"
+    And I should see "OT3" in the ".form-autocomplete-selection" "css_element"
+    And I should not see "OT2" in the ".form-autocomplete-selection" "css_element"
index 818b8ce..151a38c 100644 (file)
@@ -202,9 +202,9 @@ class mod_forum_generator extends testing_module_generator {
         // Add the discussion.
         $record->id = forum_add_discussion($record, null, null, $record->userid);
 
-        if (isset($timemodified) || isset($mailed)) {
-            $post = $DB->get_record('forum_posts', array('discussion' => $record->id));
+        $post = $DB->get_record('forum_posts', array('discussion' => $record->id));
 
+        if (isset($timemodified) || isset($mailed)) {
             if (isset($mailed)) {
                 $post->mailed = $mailed;
             }
@@ -220,6 +220,14 @@ class mod_forum_generator extends testing_module_generator {
             $DB->update_record('forum_posts', $post);
         }
 
+        if (property_exists($record, 'tags')) {
+            $cm = get_coursemodule_from_instance('forum', $record->forum);
+            $tags = is_array($record->tags) ? $record->tags : preg_split('/,/', $record->tags);
+
+            core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $post->id,
+                context_module::instance($cm->id), $tags);
+        }
+
         return $record;
     }
 
@@ -297,6 +305,15 @@ class mod_forum_generator extends testing_module_generator {
         // Add the post.
         $record->id = $DB->insert_record('forum_posts', $record);
 
+        if (property_exists($record, 'tags')) {
+            $discussion = $DB->get_record('forum_discussions', ['id' => $record->discussion]);
+            $cm = get_coursemodule_from_instance('forum', $discussion->forum);
+            $tags = is_array($record->tags) ? $record->tags : preg_split('/,/', $record->tags);
+
+            core_tag_tag::set_item_tags('mod_forum', 'forum_posts', $record->id,
+                context_module::instance($cm->id), $tags);
+        }
+
         // Update the last post.
         forum_discussion_update_last_post($record->discussion);
 
index 0be1d4a..8a4e20f 100644 (file)
@@ -117,6 +117,11 @@ class mod_forum_generator_testcase extends advanced_testcase {
         // Check the discussions were correctly created.
         $this->assertEquals(3, $DB->count_records_select('forum_discussions', 'forum = :forum',
             array('forum' => $forum->id)));
+
+        $record['tags'] = array('Cats', 'mice');
+        $record = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
+        $this->assertEquals(array('Cats', 'mice'),
+            array_values(core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $record->firstpost)));
     }
 
     /**
@@ -160,6 +165,11 @@ class mod_forum_generator_testcase extends advanced_testcase {
         // is generated as well, so we should have 4 posts, not 3.
         $this->assertEquals(4, $DB->count_records_select('forum_posts', 'discussion = :discussion',
             array('discussion' => $discussion->id)));
+
+        $record->tags = array('Cats', 'mice');
+        $record = self::getDataGenerator()->get_plugin_generator('mod_forum')->create_post($record);
+        $this->assertEquals(array('Cats', 'mice'),
+            array_values(core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $record->id)));
     }
 
     public function test_create_content() {
@@ -187,16 +197,21 @@ class mod_forum_generator_testcase extends advanced_testcase {
         $post3 = $generator->create_content($forum, array('discussion' => $post1->discussion));
         // This should create posts answering another post.
         $post4 = $generator->create_content($forum, array('parent' => $post2->id));
+        // This should create post with tags.
+        $post5 = $generator->create_content($forum, array('parent' => $post2->id, 'tags' => array('Cats', 'mice')));
 
         $discussionrecords = $DB->get_records('forum_discussions', array('forum' => $forum->id));
         $postrecords = $DB->get_records('forum_posts');
         $postrecords2 = $DB->get_records('forum_posts', array('discussion' => $post1->discussion));
         $this->assertEquals(1, count($discussionrecords));
-        $this->assertEquals(4, count($postrecords));
-        $this->assertEquals(4, count($postrecords2));
+        $this->assertEquals(5, count($postrecords));
+        $this->assertEquals(5, count($postrecords2));
         $this->assertEquals($post1->id, $discussionrecords[$post1->discussion]->firstpost);
         $this->assertEquals($post1->id, $postrecords[$post2->id]->parent);
         $this->assertEquals($post1->id, $postrecords[$post3->id]->parent);
         $this->assertEquals($post2->id, $postrecords[$post4->id]->parent);
+
+        $this->assertEquals(array('Cats', 'mice'),
+            array_values(core_tag_tag::get_item_tags_array('mod_forum', 'forum_posts', $post5->id)));
     }
 }
index 0af8f26..59ed1cb 100644 (file)
@@ -26,6 +26,7 @@ defined('MOODLE_INTERNAL') || die();
 
 global $CFG;
 require_once($CFG->dirroot . '/mod/forum/lib.php');
+require_once($CFG->dirroot . '/mod/forum/locallib.php');
 require_once($CFG->dirroot . '/rating/lib.php');
 
 class mod_forum_lib_testcase extends advanced_testcase {
@@ -3410,6 +3411,94 @@ class mod_forum_lib_testcase extends advanced_testcase {
         $this->assertNull($actionevent);
     }
 
+    public function test_mod_forum_get_tagged_posts() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $this->setAdminUser();
+
+        // Setup test data.
+        $forumgenerator = $this->getDataGenerator()->get_plugin_generator('mod_forum');
+        $course3 = $this->getDataGenerator()->create_course();
+        $course2 = $this->getDataGenerator()->create_course();
+        $course1 = $this->getDataGenerator()->create_course();
+        $forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id));
+        $forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id));
+        $forum3 = $this->getDataGenerator()->create_module('forum', array('course' => $course3->id));
+        $post11 = $forumgenerator->create_content($forum1, array('tags' => array('Cats', 'Dogs')));
+        $post12 = $forumgenerator->create_content($forum1, array('tags' => array('Cats', 'mice')));
+        $post13 = $forumgenerator->create_content($forum1, array('tags' => array('Cats')));
+        $post14 = $forumgenerator->create_content($forum1);
+        $post15 = $forumgenerator->create_content($forum1, array('tags' => array('Cats')));
+        $post16 = $forumgenerator->create_content($forum1, array('tags' => array('Cats'), 'hidden' => true));
+        $post21 = $forumgenerator->create_content($forum2, array('tags' => array('Cats')));
+        $post22 = $forumgenerator->create_content($forum2, array('tags' => array('Cats', 'Dogs')));
+        $post23 = $forumgenerator->create_content($forum2, array('tags' => array('mice', 'Cats')));
+        $post31 = $forumgenerator->create_content($forum3, array('tags' => array('mice', 'Cats')));
+
+        $tag = core_tag_tag::get_by_name(0, 'Cats');
+
+        // Admin can see everything.
+        $res = mod_forum_get_tagged_posts($tag, /*$exclusivemode = */false,
+            /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$post = */0);
+        $this->assertRegExp('/'.$post11->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post12->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post13->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post14->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post15->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post16->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post21->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post22->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post23->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post31->subject.'</', $res->content);
+        $this->assertEmpty($res->prevpageurl);
+        $this->assertNotEmpty($res->nextpageurl);
+        $res = mod_forum_get_tagged_posts($tag, /*$exclusivemode = */false,
+            /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$post = */1);
+        $this->assertNotRegExp('/'.$post11->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post12->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post13->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post14->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post15->subject.'</', $res->content);
+        $this->assertNotRegExp('/'.$post16->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post21->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post22->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post23->subject.'</', $res->content);
+        $this->assertRegExp('/'.$post31->subject.'</', $res->content);
+        $this->assertNotEmpty($res->prevpageurl);
+        $this->assertEmpty($res->nextpageurl);
+
+        // Create and enrol a user.
+        $student = self::getDataGenerator()->create_user();
+        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
+        $this->getDataGenerator()->enrol_user($student->id, $course1->id, $studentrole->id, 'manual');
+        $this->getDataGenerator()->enrol_user($student->id, $course2->id, $studentrole->id, 'manual');
+        $this->setUser($student);
+        core_tag_index_builder::reset_caches();
+
+        // User can not see posts in course 3 because he is not enrolled.
+        $res = mod_forum_get_tagged_posts($tag, /*$exclusivemode = */false,
+            /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$post = */1);
+        $this->assertRegExp('/'.$post22->subject.'/', $res->content);
+        $this->assertRegExp('/'.$post23->subject.'/', $res->content);
+        $this->assertNotRegExp('/'.$post31->subject.'/', $res->content);
+
+        // User can search forum posts inside a course.
+        $coursecontext = context_course::instance($course1->id);
+        $res = mod_forum_get_tagged_posts($tag, /*$exclusivemode = */false,
+            /*$fromctx = */0, /*$ctx = */$coursecontext->id, /*$rec = */1, /*$post = */0);
+        $this->assertRegExp('/'.$post11->subject.'/', $res->content);
+        $this->assertRegExp('/'.$post12->subject.'/', $res->content);
+        $this->assertRegExp('/'.$post13->subject.'/', $res->content);
+        $this->assertNotRegExp('/'.$post14->subject.'/', $res->content);
+        $this->assertRegExp('/'.$post15->subject.'/', $res->content);
+        $this->assertRegExp('/'.$post16->subject.'/', $res->content);
+        $this->assertNotRegExp('/'.$post21->subject.'/', $res->content);
+        $this->assertNotRegExp('/'.$post22->subject.'/', $res->content);
+        $this->assertNotRegExp('/'.$post23->subject.'/', $res->content);
+        $this->assertEmpty($res->nextpageurl);
+    }
+
     /**
      * Creates an action event.
      *
index 4246a2f..da77f51 100644 (file)
@@ -24,6 +24,6 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2016120500;       // The current module version (Date: YYYYMMDDXX)
+$plugin->version   = 2016120501;       // The current module version (Date: YYYYMMDDXX)
 $plugin->requires  = 2016112900;       // Requires this Moodle version
 $plugin->component = 'mod_forum';      // Full name of the plugin (used for diagnostics)
index 241d842..5c05e5c 100644 (file)
                     <input type="text" class="form-control" name="user" id="user" value="{{user}}">
                 </td>
             </tr>
+            {{#tagsenabled}}
+                <tr>
+                    <td class="c0">
+                        <label for="tags">{{#str}}searchtags, forum{{/str}}</label>
+                    </td>
+                    <td class="c1">
+                        <select class="custom-select" name="tags[]"
+                                id="tags" multiple>
+                            {{#tagoptions}}
+                                <option value="{{value}}" {{#selected}}selected{{/selected}}>{{{text}}}</option>
+                            {{/tagoptions}}
+                        </select>
+                    </td>
+                </tr>
+            {{/tagsenabled}}
             <tr>
                 <td> </td>
                 <td class="submit">