Search for content by tags
authorAlexander Ebert <ebert@woltlab.com>
Fri, 4 Jan 2019 11:43:07 +0000 (12:43 +0100)
committerAlexander Ebert <ebert@woltlab.com>
Fri, 4 Jan 2019 11:43:07 +0000 (12:43 +0100)
See #2689

com.woltlab.wcf/page.xml
com.woltlab.wcf/templates/combinedTagged.tpl [new file with mode: 0644]
com.woltlab.wcf/templates/search.tpl
wcfsetup/install/files/lib/data/article/TaggedArticleList.class.php
wcfsetup/install/files/lib/form/TagSearchForm.class.php
wcfsetup/install/files/lib/page/CombinedTaggedPage.class.php
wcfsetup/install/files/lib/system/tagging/AbstractCombinedTaggable.class.php
wcfsetup/install/files/lib/system/tagging/TagEngine.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 3e76e8a798fa012d71f8576e6f2829128a608486..9107dc0e88aa0f751a4655c15121eff570bfb075 100644 (file)
                                <title>Suche</title>
                        </content>
                </page>
+               <page identifier="com.woltlab.wcf.TagSearch">
+                       <pageType>system</pageType>
+                       <controller>wcf\form\TagSearchForm</controller>
+                       <name language="de">Suche nach Tags</name>
+                       <name language="en">Search by Tags</name>
+                       <permissions>user.tag.canViewTag</permissions>
+                       <content language="en">
+                               <title>Search by Tags</title>
+                       </content>
+                       <content language="de">
+                               <title>Suche nach Tags</title>
+                       </content>
+               </page>
                <page identifier="com.woltlab.wcf.Settings">
                        <pageType>system</pageType>
                        <controller>wcf\form\SettingsForm</controller>
diff --git a/com.woltlab.wcf/templates/combinedTagged.tpl b/com.woltlab.wcf/templates/combinedTagged.tpl
new file mode 100644 (file)
index 0000000..3e890a5
--- /dev/null
@@ -0,0 +1,80 @@
+{capture assign='pageTitle'}{lang}wcf.tagging.combinedTaggedObjects.{@$objectType}{/lang}{if $pageNo > 1} - {lang}wcf.page.pageNo{/lang}{/if}{/capture}
+
+{capture assign='contentHeader'}
+       <header class="contentHeader">
+               <div class="contentHeaderTitle">
+                       <h1 class="contentTitle">{lang}wcf.tagging.combinedTaggedObjects.{@$objectType}{/lang}</h1>
+                       <ul class="tagList" style="margin-top: 10px">
+                               {foreach from=$combinedTags item=tag}
+                                       <li><a href="{link controller='Tagged' object=$tag}objectType={@$objectType}{/link}" class="tag jsTooltip" title="{lang}wcf.tagging.taggedObjects.{@$objectType}{/lang}">{$tag->name}</a></li>
+                               {/foreach}
+                       </ul>
+               </div>
+       </header>
+{/capture}
+
+{capture assign='linkParameters'}{implode from=$combinedTags item=tag glue='&'}tagIDs[]={@$tag->tagID}{/implode}{/capture}
+
+{capture assign='headContent'}
+       {if $pageNo < $pages}
+               <link rel="next" href="{link controller='CombinedTagged'}{@$linkParameters}&objectType={@$objectType}&pageNo={@$pageNo+1}{/link}">
+       {/if}
+       {if $pageNo > 1}
+               <link rel="prev" href="{link controller='CombinedTagged'}{@$linkParameters}&objectType={@$objectType}{if $pageNo > 2}&pageNo={@$pageNo-1}{/if}{/link}">
+       {/if}
+       <link rel="canonical" href="{link controller='CombinedTagged'}{@$linkParameters}&objectType={@$objectType}{if $pageNo > 1}&pageNo={@$pageNo}{/if}{/link}">
+{/capture}
+
+{capture assign='sidebarLeft'}
+       <section class="box" data-static-box-identifier="com.woltlab.wcf.TaggedMenu">
+               <h2 class="boxTitle">{lang}wcf.tagging.objectTypes{/lang}</h2>
+               
+               <nav class="boxContent">
+                       <ul class="boxMenu">
+                               {foreach from=$availableObjectTypes item=availableObjectType}
+                                       <li{if $objectType == $availableObjectType->objectType} class="active"{/if}><a class="boxMenuLink" href="{link controller='CombinedTagged'}{@$linkParameters}&objectType={@$availableObjectType->objectType}{/link}">{lang}wcf.tagging.objectType.{@$availableObjectType->objectType}{/lang}</a></li>
+                               {/foreach}
+                       </ul>
+               </nav>
+       </section>
+       
+       <section class="box" data-static-box-identifier="com.woltlab.wcf.TaggedTagCloud">
+               <h2 class="boxTitle">{lang}wcf.tagging.tags{/lang}</h2>
+               
+               <div class="boxContent">
+                       {include file='tagCloudBox' taggableObjectType=$objectType}
+               </div>
+       </section>
+{/capture}
+
+{include file='header'}
+
+{hascontent}
+       <div class="paginationTop">
+               {content}{pages print=true assign=pagesLinks controller='CombinedTagged' link="$linkParameters&objectType=$objectType&pageNo=%d"}{/content}
+       </div>
+{/hascontent}
+
+{if $items}
+       {include file=$resultListTemplateName application=$resultListApplication}
+{else}
+       <p class="info">{lang}wcf.tagging.taggedObjects.noResults{/lang}</p>
+{/if}
+
+<footer class="contentFooter">
+       {hascontent}
+               <div class="paginationBottom">
+                       {content}{@$pagesLinks}{/content}
+               </div>
+       {/hascontent}
+       
+       {hascontent}
+               <nav class="contentFooterNavigation">
+                       <ul>
+                               {content}{event name='contentFooterNavigation'}{/content}
+                       </ul>
+               </nav>
+       {/hascontent}
+</footer>
+
+{include file='footer'}
index 20472ee81c8a02fefc0d4a9f666003c53335aa9b..307600bd03ab134224cc9eadd40a65f2049acd37 100644 (file)
@@ -11,7 +11,7 @@
                <nav class="tabMenu">
                        <ul>
                                <li class="active"><a href="{link controller='Search'}{/link}">{lang}wcf.search.type.keywords{/lang}</a></li>
