Added label support for articles
authorMarcel Werk <burntime@woltlab.com>
Wed, 5 Apr 2017 11:36:41 +0000 (13:36 +0200)
committerMarcel Werk <burntime@woltlab.com>
Wed, 5 Apr 2017 11:36:41 +0000 (13:36 +0200)
Closes #2184

18 files changed:
com.woltlab.wcf/objectType.xml
com.woltlab.wcf/templates/article.tpl
com.woltlab.wcf/templates/articleListItems.tpl
wcfsetup/install/files/acp/templates/articleAdd.tpl
wcfsetup/install/files/acp/templates/articleList.tpl
wcfsetup/install/files/js/WCF.Label.js
wcfsetup/install/files/lib/acp/form/ArticleAddForm.class.php
wcfsetup/install/files/lib/acp/form/ArticleEditForm.class.php
wcfsetup/install/files/lib/data/article/Article.class.php
wcfsetup/install/files/lib/data/article/ViewableArticle.class.php
wcfsetup/install/files/lib/data/article/ViewableArticleList.class.php
wcfsetup/install/files/lib/data/article/category/ArticleCategory.class.php
wcfsetup/install/files/lib/system/cache/builder/ArticleCategoryLabelCacheBuilder.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/label/object/ArticleLabelObjectHandler.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/label/object/type/ArticleCategoryLabelObjectTypeHandler.class.php [new file with mode: 0644]
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/setup/db/install.sql

index 5132dfb6fe78dcf8d115e84ad5992b0f4f02c93b..57c32ba69d6051059f23d65e3fc5083032b802ed 100644 (file)
                        <name>com.woltlab.wcf.article.content</name>
                        <definitionname>com.woltlab.wcf.message</definitionname>
                </type>
+               <type>
+                       <name>com.woltlab.wcf.article</name>
+                       <definitionname>com.woltlab.wcf.label.object</definitionname>
+                       <classname>wcf\system\label\object\ArticleLabelObjectHandler</classname>
+               </type>
+               <type>
+                       <name>com.woltlab.wcf.article.category</name>
+                       <definitionname>com.woltlab.wcf.label.objectType</definitionname>
+                       <classname>wcf\system\label\object\type\ArticleCategoryLabelObjectTypeHandler</classname>
+               </type>
                <!-- /articles -->
                
                <type>
index 43b6b41f25a36db85e6b43c0e1c66ea253fcf8fe..11126c9e44701189b985c731344eb01b22ced13a 100644 (file)
@@ -7,6 +7,17 @@
                <div class="contentHeaderTitle">
                        <h1 class="contentTitle" itemprop="name headline">{$articleContent->title}</h1>
                        <ul class="inlineList contentHeaderMetaData articleMetaData">
+                               {if $article->hasLabels()}
+                                       <li>
+                                               <span class="icon icon16 fa-tags"></span>
+                                               <ul class="labelList">
+                                                       {foreach from=$article->getLabels() item=label}
+                                                               <li><span class="label badge{if $label->getClassNames()} {$label->getClassNames()}{/if}">{lang}{$label->label}{/lang}</span></li>
+                                                       {/foreach}
+                                               </ul>
+                                       </li>
+                               {/if}
+                               
                                <li itemprop="author" itemscope itemtype="http://schema.org/Person">
                                        <span class="icon icon16 fa-user"></span>
                                        {if $article->userID}
index 503e09c690cc83055c1b724f2b05f6a94835ddd4..ed497e932ab2593a7f05bb661eeed3dea1c471fd 100644 (file)
                                                <div class="containerHeadline">
                                                        <h3 class="articleListTitle">{$article->getTitle()}</h3>
                                                        <ul class="inlineList articleListMetaData">
+                                                               {if $article->hasLabels()}
+                                                                       <li>
+                                                                               <span class="icon icon16 fa-tags"></span>
+                                                                               <ul class="labelList">
+                                                                                       {foreach from=$article->getLabels() item=label}
+                                                                                               <li><span class="label badge{if $label->getClassNames()} {$label->getClassNames()}{/if}">{lang}{$label->label}{/lang}</span></li>
+                                                                                       {/foreach}
+                                                                               </ul>
+                                                                       </li>
+                                                               {/if}
+                                                               
                                                                <li>
                                                                        <span class="icon icon16 fa-clock-o"></span>
                                                                        {@$article->time|time}