-                               <li><a href="{link controller='TagSearch'}{/link}">{lang}wcf.search.type.tags{/lang}</a></li>
+                               {if $__wcf->session->getPermission('user.tag.canViewTag')}<li><a href="{link controller='TagSearch'}{/link}">{lang}wcf.search.type.tags{/lang}</a></li>{/if}
                                
                                {event name='tabMenuTabs'}
                        </ul>
index 8d02089e1a50682be1e5a74e6ab428fff56d7a29..045b68fc578f6666ff314f11344df174b77a69b7 100644 (file)
@@ -16,14 +16,27 @@ class TaggedArticleList extends AccessibleArticleList {
        /**
         * Creates a new CategoryArticleList object.
         *
-        * @param       Tag|Tag[]       $tag
+        * @param Tag|Tag[] $tags
         */
-       public function __construct($tag) {
+       public function __construct($tags) {
                parent::__construct();
                
                $this->sqlOrderBy = 'article.time ' . ARTICLE_SORT_ORDER;
                
-               $innerSql = TagEngine::getInstance()->getSqlForObjectsByTags('com.woltlab.wcf.article', $tag);
-               $this->getConditionBuilder()->add("article.articleID IN (SELECT articleID FROM wcf".WCF_N."_article_content WHERE articleContentID IN (".$innerSql['sql']."))", [$innerSql['parameters']]);
+               $tagIDs = TagEngine::getInstance()->getTagIDs($tags);
+               $this->getConditionBuilder()->add("article.articleID IN (
+                       SELECT articleID FROM wcf".WCF_N."_article_content WHERE articleContentID IN (
+                               SELECT          objectID
+                               FROM            wcf".WCF_N."_tag_to_object
+                               WHERE           objectTypeID = ?
+                                               AND tagID IN (?)
+                               GROUP BY        objectID
+                               HAVING          COUNT(objectID) = ?
+                       )
+               )", [
+                       TagEngine::getInstance()->getObjectTypeID('com.woltlab.wcf.article'),
+                       $tagIDs,
+                       count($tagIDs)
+               ]);
        }
 }