index f45f04b63873ff6ba1e8d772a5bd0e6142503165..02cde310a33618cf9d909553c0706f69c655d02f 100644 (file)
@@ -81,7 +81,7 @@
        {if $lastVersion}<p class="info">{lang}wcf.acp.article.lastVersion{/lang}</p>{/if}
 {/if}
 
-<form method="post" action="{if $action == 'add'}{link controller='ArticleAdd'}{/link}{else}{link controller='ArticleEdit' id=$articleID}{/link}{/if}">
+<form class="articleAddForm" method="post" action="{if $action == 'add'}{link controller='ArticleAdd'}{/link}{else}{link controller='ArticleEdit' id=$articleID}{/link}{/if}">
        <div class="section">
                <dl{if $errorField == 'categoryID'} class="formError"{/if}>
                        <dt><label for="categoryID">{lang}wcf.acp.article.category{/lang}</label></dt>
                        </dd>
                </dl>
                
+               {if $labelGroups|count}
+                       {foreach from=$labelGroups item=labelGroup}
+                               {if $labelGroup|count}
+                                       <dl{if $errorField == 'label' && $errorType[$labelGroup->groupID]|isset} class="formError"{/if}>
+                                               <dt><label>{$labelGroup->getTitle()}</label></dt>
+                                               <dd>
+                                                       <ul class="labelList jsOnly" data-object-id="{@$labelGroup->groupID}">
+                                                               <li class="dropdown labelChooser" id="labelGroup{@$labelGroup->groupID}" data-group-id="{@$labelGroup->groupID}" data-force-selection="{if $labelGroup->forceSelection}true{else}false{/if}">
+                                                                       <div class="dropdownToggle" data-toggle="labelGroup{@$labelGroup->groupID}"><span class="badge label">{lang}wcf.label.none{/lang}</span></div>
+                                                                       <div class="dropdownMenu">
+                                                                               <ul class="scrollableDropdownMenu">
+                                                                                       {foreach from=$labelGroup item=label}
+                                                                                               <li data-label-id="{@$label->labelID}"><span><span class="badge label{if $label->getClassNames()} {@$label->getClassNames()}{/if}">{lang}{$label->label}{/lang}</span></span></li>
+                                                                                       {/foreach}
+                                                                               </ul>
+                                                                       </div>
+                                                               </li>
+                                                       </ul>
+                                                       <noscript>
+                                                               <select name="labelIDs[{@$labelGroup->groupID}]">
+                                                                       {foreach from=$labelGroup item=label}
+                                                                               <option value="{@$label->labelID}">{lang}{$label->label}{/lang}</option>
+                                                                       {/foreach}
+                                                               </select>
+                                                       </noscript>
+                                                       {if $errorField == 'label' && $errorType[$labelGroup->groupID]|isset}
+                                                               <small class="innerError">
+                                                                       {if $errorType[$labelGroup->groupID] == 'missing'}
+                                                                               {lang}wcf.label.error.missing{/lang}
+                                                                       {else}
+                                                                               {lang}wcf.label.error.invalid{/lang}
+                                                                       {/if}
+                                                               </small>
+                                                       {/if}
+                                               </dd>
+                                       </dl>
+                               {/if}
+                       {/foreach}
+               {/if}
+               
                <dl{if $errorField == 'username'} class="formError"{/if}>
                        <dt><label for="username">{lang}wcf.acp.article.author{/lang}</label></dt>
                        <dd>
        </div>
 </form>
 
+{js application='wcf' file='WCF.Label' bundle='WCF.Combined'}
+<script data-relocate="true">
+       $(function() {
+               WCF.Language.addObject({
+                       'wcf.label.none': '{lang}wcf.label.none{/lang}',
+               });
+               
+               {if !$labelGroups|empty}
+                       new WCF.Label.ArticleLabelChooser({ {implode from=$labelGroupsToCategories key=__labelCategoryID item=labelGroupIDs}{@$__labelCategoryID}: [ {implode from=$labelGroupIDs item=labelGroupID}{@$labelGroupID}{/implode} ] {/implode} }, { {implode from=$labelIDs key=groupID item=labelID}{@$groupID}: {@$labelID}{/implode} }, '.articleAddForm');
+               {/if}
+       });
+</script>
+
 {include file='footer'}
index 74d4bd24df476400435aa8ec5ef19ac341ca72e3..4d6dbcf8bf0d9a3bb6ab4e360aaa0b001367494b 100644 (file)
                                                                </span>
                                                                
                                                                <div class="containerHeadline">
+                                                                       {if $article->hasLabels()}
+                                                                               <ul class="labelList" style="float: right; padding-left: 7px;">
+                                                                                       {foreach from=$article->getLabels() item=label}
+                                                                                               <li><span class="badge label{if $label->getClassNames()} {$label->getClassNames()}{/if}">{lang}{$label->label}{/lang}</span></li>
+                                                                                       {/foreach}
+                                                                               </ul>
+                                                                       {/if}
+                                                                       
                                                                        <h3>
                                                                                {if $article->isDeleted}<span class="badge label red jsIconDeleted">{lang}wcf.message.status.deleted{/lang}</span>{/if}
                                                                                {if $article->publicationStatus == 0}<span class="badge">{lang}wcf.acp.article.publicationStatus.unpublished{/lang}</span>{/if}
index 5075c437503c068f4b81584cf385a7a778195d80..a9fc4c5d9a4393f1e7f441e57ced28b04a5e89e0 100644 (file)
@@ -279,3 +279,65 @@ WCF.Label.Chooser = Class.extend({
                }
        }
 });
+
+/**
+ * Handles displaying label groups based on the selected categories.
+ */
+WCF.Label.ArticleLabelChooser = WCF.Label.Chooser.extend({
+       /**
+        * maps the available label group ids to the categories
+        * @var object
+        */
+       _labelGroupsToCategories: null,
+       
+       /**
+        * Initializes a new WCF.Label.ArticleLabelChooser object.
+        *
+        * @param       object          labelGroupsToCategories
+        * @param       object          selectedLabelIDs
+        * @param       string          containerSelector
+        * @param       string          submitButtonSelector
+        * @param       boolean         showWithoutSelection
+        */
+       init: function(labelGroupsToCategories, selectedLabelIDs, containerSelector, submitButtonSelector, showWithoutSelection) {
+               this._super(selectedLabelIDs, containerSelector, submitButtonSelector, showWithoutSelection);
+               this._labelGroupsToCategories = labelGroupsToCategories;
+               
+               this._updateLabelGroups();
+               
+               $('#categoryID').change($.proxy(this._updateLabelGroups, this));
+       },
+       
+       /**
+        * Updates the visible label groups based on the selected categories.
+        */
+       _updateLabelGroups: function() {
+               // hide all label choosers first
+               $('.labelChooser').each(function(index, element) {
+                       $(element).parents('dl:eq(0)').hide();
+               })
+               
+               var visibleGroupIDs = [];
+               var categoryID = parseInt($('#categoryID').val());
+               
+               if (this._labelGroupsToCategories[categoryID]) {
+                       for (var i = 0, length = this._labelGroupsToCategories[categoryID].length; i < length; i++) {
+                               $('#labelGroup' + this._labelGroupsToCategories[categoryID][i]).parents('dl:eq(0)').show();
+                       }
+               }
+       },
+       
+       /**
+        * @see WCF.Label.Chooser._submit()
+        */
+       _submit: function() {
+               // delete non-selected groups to avoid sumitting these labels
+               for (var groupID in this._groups) {
+                       if (!this._groups[groupID].is(':visible')) {
+                               delete this._groups[groupID];
+                       }
+               }
+               
+               this._super();
+       }
+});
\ No newline at end of file
index eee902bee1d6c8af475b07d4dcefd61eaabf7422..93173309ce586a5b292d5f133255ac1fee0e8342 100644 (file)
@@ -4,13 +4,16 @@ use wcf\data\article\category\ArticleCategory;
 use wcf\data\article\Article;
 use wcf\data\article\ArticleAction;
 use wcf\data\category\CategoryNodeTree;
+use wcf\data\label\group\ViewableLabelGroup;
 use wcf\data\language\Language;
 use wcf\data\media\Media;
 use wcf\data\media\ViewableMediaList;
 use wcf\data\user\User;
 use wcf\form\AbstractForm;