index e4aef35b2cf7eedcf3ebc327a7a11454d8abf3ea..8595798e03eac4a1dacd903efb0f7e11a2390f01 100644 (file)
@@ -15,7 +15,7 @@ use wcf\util\HeaderUtil;
  * Shows the tag search form.
  * 
  * @author      Alexander Ebert
- * @copyright   2001-2018 WoltLab GmbH
+ * @copyright   2001-2019 WoltLab GmbH
  * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package     WoltLabSuite\Core\Form
  * @since       3.2
@@ -31,6 +31,11 @@ class TagSearchForm extends AbstractCaptchaForm {
         */
        public $languageID;
        
+       /**
+        * @inheritDoc
+        */
+       public $neededPermissions = ['user.tag.canViewTag'];
+       
        /**
         * @var TagCloud
         */
@@ -102,8 +107,14 @@ class TagSearchForm extends AbstractCaptchaForm {
        public function save() {
                parent::save();
                
+               $tagIDs = '';
+               foreach ($this->tags as $tag) {
+                       if (!empty($tagIDs)) $tagIDs .= '&';
+                       $tagIDs .= 'tagIDs[]=' . $tag->tagID;
+               }
+               
                HeaderUtil::redirect(
-                       LinkHandler::getInstance()->getLink('CombinedTagged', ['tagIDs' => array_keys($this->tags)]),
+                       LinkHandler::getInstance()->getLink('CombinedTagged', [], $tagIDs),
                        true,
                        true
                );
index 992a472052fa07c0de22eda648f7be5632e22a5c..223421dc8695c87860ebaa12088125680f58b25e 100644 (file)
@@ -6,28 +6,66 @@ use wcf\data\tag\Tag;
 use wcf\data\tag\TagList;
 use wcf\system\exception\IllegalLinkException;
 use wcf\system\exception\PermissionDeniedException;
+use wcf\system\tagging\ICombinedTaggable;
 use wcf\system\tagging\TypedTagCloud;
 use wcf\system\WCF;
 use wcf\util\ArrayUtil;
 use wcf\util\StringUtil;
 
 /**
- * Shows the a list of tagged objects.
+ * Shows the a list of objects matching a combination of tags.
  * 
- * @author     Marcel Werk
- * @copyright  2001-2018 WoltLab GmbH
- * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
- * @package    WoltLabSuite\Core\Page
+ * @author      Alexander Ebert
+ * @copyright   2001-2019 WoltLab GmbH
+ * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package     WoltLabSuite\Core\Page
  */
-class CombinedTaggedPage extends TaggedPage {
+class CombinedTaggedPage extends MultipleLinkPage {
+       /**
+        * @var ObjectType[]
+        */
+       public $availableObjectTypes = [];
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededModules = ['MODULE_TAGGING'];
+       
+       /**
+        * @inheritDoc
+        */
+       public $neededPermissions = ['user.tag.canViewTag'];
+       
+       /**
+        * @var ObjectType
+        */
+       public $objectType;
+       
+       /**
+        * @var ICombinedTaggable
+        */
+       public $processor;
+       
+       /**
+        * @var Tag[]
+        */
        public $tags = [];
+       
+       /**
+        * @var int[]
+        */
        public $tagIDs = [];
        
+       /**
+        * @var TypedTagCloud
+        */
+       public $tagCloud;
+       
        /**
         * @inheritDoc
         */
        public function readParameters() {
-               MultipleLinkPage::readParameters();
+               parent::readParameters();
                
                if (isset($_GET['tagIDs']) && is_array($this->tagIDs)) $this->tagIDs = ArrayUtil::toIntegerArray($_GET['tagIDs']);
                if (empty($this->tagIDs)) {
@@ -51,6 +89,10 @@ class CombinedTaggedPage extends TaggedPage {
                        if (!$objectType->validateOptions() || !$objectType->validatePermissions()) {
                                unset($this->availableObjectTypes[$key]);
                        }
+                       
+                       if (!$objectType->getProcessor() instanceof ICombinedTaggable) {
+                               unset($this->availableObjectTypes[$key]);
+                       }
                }
                
                if (empty($this->availableObjectTypes)) {
@@ -68,13 +110,15 @@ class CombinedTaggedPage extends TaggedPage {
                        // No object type provided, use the first object type.
                        $this->objectType = reset($this->availableObjectTypes);
                }
+               
+               $this->processor = $this->objectType->getProcessor();
        }
        
        /**
         * @inheritDoc
         */
        protected function initObjectList() {
-               $this->objectList = $this->objectType->getProcessor()->getObjectListFor($this->tags);
+               $this->objectList = $this->processor->getObjectListFor($this->tags);
        }
        
        /**
@@ -93,12 +137,12 @@ class CombinedTaggedPage extends TaggedPage {
                parent::assignVariables();
                
                WCF::getTPL()->assign([
-                       'tag' => $this->tag,
+                       'combinedTags' => $this->tags,
                        'tags' => $this->tagCloud->getTags(100),
                        'availableObjectTypes' => $this->availableObjectTypes,
                        'objectType' => $this->objectType->objectType,
-                       'resultListTemplateName' => $this->objectType->getProcessor()->getTemplateName(),
-                       'resultListApplication' => $this->objectType->getProcessor()->getApplication()
+                       'resultListTemplateName' => $this->processor->getTemplateName(),
+                       'resultListApplication' => $this->processor->getApplication()
                ]);
                
                if (count($this->objectList) === 0) {
index 3c2c29a489844b5c2043cc02afaebceed765bc11..24d5a3eb1a2285c54d1338078c90dce38266f2b0 100644 (file)
@@ -6,7 +6,7 @@ use wcf\data\tag\Tag;
  * Abstract implementation of a taggable with support for searches with multiple tags.
  * 
  * @author      Alexander Ebert
- * @copyright   2001-2018 WoltLab GmbH
+ * @copyright   2001-2019 WoltLab GmbH
  * @license     GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
  * @package     WoltLabSuite\Core\System\Tagging
  * @since       3.2
index 6745df00b0beca0e430ac7e41046f7c30d49ef17..460ede456323992f78e7c1fd3e7705ac09f2628f 100644 (file)
@@ -232,6 +232,16 @@ class TagEngine extends SingletonFactory {
                return key($languageIDs);
        }
        
+       /**
+        * @param Tag[] $tags
+        * @return int[]
+        */
+       public function getTagIDs($tags) {
+               return array_map(function($tag) {
+                       return $tag->tagID;
+               }, $tags);
+       }
+       
        /**
         * Generates the inner SQL statement to fetch object ids that have all listed
         * tags assigned to them.
@@ -241,9 +251,9 @@ class TagEngine extends SingletonFactory {
         * @return array
         * @since 3.2
         */
-       public function getSqlForObjectsByTags($objectType, array $tags) {
+       public function getSubselectForObjectsByTags($objectType, array $tags) {
                $parameters = [$this->getObjectTypeID($objectType)];
-               $tagIDs = implode(',', array_map(function(Tag $tag) {
+               $tagIDs = implode(',', array_map(function(Tag $tag) use (&$parameters) {
                        $parameters[] = $tag->tagID;
                        
                        return '?';
@@ -263,6 +273,20 @@ class TagEngine extends SingletonFactory {
                ];
        }
        
+       public function setJoinCondition($objectType, array $tags, PreparedStatementConditionBuilder $conditions) {
+               $conditions->add('tag_to_object.objectTypeID = ?', [$this->getObjectTypeID($objectType)]);
+               
+               $tagIDs = [];
+               foreach ($tags as $tag) {
+                       $tagIDs[] = $tag->tagID;
+               }
+               $conditions->add('tag_to_object.tagID IN (?)', [$tagIDs]);
+       }
+       
+       public function getSqlGroupAndHaving(array $tags) {
+               return 'GROUP BY tag_to_object.objectID HAVING COUNT(tag_to_object.objectID) = ' . count($tags);
+       }
+       
        /**
         * Returns the matching tags by name.
         * 
index f4ee2945d0bfd0cdb046b9d11cb3c3246e3ff13e..1f6b549a2cecdc75e45e90a8bc2e6b2753e49d8c 100644 (file)
@@ -4174,6 +4174,9 @@ Dateianhänge:
                <item name="wcf.search.type.com.woltlab.wcf.article"><![CDATA[Artikel]]></item>
                <item name="wcf.search.object.com.woltlab.wcf.page"><![CDATA[Seite]]></item>
                <item name="wcf.search.type.com.woltlab.wcf.page"><![CDATA[Seiten]]></item>
+               <item name="wcf.search.type.keywords"><![CDATA[Suche nach Begriffen]]></item>
+               <item name="wcf.search.type.tags"><![CDATA[Suche nach Tags]]></item>
+               <item name="wcf.search.type.tags.popular"><![CDATA[Beliebte Tags]]></item>
        </category>
        <category name="wcf.style">
                <item name="wcf.style.changeStyle"><![CDATA[Stil ändern]]></item>
@@ -4184,6 +4187,8 @@ Dateianhänge:
                <item name="wcf.style.colorPicker.button.apply"><![CDATA[Übernehmen]]></item>
        </category>
        <category name="wcf.tagging">
+               <item name="wcf.tagging.combinedTaggedObjects"><![CDATA[{implode from=$combinedTags item=tag glue=', '}„{$tag->name}“{/implode}]]></item>
+               <item name="wcf.tagging.combinedTaggedObjects.com.woltlab.wcf.article"><![CDATA[Artikel mit den Tags]]></item>
                <item name="wcf.tagging.tags"><![CDATA[Tags]]></item>
                <item name="wcf.tagging.tags.add"><![CDATA[Tags]]></item>
                <item name="wcf.tagging.tags.description"><![CDATA[Mehrere Tags müssen durch ein Komma getrennt werden.]]></item>
index ba623ea20081cd29406e4f70f463d96aaf2e1a4e..4f0bd47df7d0febfed82d264d7d1707d1536c019 100644 (file)
@@ -4176,6 +4176,9 @@ Attachments:
                <item name="wcf.search.type.com.woltlab.wcf.article"><![CDATA[Articles]]></item>
                <item name="wcf.search.object.com.woltlab.wcf.page"><![CDATA[Page]]></item>
                <item name="wcf.search.type.com.woltlab.wcf.page"><![CDATA[Pages]]></item>
+               <item name="wcf.search.type.keywords"><![CDATA[Search by Terms]]></item>
+               <item name="wcf.search.type.tags"><![CDATA[Search by Tags]]></item>
+               <item name="wcf.search.type.tags.popular"><![CDATA[Popular Tags]]></item>
        </category>
        <category name="wcf.style">
                <item name="wcf.style.changeStyle"><![CDATA[Change Style]]></item>
@@ -4186,13 +4189,15 @@ Attachments:
                <item name="wcf.style.colorPicker.button.apply"><![CDATA[Apply]]></item>
        </category>
        <category name="wcf.tagging">
+               <item name="wcf.tagging.combinedTaggedObjects"><![CDATA[{implode from=$combinedTags item=tag glue=', '}“{$tag->name}”{/implode}]]></item>
+               <item name="wcf.tagging.combinedTaggedObjects.com.woltlab.wcf.article"><![CDATA[Articles Tagged with]]></item>
                <item name="wcf.tagging.tags"><![CDATA[Tags]]></item>
                <item name="wcf.tagging.tags.add"><![CDATA[Tags]]></item>
                <item name="wcf.tagging.tags.description"><![CDATA[Separate multiple tags with a comma.]]></item>
                <item name="wcf.tagging.objectTypes"><![CDATA[Content]]></item>
                <item name="wcf.tagging.taggedObjects.noResults"><![CDATA[No items matched this tag.]]></item>
                <item name="wcf.tagging.objectType.com.woltlab.wcf.article"><![CDATA[Articles]]></item>
-               <item name="wcf.tagging.taggedObjects.com.woltlab.wcf.article"><![CDATA[Articles Tagged With “{$tag->name}”]]></item>
+               <item name="wcf.tagging.taggedObjects.com.woltlab.wcf.article"><![CDATA[Articles Tagged with “{$tag->name}”]]></item>
        </category>
        <category name="wcf.user">
                <item name="wcf.user.confirmEmail"><![CDATA[Confirm Email]]></item>