+use wcf\system\cache\builder\ArticleCategoryLabelCacheBuilder;
 use wcf\system\exception\UserInputException;
 use wcf\system\html\input\HtmlInputProcessor;
+use wcf\system\label\object\ArticleLabelObjectHandler;
 use wcf\system\language\LanguageFactory;
 use wcf\system\request\LinkHandler;
 use wcf\system\WCF;
@@ -163,6 +166,24 @@ class ArticleAddForm extends AbstractForm {
         */
        public $availableLanguages = [];
        
+       /**
+        * label group list
+        * @var ViewableLabelGroup[]
+        */
+       public $labelGroups;
+       
+       /**
+        * list of label ids
+        * @var integer[]
+        */
+       public $labelIDs = [];
+       
+       /**
+        * maps the label group ids to the article category ids
+        * @var array
+        */
+       public $labelGroupsToCategories = [];
+       
        /**
         * @inheritDoc
         */
@@ -175,6 +196,9 @@ class ArticleAddForm extends AbstractForm {
                $this->availableLanguages = LanguageFactory::getInstance()->getLanguages();
                
                $this->readMultilingualSetting();
+               
+               // labels
+               ArticleLabelObjectHandler::getInstance()->setCategoryIDs(ArticleCategory::getAccessibleCategoryIDs());
        }
        
        /**
@@ -199,6 +223,7 @@ class ArticleAddForm extends AbstractForm {
                parent::readFormParameters();
                
                $this->enableComments = 0;
+               if (isset($_POST['labelIDs']) && is_array($_POST['labelIDs'])) $this->labelIDs = $_POST['labelIDs'];
                if (isset($_POST['username'])) $this->username = StringUtil::trim($_POST['username']);
                if (isset($_POST['time'])) {
                        $this->time = $_POST['time'];
@@ -336,6 +361,29 @@ class ArticleAddForm extends AbstractForm {
                        $this->htmlInputProcessors[0] = new HtmlInputProcessor();
                        $this->htmlInputProcessors[0]->process($this->content[0], 'com.woltlab.wcf.article.content', 0);
                }
+               
+               $this->validateLabelIDs();
+       }
+       
+       /**
+        * Validates the selected labels.
+        */
+       protected function validateLabelIDs() {
+               // set category ids to selected category ids for validation
+               ArticleLabelObjectHandler::getInstance()->setCategoryIDs([$this->categoryID]);
+               
+               $validationResult = ArticleLabelObjectHandler::getInstance()->validateLabelIDs($this->labelIDs, 'canSetLabel', false);
+               
+               // reset category ids to accessible category ids
+               ArticleLabelObjectHandler::getInstance()->setCategoryIDs(ArticleCategory::getAccessibleCategoryIDs());
+               
+               if (!empty($validationResult[0])) {
+                       throw new UserInputException('labelIDs');
+               }
+               
+               if (!empty($validationResult)) {
+                       throw new UserInputException('label', $validationResult);
+               }
        }
        
        /**
@@ -378,11 +426,16 @@ class ArticleAddForm extends AbstractForm {
                        'enableComments' => $this->enableComments,
                        'userID' => $this->author->userID,
                        'username' => $this->author->username,
-                       'isMultilingual' => $this->isMultilingual
+                       'isMultilingual' => $this->isMultilingual,
+                       'hasLabels' => empty($this->labelIDs) ? 0 : 1
                ];
                
                $this->objectAction = new ArticleAction([], 'create', ['data' => array_merge($this->additionalFields, $data), 'content' => $content]);
-               $this->objectAction->executeAction();
+               $article = $this->objectAction->executeAction()['returnValues'];
+               // save labels
+               if (!empty($this->labelIDs)) {
+                       ArticleLabelObjectHandler::getInstance()->setLabels($this->labelIDs, $article->articleID);
+               }
                
                // call saved event
                $this->saved();
@@ -406,6 +459,9 @@ class ArticleAddForm extends AbstractForm {
        public function readData() {
                parent::readData();
                
+               $this->labelGroupsToCategories = ArticleCategoryLabelCacheBuilder::getInstance()->getData();
+               $this->labelGroups = ArticleCategory::getAccessibleLabelGroups();
+                               
                if (empty($_POST)) {
                        $this->setDefaultValues();
                }
@@ -449,7 +505,10 @@ class ArticleAddForm extends AbstractForm {
                        'teaser' => $this->teaser,
                        'content' => $this->content,
                        'availableLanguages' => $this->availableLanguages,
-                       'categoryNodeList' => (new CategoryNodeTree('com.woltlab.wcf.article.category'))->getIterator()
+                       'categoryNodeList' => (new CategoryNodeTree('com.woltlab.wcf.article.category'))->getIterator(),
+                       'labelIDs' => $this->labelIDs,
+                       'labelGroups' => $this->labelGroups,
+                       'labelGroupsToCategories' => $this->labelGroupsToCategories
                ]);
        }
 }
index 1289d1cc3728748b783204b7fdf658d4dadf17cc..c26cd709ab6dc47eb8455c519f2924207c4d698b 100644 (file)
@@ -5,6 +5,7 @@ use wcf\data\article\ArticleAction;
 use wcf\form\AbstractForm;
 use wcf\system\exception\IllegalLinkException;
 use wcf\system\exception\PermissionDeniedException;
+use wcf\system\label\object\ArticleLabelObjectHandler;
 use wcf\system\language\LanguageFactory;
 use wcf\system\tagging\TagEngine;
 use wcf\system\version\VersionTracker;
@@ -71,6 +72,10 @@ class ArticleEditForm extends ArticleAddForm {
        public function save() {
                AbstractForm::save();
                
+               // save labels
+               ArticleLabelObjectHandler::getInstance()->setLabels($this->labelIDs, $this->article->articleID);
+               $labelIDs = ArticleLabelObjectHandler::getInstance()->getAssignedLabels([$this->article->articleID], false);
+                               
                $content = [];
                if ($this->isMultilingual) {
                        foreach (LanguageFactory::getInstance()->getLanguages() as $language) {
@@ -104,7 +109,8 @@ class ArticleEditForm extends ArticleAddForm {
                        'enableComments' => $this->enableComments,
                        'userID' => $this->author->userID,
                        'username' => $this->author->username,
-                       'time' => $this->timeObj->getTimestamp()
+                       'time' => $this->timeObj->getTimestamp(),
+                       'hasLabels' => (isset($labelIDs[$this->article->articleID]) && !empty($labelIDs[$this->article->articleID])) ? 1 : 0
                ];
                
                $this->objectAction = new ArticleAction([$this->article], 'update', ['data' => array_merge($this->additionalFields, $data), 'content' => $content]);
@@ -164,6 +170,14 @@ class ArticleEditForm extends ArticleAddForm {
                        }
                        
                        $this->readImages();
+                       
+                       // labels
+                       $assignedLabels = ArticleLabelObjectHandler::getInstance()->getAssignedLabels([$this->article->articleID], true);
+                       if (isset($assignedLabels[$this->article->articleID])) {
+                               foreach ($assignedLabels[$this->article->articleID] as $label) {
+                                       $this->labelIDs[$label->groupID] = $label->labelID;
+                               }
+                       }
                }
        }
        
index a8f6203ab315288a024d2259c462ee99fae084d9..291eac9d3f21233c0dd6d7c21b068f695df21161 100644 (file)
@@ -28,6 +28,7 @@ use wcf\system\WCF;
  * @property-read      integer         $views                  number of times the article has been viewed
  * @property-read      integer         $cumulativeLikes        cumulative result of likes (counting `+1`) and dislikes (counting `-1`) for the article
  * @property-read      integer         $isDeleted              is 1 if the article is in trash bin, otherwise 0
+ * @property-read      integer         $hasLabels              is `1` if labels are assigned to the article
  */
 class Article extends DatabaseObject implements ILinkableObject {
        /**
index a03231f9efce878e91f464bdf4d72616fe72fcde..eb1a8509dec91ef8289755183ee18d0e0cbdeef0 100644 (file)
@@ -3,6 +3,7 @@ namespace wcf\data\article;
 use wcf\data\article\category\ArticleCategory;
 use wcf\data\article\content\ArticleContent;
 use wcf\data\article\content\ViewableArticleContent;
+use wcf\data\label\Label;
 use wcf\data\media\ViewableMedia;
 use wcf\data\user\User;
 use wcf\data\user\UserProfile;
@@ -51,6 +52,12 @@ class ViewableArticle extends DatabaseObjectDecorator {
         */
        protected static $unreadArticles;
        
+       /**
+        * list of assigned labels
+        * @var Label[]
+        */
+       protected $labels = [];
+       
        /**
         * Returns a specific article decorated as viewable article or `null` if it does not exist.
         *
@@ -157,6 +164,33 @@ class ViewableArticle extends DatabaseObjectDecorator {
                return $this->time > $this->getVisitTime();
        }
        
+       /**
+        * Adds a label.
+        *
+        * @param       Label   $label
+        */
+       public function addLabel(Label $label) {
+               $this->labels[$label->labelID] = $label;
+       }
+       
+       /**
+        * Returns a list of labels.
+        *
+        * @return      Label[]
+        */
+       public function getLabels() {
+               return $this->labels;
+       }
+       
+       /**
+        * Returns true if one or more labels are assigned to this article.
+        *
+        * @return      boolean
+        */
+       public function hasLabels() {
+               return !empty($this->labels);
+       }
+       
        /**
         * Returns the number of unread articles.
         *
index cf1b3bf134391effeb3eb4226be986bad2b3b483..762a9de7d91ec166e908c96526f026f84f43aa14 100644 (file)
@@ -2,6 +2,7 @@
 namespace wcf\data\article;
 use wcf\data\article\content\ViewableArticleContentList;
 use wcf\system\cache\runtime\UserProfileRuntimeCache;
+use wcf\system\label\object\ArticleLabelObjectHandler;
 use wcf\system\like\LikeHandler;
 use wcf\system\visitTracker\VisitTracker;
 use wcf\system\WCF;
@@ -57,11 +58,14 @@ class ViewableArticleList extends ArticleList {
        public function readObjects() {
                parent::readObjects();
                
-               $userIDs = [];
+               $userIDs = $articleIDs = [];
                foreach ($this->getObjects() as $article) {
                        if ($article->userID) {
                                $userIDs[] = $article->userID;
                        }
+                       if ($article->hasLabels) {
+                               $articleIDs[] = $article->articleID;
+                       }
                }
                
                // cache user profiles
@@ -79,6 +83,16 @@ class ViewableArticleList extends ArticleList {
                                $this->objects[$articleContent->articleID]->setArticleContent($articleContent);
                        }
                }
+               
+               // get labels
+               if (!empty($articleIDs)) {
+                       $assignedLabels = ArticleLabelObjectHandler::getInstance()->getAssignedLabels($articleIDs);
+                       foreach ($assignedLabels as $articleID => $labels) {
+                               foreach ($labels as $label) {
+                                       $this->objects[$articleID]->addLabel($label);
+                               }
+                       }
+               }
        }
        
        /**
index c014dc62858330bdf700d6f94994207656c3affc..4024fd2a13229369764cb4c2c9087b52ab7b4b0e 100644 (file)
@@ -1,12 +1,15 @@
 <?php
 namespace wcf\data\article\category;
 use wcf\data\category\AbstractDecoratedCategory;
+use wcf\data\label\group\ViewableLabelGroup;
 use wcf\data\user\User;
 use wcf\data\user\UserProfile;
 use wcf\data\IAccessibleObject;
 use wcf\data\ITitledLinkObject;
+use wcf\system\cache\builder\ArticleCategoryLabelCacheBuilder;
 use wcf\system\category\CategoryHandler;
 use wcf\system\category\CategoryPermissionHandler;
+use wcf\system\label\LabelHandler;
 use wcf\system\request\LinkHandler;
 use wcf\system\WCF;
 
@@ -118,4 +121,24 @@ class ArticleCategory extends AbstractDecoratedCategory implements IAccessibleOb
                
                return $categoryIDs;
        }
+       
+       /**
+        * Returns the label groups for all accessible categories.
+        *
+        * @return      ViewableLabelGroup[]
+        */
+       public static function getAccessibleLabelGroups() {
+               $labelGroupsToCategories = ArticleCategoryLabelCacheBuilder::getInstance()->getData();
+               $accessibleCategoryIDs = self::getAccessibleCategoryIDs();
+               
+               $groupIDs = [];
+               foreach ($labelGroupsToCategories as $categoryID => $__groupIDs) {
+                       if (in_array($categoryID, $accessibleCategoryIDs)) {
+                               $groupIDs = array_merge($groupIDs, $__groupIDs);
+                       }
+               }
+               if (empty($groupIDs)) return [];
+               
+               return LabelHandler::getInstance()->getLabelGroups(array_unique($groupIDs));
+       }
 }
diff --git a/wcfsetup/install/files/lib/system/cache/builder/ArticleCategoryLabelCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/ArticleCategoryLabelCacheBuilder.class.php
new file mode 100644 (file)
index 0000000..a72fa6c
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+namespace wcf\system\cache\builder;
+use wcf\data\object\type\ObjectTypeCache;
+use wcf\system\category\CategoryHandler;
+use wcf\system\database\util\PreparedStatementConditionBuilder;
+use wcf\system\WCF;
+
+/**
+ * Caches the available label group ids for article categories.
+ *
+ * @author     Marcel Werk
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Cache\Builder
+ * @since       3.1
+ */
+class ArticleCategoryLabelCacheBuilder extends AbstractCacheBuilder {
+       /**
+        * @inheritDoc
+        */
+       protected function rebuild(array $parameters) {
+               $conditionBuilder = new PreparedStatementConditionBuilder();
+               $conditionBuilder->add('objectTypeID = ?', [ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.label.objectType', 'com.woltlab.wcf.article.category')->objectTypeID]);
+               $conditionBuilder->add('objectID IN (SELECT categoryID FROM wcf'.WCF_N.'_category WHERE objectTypeID = ?)', [CategoryHandler::getInstance()->getObjectTypeByName('com.woltlab.wcf.article.category')->objectTypeID]);
+               
+               $sql = "SELECT  groupID, objectID
+                       FROM    wcf".WCF_N."_label_group_to_object
+                       ".$conditionBuilder;
+               $statement = WCF::getDB()->prepareStatement($sql);
+               $statement->execute($conditionBuilder->getParameters());
+               
+               return $statement->fetchMap('objectID', 'groupID', false);
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/label/object/ArticleLabelObjectHandler.class.php b/wcfsetup/install/files/lib/system/label/object/ArticleLabelObjectHandler.class.php
new file mode 100644 (file)
index 0000000..d0398ae
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+namespace wcf\system\label\object;
+use wcf\system\cache\builder\ArticleCategoryLabelCacheBuilder;
+use wcf\system\label\LabelHandler;
+
+/**
+ * Label handler for articles.
+ *
+ * @author     Marcel Werk
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Label\Object
+ * @since       3.1
+ */
+class ArticleLabelObjectHandler extends AbstractLabelObjectHandler {
+       /**
+        * @inheritDoc
+        */
+       protected $objectType = 'com.woltlab.wcf.article';
+       
+       /**
+        * Sets the label groups available for the categories with the given ids.
+        * 
+        * @param       integer[]               $categoryIDs
+        */
+       public function setCategoryIDs($categoryIDs) {
+               $labelGroupsToCategories = ArticleCategoryLabelCacheBuilder::getInstance()->getData();
+               
+               $groupIDs = [];
+               foreach ($labelGroupsToCategories as $categoryID => $__groupIDs) {
+                       if (in_array($categoryID, $categoryIDs)) {
+                               $groupIDs = array_merge($groupIDs, $__groupIDs);
+                       }
+               }
+               
+               $this->labelGroups = [];
+               if (!empty($groupIDs)) {
+                       $this->labelGroups = LabelHandler::getInstance()->getLabelGroups(array_unique($groupIDs));
+               }
+       }
+}
diff --git a/wcfsetup/install/files/lib/system/label/object/type/ArticleCategoryLabelObjectTypeHandler.class.php b/wcfsetup/install/files/lib/system/label/object/type/ArticleCategoryLabelObjectTypeHandler.class.php
new file mode 100644 (file)
index 0000000..5687b73
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+namespace wcf\system\label\object\type;
+use wcf\data\article\category\ArticleCategoryNode;
+use wcf\data\article\category\ArticleCategoryNodeTree;
+use wcf\system\cache\builder\ArticleCategoryLabelCacheBuilder;
+
+/**
+ * Object type handler for article categories.
+ *
+ * @author     Marcel Werk
+ * @copyright  2001-2017 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    WoltLabSuite\Core\System\Label\Object\Type
+ * @since       3.1
+ */
+class ArticleCategoryLabelObjectTypeHandler extends AbstractLabelObjectTypeHandler {
+       /**
+        * category list
+        * @var \RecursiveIteratorIterator
+        */
+       public $categoryList;
+       
+       /**
+        * @inheritDoc
+        */
+       protected function init() {
+               $categoryTree = new ArticleCategoryNodeTree('com.woltlab.wcf.article.category');
+               $this->categoryList = $categoryTree->getIterator();
+               $this->categoryList->setMaxDepth(0);
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function setObjectTypeID($objectTypeID) {
+               parent::setObjectTypeID($objectTypeID);
+               
+               $this->container = new LabelObjectTypeContainer($this->objectTypeID);
+               /** @var ArticleCategoryNode $category */
+               foreach ($this->categoryList as $category) {
+                       $this->container->add(new LabelObjectType($category->getTitle(), $category->categoryID, 0));
+                       foreach ($category as $subCategory) {
+                               $this->container->add(new LabelObjectType($subCategory->getTitle(), $subCategory->categoryID, 1));
+                               foreach ($subCategory as $subSubCategory) {
+                                       $this->container->add(new LabelObjectType($subSubCategory->getTitle(), $subSubCategory->categoryID, 2));
+                               }
+                       }
+               }
+       }
+       
+       /**
+        * @inheritDoc
+        */
+       public function save() {
+               ArticleCategoryLabelCacheBuilder::getInstance()->reset();
+       }
+}
index 1bea3e1fb17b3d8f32c72c26b7f38ffc38c18ae4..8208b650d1e30a90780116ddf58f443bffd0d50b 100644 (file)
                <item name="wcf.acp.label.showOrder.description"><![CDATA[Reihenfolge des Labels innerhalb seiner Labelgruppe. Wenn {if LANGUAGE_USE_INFORMAL_VARIANT}du{else}Sie{/if} das Feld leer {if LANGUAGE_USE_INFORMAL_VARIANT}lässt{else}lassen{/if}, wird das Label an letzter Position einsortiert.]]></item>
                <item name="wcf.acp.label.sortAfterGroupFiltering"><![CDATA[Wenn {if LANGUAGE_USE_INFORMAL_VARIANT}du{else}Sie{/if} die Label-Liste nur nach einer bestimmten Labelgruppe {if LANGUAGE_USE_INFORMAL_VARIANT}filterst{else}filtern{/if}, {if LANGUAGE_USE_INFORMAL_VARIANT}kannst du{else}können Sie{/if} die Labels innerhalb dieser Gruppe durch Ziehen und Loslassen sortieren.]]></item>
                <item name="wcf.acp.label.filter"><![CDATA[Filter]]></item>
+               <item name="wcf.acp.label.container.com.woltlab.wcf.article.category"><![CDATA[Artikel]]></item>
        </category>
        
        <category name="wcf.acp.language">
index c9460dc35702521c363683032141a1fbbc95bb65..718245d98b46b37ed997cca3b8576fbecc9b7bb1 100644 (file)
@@ -622,6 +622,7 @@ Examples for medium ID detection:
                <item name="wcf.acp.label.showOrder.description"><![CDATA[Display order of the label in its label group. If you leave this field empty, the label will be placed at the last position.]]></item>
                <item name="wcf.acp.label.sortAfterGroupFiltering"><![CDATA[If you only filter the label list by a certain label group, you can sort the labels in this group using drag and drop.]]></item>
                <item name="wcf.acp.label.filter"><![CDATA[Filter]]></item>
+               <item name="wcf.acp.label.container.com.woltlab.wcf.article.category"><![CDATA[Articles]]></item>
        </category>
        
        <category name="wcf.acp.language">
index f72f9f7c060a83c82e6ff2db179f3b5eb3200352..4617265ab4ea35684ff85c1bd9121f117c7afae8 100644 (file)
@@ -168,6 +168,7 @@ CREATE TABLE wcf1_article (
        views MEDIUMINT(7) NOT NULL DEFAULT 0,
        cumulativeLikes MEDIUMINT(7) NOT NULL DEFAULT 0,
        isDeleted TINYINT(1) NOT NULL DEFAULT 0,
+       hasLabels TINYINT(1) NOT NULL DEFAULT 0,
        
        KEY (time)
 );