From: Marcel Werk Date: Mon, 20 May 2013 22:52:02 +0000 (+0200) Subject: Merged com.woltlab.wcf.tagging into WCF X-Git-Tag: 2.0.0_Beta_1~120 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=04c06e85ba3f17fdbea4513350fa84616cc852a2;p=GitHub%2FWoltLab%2FWCF.git Merged com.woltlab.wcf.tagging into WCF --- diff --git a/com.woltlab.wcf/acpMenu.xml b/com.woltlab.wcf/acpMenu.xml index 2f042a83f9..431929bd46 100644 --- a/com.woltlab.wcf/acpMenu.xml +++ b/com.woltlab.wcf/acpMenu.xml @@ -522,6 +522,24 @@ 4 + + wcf.acp.menu.link.content + + + + + wcf.acp.menu.link.tag + admin.content.tag.canManageTag + 1 + + + + + wcf.acp.menu.link.tag + admin.content.tag.canManageTag + 2 + + 5 diff --git a/com.woltlab.wcf/objectTypeDefinition.xml b/com.woltlab.wcf/objectTypeDefinition.xml index 5498a02015..2596592e8c 100644 --- a/com.woltlab.wcf/objectTypeDefinition.xml +++ b/com.woltlab.wcf/objectTypeDefinition.xml @@ -109,5 +109,10 @@ com.woltlab.wcf.label.objectType wcf\system\label\object\type\ILabelObjectTypeHandler + + + com.woltlab.wcf.tagging.taggableObject + wcf\system\tagging\ITaggable + diff --git a/com.woltlab.wcf/option.xml b/com.woltlab.wcf/option.xml index d3ffe14cf6..e9a602c187 100644 --- a/com.woltlab.wcf/option.xml +++ b/com.woltlab.wcf/option.xml @@ -297,6 +297,12 @@ 1 + + + + + + diff --git a/com.woltlab.wcf/template/tagCloudBox.tpl b/com.woltlab.wcf/template/tagCloudBox.tpl new file mode 100644 index 0000000000..ded4835c77 --- /dev/null +++ b/com.woltlab.wcf/template/tagCloudBox.tpl @@ -0,0 +1,9 @@ +{hascontent} + +{/hascontent} \ No newline at end of file diff --git a/com.woltlab.wcf/template/tagInput.tpl b/com.woltlab.wcf/template/tagInput.tpl new file mode 100644 index 0000000000..910eb81783 --- /dev/null +++ b/com.woltlab.wcf/template/tagInput.tpl @@ -0,0 +1,20 @@ +
+
+
+
+ + {lang}wcf.tagging.tags.description{/lang} +
+
+ + \ No newline at end of file diff --git a/com.woltlab.wcf/template/tagged.tpl b/com.woltlab.wcf/template/tagged.tpl new file mode 100644 index 0000000000..1c8a3cce8d --- /dev/null +++ b/com.woltlab.wcf/template/tagged.tpl @@ -0,0 +1,88 @@ +{include file='documentHeader'} + + + {lang}wcf.tagging.taggedObjects.{@$objectType}{/lang} - {PAGE_TITLE|language} + + {include file='headInclude'} + + {if $pageNo < $pages} + + {/if} + {if $pageNo > 1} + + {/if} + + + + + +{capture assign='sidebar'} +
+ {lang}wcf.tagging.objectTypes{/lang} + + +
+ +
+ {lang}wcf.tagging.tags{/lang} + + +
+{/capture} + +{include file='header' sidebarOrientation='left'} + +
+

{lang}wcf.tagging.taggedObjects.{@$objectType}{/lang}

+
+ +{include file='userNotice'} + +
+ {pages print=true assign=pagesLinks controller='Tagged' object=$tag link="objectType=$objectType&pageNo=%d"} + + {hascontent} + + {/hascontent} +
+ +{if $items} + {include file=$resultListTemplateName application=$resultListApplication} +{else} +

{lang}wcf.tagging.taggedObjects.noResults{/lang}

+{/if} + +
+ {@$pagesLinks} + + {hascontent} + + {/hascontent} +
+ +{include file='footer'} + + + \ No newline at end of file diff --git a/com.woltlab.wcf/userGroupOption.xml b/com.woltlab.wcf/userGroupOption.xml index 9fb1123cfb..9964a11b27 100644 --- a/com.woltlab.wcf/userGroupOption.xml +++ b/com.woltlab.wcf/userGroupOption.xml @@ -95,6 +95,9 @@ admin.content + + admin.content + admin @@ -545,6 +548,13 @@ png]]> 0 1 + + diff --git a/wcfsetup/install/files/acp/templates/tagAdd.tpl b/wcfsetup/install/files/acp/templates/tagAdd.tpl new file mode 100644 index 0000000000..ae76dceba2 --- /dev/null +++ b/wcfsetup/install/files/acp/templates/tagAdd.tpl @@ -0,0 +1,120 @@ +{include file='header' pageTitle='wcf.acp.tag.'|concat:$action} + +
+

{lang}wcf.acp.tag.{$action}{/lang}

+
+ +{if $errorField} +

{lang}wcf.global.form.error{/lang}

+{/if} + +{if $success|isset} +

{lang}wcf.global.success.{$action}{/lang}

+{/if} + +
+ {hascontent} + + {/hascontent} +
+ +
+
+
+ {lang}wcf.global.form.data{/lang} + + +
+
+ + {if $errorField == 'name'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {elseif $errorType == 'duplicate'} + {lang}wcf.acp.tag.error.name.duplicate{/lang} + {/if} + + {/if} +
+ + + {hascontent} + +
+
+ + {if $errorField == 'languageID'} + + {lang}wcf.acp.tag.error.languageID.{$errorType}{/lang} + + {/if} +
+ + {/hascontent} + + {if !$tagObj|isset || $tagObj->synonymFor === null} +
+
+
+
+ + {if $errorField == 'synonyms'} + + {if $errorType == 'duplicate'} + {lang}wcf.acp.tag.error.synonym.duplicate{/lang} + {/if} + + {/if} +
+
+ + + + {elseif $tagObj|isset} +
+
+
+ {lang}wcf.acp.tag.synonyms.isSynonym{/lang} +
+
+ {/if} + + {event name='dataFields'} +
+ + {event name='fieldsets'} +
+ +
+ +
+
+ +{include file='footer'} \ No newline at end of file diff --git a/wcfsetup/install/files/acp/templates/tagList.tpl b/wcfsetup/install/files/acp/templates/tagList.tpl new file mode 100644 index 0000000000..eda77f563c --- /dev/null +++ b/wcfsetup/install/files/acp/templates/tagList.tpl @@ -0,0 +1,104 @@ +{include file='header' pageTitle='wcf.acp.tag.list'} + + + +
+

{lang}wcf.acp.tag.list{/lang}

+
+ +{if $items} +
+
+
{lang}wcf.acp.tag.list.search{/lang} +
+
+
+ +
+
+
+ +
+ + {@SID_INPUT_TAG} +
+
+
+{/if} + +
+ {pages print=true assign=pagesLinks controller="TagList" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder&search=$search"} + + +
+ +{if $objects|count} +
+
+

{lang}wcf.acp.tag.list{/lang} {#$items}

+
+ + + + + + + + + + + {event name='columnHeads'} + + + + + {foreach from=$objects item=tag} + + + + + + + + + {event name='columns'} + + {/foreach} + +
{lang}wcf.global.objectID{/lang}{lang}wcf.acp.tag.name{/lang}{lang}wcf.acp.tag.usageCount{/lang}{lang}wcf.acp.tag.languageID{/lang}{lang}wcf.acp.tag.synonymFor{/lang}
+ + + + {event name='rowButtons'} + {#$tag->tagID}{$tag->name}{if $tag->synonymFor === null}{#$tag->usageCount}{/if}{if $tag->languageName !== null}{$tag->languageName} ({$tag->languageCode}){/if}{if $tag->synonymFor !== null}{$tag->synonymName}{/if}
+ +
+ +
+ {@$pagesLinks} + + +
+{else} +

{lang}wcf.acp.tag.noneAvailable{/lang}

+{/if} + +{include file='footer'} diff --git a/wcfsetup/install/files/js/WCF.Tagging.js b/wcfsetup/install/files/js/WCF.Tagging.js new file mode 100644 index 0000000000..6521c8a0b8 --- /dev/null +++ b/wcfsetup/install/files/js/WCF.Tagging.js @@ -0,0 +1,152 @@ +/** + * Tagging System for WCF + * + * @author Alexander Ebert + * @copyright 2001-2013 WoltLab GmbH + * @license GNU Lesser General Public License + */ + +/** + * Namespace for tagging related functions. + */ +WCF.Tagging = {}; + +/** + * Editable tag list. + * + * @see WCF.EditableItemList + */ +WCF.Tagging.TagList = WCF.EditableItemList.extend({ + /** + * @see WCF.EditableItemList._className + */ + _className: 'wcf\\data\\tag\\TagAction', + + /** + * maximum tag length + * @var integer + */ + _maxLength: 0, + + /** + * @see WCF.EditableItemList.init() + */ + init: function(itemListSelector, searchInputSelector, maxLength) { + this._allowCustomInput = true; + this._maxLength = maxLength; + + this._super(itemListSelector, searchInputSelector); + + this._data = [ ]; + this._search = new WCF.Tagging.TagSearch(this._searchInput, $.proxy(this.addItem, this)); + this._itemList.addClass('tagList'); + }, + + /** + * @see WCF.EditableItemList._keyDown() + */ + _keyDown: function(event) { + if (this._super(event)) { + // ignore submit event + if (event === null) { + return true; + } + + var $keyCode = event.which; + // allow [backspace], [escape], [enter] and [delete] + if ($keyCode === 8 || $keyCode === 27 || $keyCode === 13 || $keyCode === 46) { + return true; + } + else if ($keyCode > 36 && $keyCode < 41) { + // allow arrow keys (37-40) + return true; + } + + if (this._searchInput.val().length >= this._maxLength) { + return false; + } + + return true; + } + + return false; + }, + + /** + * @see WCF.EditableItemList._submit() + */ + _submit: function() { + this._super(); + + for (var $i = 0, $length = this._data.length; $i < $length; $i++) { + // deleting items leaves crappy indices + if (this._data[$i]) { + $('').appendTo(this._form); + } + }; + }, + + /** + * @see WCF.EditableItemList.addItem() + */ + addItem: function(data) { + // enforce max length by trimming values + if (!data.objectID && data.label.length > this._maxLength) { + data.label = data.label.substr(0, this._maxLength); + } + + var result = this._super(data); + $(this._itemList).find('.badge:not(tag)').addClass('tag'); + + return result; + }, + + /** + * @see WCF.EditableItemList._addItem() + */ + _addItem: function(objectID, label) { + this._data.push(label); + }, + + /** + * @see WCF.EditableItemList._removeItem() + */ + _removeItem: function(objectID, label) { + for (var $i = 0, $length = this._data.length; $i < $length; $i++) { + if (this._data[$i] === label) { + delete this._data[$i]; + return; + } + } + }, + + /** + * @see WCF.EditableItemList.load() + */ + load: function(data) { + if (data && data.length) { + for (var $i = 0, $length = data.length; $i < $length; $i++) { + this.addItem({ objectID: 0, label: data[$i] }); + } + } + } +}); + +/** + * Search handler for tags. + * + * @see WCF.Search.Base + */ +WCF.Tagging.TagSearch = WCF.Search.Base.extend({ + /** + * @see WCF.Search.Base._className + */ + _className: 'wcf\\data\\tag\\TagAction', + + /** + * @see WCF.Search.Base.init() + */ + init: function(searchInput, callback, excludedSearchValues, commaSeperated) { + this._super(searchInput, callback, excludedSearchValues, commaSeperated, false); + } +}); \ No newline at end of file diff --git a/wcfsetup/install/files/js/WCF.Tagging.min.js b/wcfsetup/install/files/js/WCF.Tagging.min.js new file mode 100644 index 0000000000..7bb0770081 --- /dev/null +++ b/wcfsetup/install/files/js/WCF.Tagging.min.js @@ -0,0 +1 @@ +WCF.Tagging={};WCF.Tagging.TagList=WCF.EditableItemList.extend({_className:"wcf\\data\\tag\\TagAction",_maxLength:0,init:function(c,a,b){this._allowCustomInput=true;this._maxLength=b;this._super(c,a);this._data=[];this._search=new WCF.Tagging.TagSearch(this._searchInput,$.proxy(this.addItem,this));this._itemList.addClass("tagList")},_keyDown:function(b){if(this._super(b)){if(b===null){return true}var a=b.which;if(a===8||a===27||a===13||a===46){return true}else{if(a>36&&a<41){return true}}if(this._searchInput.val().length>=this._maxLength){return false}return true}return false},_submit:function(){this._super();for(var b=0,a=this._data.length;b').appendTo(this._form)}}},addItem:function(b){if(!b.objectID&&b.label.length>this._maxLength){b.label=b.label.substr(0,this._maxLength)}var a=this._super(b);$(this._itemList).find(".badge:not(tag)").addClass("tag");return a},_addItem:function(b,a){this._data.push(a)},_removeItem:function(d,a){for(var c=0,b=this._data.length;c + * @package com.woltlab.wcf.tagging + * @subpackage acp.form + * @category Community Framework + */ +class TagAddForm extends AbstractForm { + /** + * @see wcf\page\AbstractPage::$activeMenuItem + */ + public $activeMenuItem = 'wcf.acp.menu.link.tag.add'; + + /** + * @see wcf\page\AbstractPage::$neededPermissions + */ + public $neededPermissions = array('admin.content.tag.canManageTag'); + + /** + * list of available languages + * @var array + */ + public $availableLanguages = array(); + + /** + * name value + * @var string + */ + public $name = ''; + + /** + * language value + * @var string + */ + public $languageID = 0; + + /** + * synonyms + * @var array + */ + public $synonyms = array(); + + /** + * @see wcf\page\IPage::readParameters() + */ + public function readParameters() { + parent::readParameters(); + + $this->availableLanguages = LanguageFactory::getInstance()->getContentLanguages(); + } + + /** + * @see wcf\form\IForm::readFormParameters() + */ + public function readFormParameters() { + parent::readFormParameters(); + + if (isset($_POST['name'])) $this->name = StringUtil::trim($_POST['name']); + if (isset($_POST['languageID'])) $this->languageID = intval($_POST['languageID']); + + // actually these are synonyms + if (isset($_POST['tags']) && is_array($_POST['tags'])) $this->synonyms = ArrayUtil::trim($_POST['tags']); + } + + /** + * @see wcf\form\IForm::validate() + */ + public function validate() { + parent::validate(); + + if (empty($this->name)) { + throw new UserInputException('name'); + } + + // validate language + if (empty($this->availableLanguages)) { + // force default language id + $this->languageID = LanguageFactory::getInstance()->getDefaultLanguageID(); + } + else { + if (!isset($this->availableLanguages[$this->languageID])) { + throw new UserInputException('languageID', 'notFound'); + } + } + + // check for duplicates + $tag = Tag::getTag($this->name, $this->languageID); + if ($tag !== null && (!isset($this->tagObj) || $tag->tagID != $this->tagObj->tagID)) { + throw new UserInputException('name', 'duplicate'); + } + + // validate synonyms + foreach ($this->synonyms as $synonym) { + if (StringUtil::toLowerCase($synonym) == StringUtil::toLowerCase($this->name)) throw new UserInputException('synonyms', 'duplicate'); + } + } + + /** + * @see wcf\page\IPage::readData() + */ + public function readData() { + parent::readData(); + + if (empty($_POST)) { + // pre-select default language id + if (!empty($this->availableLanguages)) { + $this->languageID = LanguageFactory::getInstance()->getDefaultLanguageID(); + if (!isset($this->availableLanguages[$this->languageID])) { + // language id is not within content languages, try user's language instead + $this->languageID = WCF::getUser()->languageID; + if (!isset($this->availableLanguages[$this->languageID])) { + // this installation is weird, just select nothing + $this->languageID = 0; + } + } + } + } + } + + /** + * @see wcf\form\IForm::save() + */ + public function save() { + parent::save(); + + // save tag + $this->objectAction = new TagAction(array(), 'create', array('data' => array( + 'name' => $this->name, + 'languageID' => $this->languageID + ))); + $this->objectAction->executeAction(); + $returnValues = $this->objectAction->getReturnValues(); + $editor = new TagEditor($returnValues['returnValues']); + + foreach ($this->synonyms as $synonym) { + if (empty($synonym)) continue; + + // find existing tag + $synonymObj = Tag::getTag($synonym, $this->languageID); + if ($synonymObj === null) { + $synonymAction = new TagAction(array(), 'create', array('data' => array( + 'name' => $synonym, + 'languageID' => $this->languageID, + 'synonymFor' => $editor->tagID + ))); + $synonymAction->executeAction(); + } + else { + $editor->addSynonym($synonymObj); + } + } + + $this->saved(); + + // reset values + $this->name = ''; + $this->synonyms = array(); + + // show success + WCF::getTPL()->assign(array( + 'success' => true + )); + } + + /** + * @see wcf\page\IPage::assignVariables() + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign(array( + 'action' => 'add', + 'availableLanguages' => $this->availableLanguages, + 'name' => $this->name, + 'languageID' => $this->languageID, + 'synonyms' => $this->synonyms + )); + } +} diff --git a/wcfsetup/install/files/lib/acp/form/TagEditForm.class.php b/wcfsetup/install/files/lib/acp/form/TagEditForm.class.php new file mode 100644 index 0000000000..6d6323635e --- /dev/null +++ b/wcfsetup/install/files/lib/acp/form/TagEditForm.class.php @@ -0,0 +1,140 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage acp.form + * @category Community Framework + */ +class TagEditForm extends TagAddForm { + /** + * @see wcf\page\AbstractPage::$activeMenuItem + */ + public $activeMenuItem = 'wcf.acp.menu.link.tag'; + + /** + * @see wcf\page\AbstractPage::$neededPermissions + */ + public $neededPermissions = array('admin.content.tag.canManageTag'); + + /** + * tag id + * @var integer + */ + public $tagID = 0; + + /** + * tag object + * @var wcf\data\tag\Tag + */ + public $tagObj = null; + + /** + * @see wcf\page\IPage::readParameters() + */ + public function readParameters() { + parent::readParameters(); + + if (isset($_REQUEST['id'])) $this->tagID = intval($_REQUEST['id']); + $this->tagObj = new Tag($this->tagID); + if (!$this->tagObj->tagID) { + throw new IllegalLinkException(); + } + } + + /** + * @see wcf\form\IForm::save() + */ + public function save() { + AbstractForm::save(); + + // update tag + $this->objectAction = new TagAction(array($this->tagID), 'update', array('data' => array( + 'name' => $this->name + ))); + $this->objectAction->executeAction(); + + if ($this->tagObj->synonymFor === null) { + // remove synonyms first + $sql = "UPDATE wcf".WCF_N."_tag + SET synonymFor = ? + WHERE synonymFor = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array( + null, + $this->tagID + )); + + $editor = new TagEditor($this->tagObj); + foreach ($this->synonyms as $synonym) { + if (empty($synonym)) continue; + + // find existing tag + $synonymObj = Tag::getTag($synonym, $this->languageID); + if ($synonymObj === null) { + $synonymAction = new TagAction(array(), 'create', array('data' => array( + 'name' => $synonym, + 'languageID' => $this->languageID, + 'synonymFor' => $this->tagID + ))); + $synonymAction->executeAction(); + } + else { + $editor->addSynonym($synonymObj); + } + } + } + + $this->saved(); + + // show success + WCF::getTPL()->assign(array( + 'success' => true + )); + } + + /** + * @see wcf\page\IPage::readData() + */ + public function readData() { + parent::readData(); + + if (empty($_POST)) { + $this->name = $this->tagObj->name; + } + + $this->languageID = $this->tagObj->languageID; + + $synonymList = new TagList(); + $synonymList->getConditionBuilder()->add('synonymFor = ?', array($this->tagObj->tagID)); + $synonymList->readObjects(); + $this->synonyms = array(); + foreach ($synonymList as $synonym) { + $this->synonyms[] = $synonym->name; + } + } + + /** + * @see wcf\page\IPage::assignVariables() + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign(array( + 'tagObj' => $this->tagObj, + 'action' => 'edit' + )); + } +} diff --git a/wcfsetup/install/files/lib/acp/page/TagListPage.class.php b/wcfsetup/install/files/lib/acp/page/TagListPage.class.php new file mode 100644 index 0000000000..42a7829295 --- /dev/null +++ b/wcfsetup/install/files/lib/acp/page/TagListPage.class.php @@ -0,0 +1,86 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage acp.page + * @category Community Framework + */ +class TagListPage extends SortablePage { + /** + * @see wcf\page\AbstractPage::$activeMenuItem + */ + public $activeMenuItem = 'wcf.acp.menu.link.tag.list'; + + /** + * @see wcf\page\AbstractPage::$neededPermissions + */ + public $neededPermissions = array('admin.content.tag.canManageTag'); + + /** + * @see wcf\page\SortablePage::$defaultSortField + */ + public $defaultSortField = 'name'; + + /** + * @see wcf\page\SortablePage::$validSortFields + */ + public $validSortFields = array('tagID', 'languageID', 'name', 'usageCount'); + + /** + * @see wcf\page\MultipleLinkPage::$objectListClassName + */ + public $objectListClassName = 'wcf\data\tag\TagList'; + + /** + * search-query + * @var string + */ + public $search = ''; + + /** + * @see wcf\page\IPage::assignVariables() + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign(array( + 'search' => $this->search + )); + } + + /** + * @see wcf\page\IPage::readParameters() + */ + public function readParameters() { + parent::readParameters(); + + if (isset($_REQUEST['search'])) $this->search = StringUtil::trim($_REQUEST['search']); + } + + /** + * @see wcf\page\MultipleLinkPage::initObjectList() + */ + protected function initObjectList() { + parent::initObjectList(); + + $this->objectList->sqlSelects = "(SELECT COUNT(*) FROM wcf".WCF_N."_tag_to_object t2o WHERE t2o.tagID = tag.tagID) AS usageCount"; + $this->objectList->sqlSelects .= ", language.languageName, language.languageCode"; + $this->objectList->sqlSelects .= ", synonym.name AS synonymName"; + + $this->objectList->sqlJoins = "LEFT JOIN wcf".WCF_N."_language language ON tag.languageID = language.languageID"; + $this->objectList->sqlJoins .= " LEFT JOIN wcf".WCF_N."_tag synonym ON tag.synonymFor = synonym.tagID"; + + if ($this->search !== '') { + $this->objectList->getConditionBuilder()->add('tag.name LIKE ?', array($this->search.'%')); + } + } +} diff --git a/wcfsetup/install/files/lib/data/tag/Tag.class.php b/wcfsetup/install/files/lib/data/tag/Tag.class.php new file mode 100644 index 0000000000..b3c40aff2f --- /dev/null +++ b/wcfsetup/install/files/lib/data/tag/Tag.class.php @@ -0,0 +1,92 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage data.tag + * @category Community Framework + */ +class Tag extends DatabaseObject implements IRouteController { + /** + * @see wcf\data\DatabaseObject::$databaseTableName + */ + protected static $databaseTableName = 'tag'; + + /** + * @see wcf\data\DatabaseObject::$databaseIndexName + */ + protected static $databaseTableIndexName = 'tagID'; + + /** + * Return the tag with the given name or null of no such tag exists. + * + * @param string $name + * @param integer $languageID + * @return mixed + */ + public static function getTag($name, $languageID = 0) { + $sql = "SELECT * + FROM wcf".WCF_N."_tag + WHERE languageID = ? + AND name = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array($languageID, $name)); + $row = $statement->fetchArray(); + if ($row !== false) return new Tag(null, $row); + + return null; + } + + /** + * Takes a string of comma separated tags and splits it into an array. + * + * @param string $tags + * @param string $separators + * @return array + */ + public static function splitString($tags, $separators = ',;') { + return array_unique(ArrayUtil::trim(preg_split('/['.preg_quote($separators).']/', $tags))); + } + + /** + * Takes a list of tags and builds a comma separated string from it. + * + * @param array $tags + * @param string $separator + * @return string + */ + public static function buildString(array $tags, $separator = ', ') { + $string = ''; + foreach ($tags as $tag) { + if (!empty($string)) $string .= $separator; + $string .= (is_object($tag) ? $tag->__toString() : $tag); + } + + return $string; + } + + /** + * @see wcf\data\ITitledObject::getTitle() + */ + public function getTitle() { + return $this->name; + } + + /** + * Returns the name of this tag. + * + * @return string + */ + public function __toString() { + return $this->getTitle(); + } +} diff --git a/wcfsetup/install/files/lib/data/tag/TagAction.class.php b/wcfsetup/install/files/lib/data/tag/TagAction.class.php new file mode 100644 index 0000000000..973f4af873 --- /dev/null +++ b/wcfsetup/install/files/lib/data/tag/TagAction.class.php @@ -0,0 +1,82 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage data.tag + * @category Community Framework + */ +class TagAction extends AbstractDatabaseObjectAction implements ISearchAction { + /** + * @see wcf\data\AbstractDatabaseObjectAction + */ + protected $allowGuestAccess = array('getSearchResultList'); + + /** + * @see wcf\data\AbstractDatabaseObjectAction::$className + */ + protected $className = 'wcf\data\tag\TagEditor'; + + /** + * @see \wcf\data\AbstractDatabaseObjectAction::$permissionsDelete + */ + protected $permissionsDelete = array('admin.content.tag.canManageTag'); + + /** + * @see \wcf\data\AbstractDatabaseObjectAction::$permissionsUpdate + */ + protected $permissionsUpdate = array('admin.content.tag.canManageTag'); + + /** + * @see wcf\data\ISearchAction::validateGetSearchResultList() + */ + public function validateGetSearchResultList() { + $this->readString('searchString', false, 'data'); + + if (isset($this->parameters['data']['excludedSearchValues']) && !is_array($this->parameters['data']['excludedSearchValues'])) { + throw new UserInputException('excludedSearchValues'); + } + } + + /** + * @see wcf\data\ISearchAction::getSearchResultList() + */ + public function getSearchResultList() { + $excludedSearchValues = array(); + if (isset($this->parameters['data']['excludedSearchValues'])) { + $excludedSearchValues = $this->parameters['data']['excludedSearchValues']; + } + $list = array(); + + $conditionBuilder = new PreparedStatementConditionBuilder(); + $conditionBuilder->add("name LIKE ?", array($this->parameters['data']['searchString'].'%')); + if (!empty($excludedSearchValues)) { + $conditionBuilder->add("name NOT IN (?)", array($excludedSearchValues)); + } + + // find tags + $sql = "SELECT tagID, name + FROM wcf".WCF_N."_tag + ".$conditionBuilder; + $statement = WCF::getDB()->prepareStatement($sql, 5); + $statement->execute($conditionBuilder->getParameters()); + while ($row = $statement->fetchArray()) { + $list[] = array( + 'label' => $row['name'], + 'objectID' => $row['tagID'] + ); + } + + return $list; + } +} diff --git a/wcfsetup/install/files/lib/data/tag/TagCloudTag.class.php b/wcfsetup/install/files/lib/data/tag/TagCloudTag.class.php new file mode 100644 index 0000000000..27956b019e --- /dev/null +++ b/wcfsetup/install/files/lib/data/tag/TagCloudTag.class.php @@ -0,0 +1,44 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage data.tag + * @category Community Framework + */ +class TagCloudTag extends DatabaseObjectDecorator { + /** + * @see wcf\data\DatabaseObjectDecorator::$baseClass + */ + protected static $baseClass = 'wcf\data\tag\Tag'; + + /** + * size of the tag in a weighted list + * @var double + */ + protected $size = 0.0; + + /** + * Sets the size of the tag. + * + * @param double $size + */ + public function setSize($size) { + $this->size = $size; + } + + /** + * Returns the size of the tag. + * + * @return double + */ + public function getSize() { + return $this->size; + } +} diff --git a/wcfsetup/install/files/lib/data/tag/TagEditor.class.php b/wcfsetup/install/files/lib/data/tag/TagEditor.class.php new file mode 100644 index 0000000000..1615e7be31 --- /dev/null +++ b/wcfsetup/install/files/lib/data/tag/TagEditor.class.php @@ -0,0 +1,71 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage data.tag + * @category Community Framework + */ +class TagEditor extends DatabaseObjectEditor { + /** + * @see wcf\data\DatabaseObjectEditor::$baseClass + */ + protected static $baseClass = 'wcf\data\tag\Tag'; + + /** + * Adds the given tag, and all of it's synonyms as a synonym. + * + * @param wcf\data\tag\Tag $synonym + */ + public function addSynonym(Tag $synonym) { + // clear up objects with both tags: the target and the synonym + // TODO: Optimize this! + $sql = "SELECT (objectTypeID || '-' || languageID || '-' || objectID || '-' || tagID) AS hash + FROM wcf".WCF_N."_tag_to_object + WHERE tagID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array( + $this->tagID + )); + $parameters = array($this->tagID, $synonym->tagID, $this->tagID, ' '); + $notIn = '?'; + while ($row = $statement->fetchArray()) { + $parameters[] = $row['hash']; + $notIn .= ', ?'; + } + + $sql = "UPDATE wcf".WCF_N."_tag_to_object + SET tagID = ? + WHERE tagID = ? + AND ".str_replace('tagID', '?', $concat)." NOT IN (".$notIn.")"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute($parameters); + + $sql = "DELETE FROM wcf".WCF_N."_tag_to_object + WHERE tagID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array( + $synonym->tagID, + )); + + $editor = new TagEditor($synonym); + $editor->update(array( + 'synonymFor' => $this->tagID + )); + + $synonymList = new TagList(); + $synonymList->getConditionBuilder()->add('synonymFor = ?', array($synonym->tagID)); + $synonymList->readObjects(); + + foreach ($synonymList as $synonym) { + $this->addSynonym($synonym); + } + } +} diff --git a/wcfsetup/install/files/lib/data/tag/TagList.class.php b/wcfsetup/install/files/lib/data/tag/TagList.class.php new file mode 100644 index 0000000000..ef0bec9662 --- /dev/null +++ b/wcfsetup/install/files/lib/data/tag/TagList.class.php @@ -0,0 +1,20 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage data.tag + * @category Community Framework + */ +class TagList extends DatabaseObjectList { + /** + * @see wcf\data\DatabaseObjectList::$className + */ + public $className = 'wcf\data\tag\Tag'; +} diff --git a/wcfsetup/install/files/lib/page/TaggedPage.class.php b/wcfsetup/install/files/lib/page/TaggedPage.class.php new file mode 100644 index 0000000000..f3f8004247 --- /dev/null +++ b/wcfsetup/install/files/lib/page/TaggedPage.class.php @@ -0,0 +1,103 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage page + * @category Community Framework + */ +class TaggedPage extends MultipleLinkPage { + /** + * tag id + * @var integer + */ + public $tagID = 0; + + /** + * tag object + * @var wcf\data\tag\Tag + */ + public $tag = null; + + /** + * object type object + * @var wcf\data\object\type\ObjectType + */ + public $objectType = null; + + /** + * tag cloud + * @var wcf\system\tagging\TypedTagCloud + */ + public $tagCloud = null; + + /** + * @see wcf\page\IPage::readParameters() + */ + public function readParameters() { + parent::readParameters(); + + // get tag id + if (isset($_REQUEST['id'])) $this->tagID = intval($_REQUEST['id']); + $this->tag = new Tag($this->tagID); + if (!$this->tag->tagID) { + throw new IllegalLinkException(); + } + + // get object type + if (isset($_REQUEST['objectType'])) { + $this->objectType = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.tagging.taggableObject', $_REQUEST['objectType']); + if ($this->objectType === null) { + throw new IllegalLinkException(); + } + } + else { + // use first object type + $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.tagging.taggableObject'); + $this->objectType = reset($objectTypes); + } + } + + /** + * @see wcf\page\MultipleLinkPage::readParameters() + */ + protected function initObjectList() { + $this->objectList = $this->objectType->getProcessor()->getObjectList($this->tag); + } + + /** + * @see wcf\page\IPage::readData() + */ + public function readData() { + parent::readData(); + + $this->tagCloud = new TypedTagCloud($this->objectType->objectType); + } + + /** + * @see wcf\page\IPage::assignVariables() + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign(array( + 'tag' => $this->tag, + 'tags' => $this->tagCloud->getTags(100), + 'availableObjectTypes' => ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.tagging.taggableObject'), + 'objectType' => $this->objectType->objectType, + 'resultListTemplateName' => $this->objectType->getProcessor()->getTemplateName(), + 'resultListApplication' => $this->objectType->getProcessor()->getApplication(), + 'allowSpidersToIndexThisPage' => true + )); + } +} diff --git a/wcfsetup/install/files/lib/system/cache/builder/TagCloudCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/TagCloudCacheBuilder.class.php new file mode 100644 index 0000000000..e7e81314cb --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/builder/TagCloudCacheBuilder.class.php @@ -0,0 +1,128 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage system.cache.builder + * @category Community Framework + */ +class TagCloudCacheBuilder extends AbstractCacheBuilder { + /** + * list of tags + * @var array + */ + protected $tags = array(); + + /** + * language ids + * @var integer + */ + protected $languageIDs = array(); + + /** + * @see wcf\system\cache\builder\AbstractCacheBuilder::$maxLifetime + */ + protected $maxLifetime = 3600; + + /** + * object type ids + * @var integer + */ + protected $objectTypeIDs = array(); + + /** + * @see wcf\system\cache\builder\AbstractCacheBuilder::rebuild() + */ + protected function rebuild(array $parameters) { + $this->languageIDs = $this->parseLanguageIDs($parameters); + + // get all taggable types + $objectTypes = ObjectTypeCache::getInstance()->getObjectTypes('com.woltlab.wcf.tagging.taggableObject'); + foreach ($objectTypes as $objectType) { + $this->objectTypeIDs[] = $objectType->objectTypeID; + } + + // get tags + $this->getTags(); + + return $this->tags; + } + + /** + * Parses a list of language ids. If one given language id evaluates to '0' all ids will be discarded. + * + * @param array $parameters + * @return array + */ + protected function parseLanguageIDs(array $parameters) { + // handle special '0' value + if (in_array(0, $parameters)) { + // discard all language ids + $parameters = array(); + } + + return $parameters; + } + + /** + * Reads associated tags. + */ + protected function getTags() { + if (!empty($this->objectTypeIDs)) { + // get tag ids + $tagIDs = array(); + $conditionBuilder = new PreparedStatementConditionBuilder(); + $conditionBuilder->add('object.objectTypeID IN (?)', array($this->objectTypeIDs)); + $conditionBuilder->add('object.languageID IN (?)', array($this->languageIDs)); + $sql = "SELECT COUNT(*) AS counter, object.tagID + FROM wcf".WCF_N."_tag_to_object object + ".$conditionBuilder->__toString()." + GROUP BY object.tagID + ORDER BY counter DESC"; + $statement = WCF::getDB()->prepareStatement($sql, 500); + $statement->execute($conditionBuilder->getParameters()); + while ($row = $statement->fetchArray()) { + $tagIDs[$row['tagID']] = $row['counter']; + } + + // get tags + if (!empty($tagIDs)) { + $sql = "SELECT * + FROM wcf".WCF_N."_tag + WHERE tagID IN (?".(count($tagIDs) > 1 ? str_repeat(',?', count($tagIDs) - 1) : '').")"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array_keys($tagIDs)); + while ($row = $statement->fetchArray()) { + $row['counter'] = $tagIDs[$row['tagID']]; + $this->tags[$row['name']] = new TagCloudTag(new Tag(null, $row)); + } + + // sort by counter + uasort($this->tags, array('self', 'compareTags')); + } + } + } + + /** + * Compares the weight between two tags. + * + * @param wcf\data\tag\TagCloudTag $tagA + * @param wcf\data\tag\TagCloudTag $tagB + * @return integer + */ + protected static function compareTags($tagA, $tagB) { + if ($tagA->counter > $tagB->counter) return -1; + if ($tagA->counter < $tagB->counter) return 1; + return 0; + } +} diff --git a/wcfsetup/install/files/lib/system/cache/builder/TypedTagCloudCacheBuilder.class.php b/wcfsetup/install/files/lib/system/cache/builder/TypedTagCloudCacheBuilder.class.php new file mode 100644 index 0000000000..9e78549234 --- /dev/null +++ b/wcfsetup/install/files/lib/system/cache/builder/TypedTagCloudCacheBuilder.class.php @@ -0,0 +1,27 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage system.cache.builder + * @category Community Framework + */ +class TypedTagCloudCacheBuilder extends TagCloudCacheBuilder { + /** + * @see wcf\system\cache\builder\AbstractCacheBuilder::rebuild() + */ + protected function rebuild(array $parameters) { + $this->objectTypeIDs = $parameters['objectTypeIDs']; + $this->languageIDs = $parameters['languageIDs']; + + // get tags + $this->getTags(); + + return $this->tags; + } +} diff --git a/wcfsetup/install/files/lib/system/tagging/ITaggable.class.php b/wcfsetup/install/files/lib/system/tagging/ITaggable.class.php new file mode 100644 index 0000000000..9fc0bfd503 --- /dev/null +++ b/wcfsetup/install/files/lib/system/tagging/ITaggable.class.php @@ -0,0 +1,37 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage system.tagging + * @category Community Framework + */ +interface ITaggable { + /** + * Returns a list of tagged objects. + * + * @param wcf\data\tag\Tag $tag + * @return wcf\data\DatabaseObjectList + */ + public function getObjectList(Tag $tag); + + /** + * Returns the template name for the result output. + * + * @return string + */ + public function getTemplateName(); + + /** + * Returns the application of the result template. + * + * @return string + */ + public function getApplication(); +} diff --git a/wcfsetup/install/files/lib/system/tagging/ITagged.class.php b/wcfsetup/install/files/lib/system/tagging/ITagged.class.php new file mode 100644 index 0000000000..035051bb9d --- /dev/null +++ b/wcfsetup/install/files/lib/system/tagging/ITagged.class.php @@ -0,0 +1,28 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage system.tagging + * @category Community Framework + */ +interface ITagged { + /** + * Gets the id of the tagged object. + * + * @return integer the id to get + */ + public function getObjectID(); + + /** + * Gets the taggable type of this tagged object. + * + * @return wcf\system\tagging\ITaggable + */ + public function getTaggable(); +} diff --git a/wcfsetup/install/files/lib/system/tagging/TagCloud.class.php b/wcfsetup/install/files/lib/system/tagging/TagCloud.class.php new file mode 100644 index 0000000000..48270733c9 --- /dev/null +++ b/wcfsetup/install/files/lib/system/tagging/TagCloud.class.php @@ -0,0 +1,117 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage system.tagging + * @category Community Framework + */ +class TagCloud { + /** + * max font size + * @var integer + */ + const MAX_FONT_SIZE = 170; + + /** + * min font size + * @var integer + */ + const MIN_FONT_SIZE = 85; + + /** + * list of tags + * @var array + */ + protected $tags = array(); + + /** + * max value of tag counter + * @var integer + */ + protected $maxCounter = 0; + + /** + * min value of tag counter + * @var integer + */ + protected $minCounter = 4294967295; + + /** + * active language ids + * @var array + */ + protected $languageIDs = array(); + + /** + * Contructs a new TagCloud object. + * + * @param array $languageIDs + */ + public function __construct(array $languageIDs = array()) { + $this->languageIDs = $languageIDs; + if (empty($this->languageIDs)) { + $this->languageIDs = array_keys(LanguageFactory::getInstance()->getLanguages()); + } + + // init cache + $this->loadCache(); + } + + /** + * Loads the tag cloud cache. + */ + protected function loadCache() { + $this->tags = TagCloudCacheBuilder::getInstance()->getData($this->languageIDs); + } + + /** + * Gets a list of weighted tags. + * + * @param integer $slice + * @return array the tags to get + */ + public function getTags($slice = 50) { + // slice list + $tags = array_slice($this->tags, 0, min($slice, count($this->tags))); + + // get min / max counter + foreach ($tags as $tag) { + if ($tag->counter > $this->maxCounter) $this->maxCounter = $tag->counter; + if ($tag->counter < $this->minCounter) $this->minCounter = $tag->counter; + } + + // assign sizes + foreach ($tags as $tag) { + $tag->setSize($this->calculateSize($tag->counter)); + } + + // sort alphabetically + ksort($tags); + + // return tags + return $tags; + } + + /** + * Returns the size of a tag with given number of uses for a weighted list. + * + * @param integer $counter + * @return double + */ + private function calculateSize($counter) { + if ($this->maxCounter == $this->minCounter) { + return 100; + } + else { + return (self::MAX_FONT_SIZE - self::MIN_FONT_SIZE) / ($this->maxCounter - $this->minCounter) * $counter + self::MIN_FONT_SIZE - ((self::MAX_FONT_SIZE - self::MIN_FONT_SIZE) / ($this->maxCounter - $this->minCounter)) * $this->minCounter; + } + } +} diff --git a/wcfsetup/install/files/lib/system/tagging/TagEngine.class.php b/wcfsetup/install/files/lib/system/tagging/TagEngine.class.php new file mode 100644 index 0000000000..ab4e63ebb6 --- /dev/null +++ b/wcfsetup/install/files/lib/system/tagging/TagEngine.class.php @@ -0,0 +1,189 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage system.tagging + * @category Community Framework + */ +class TagEngine extends SingletonFactory { + /** + * Adds tags to a tagged object. + * + * @param string $objectType + * @param integer $objectID + * @param array $tags + * @param integer $languageID + * @param boolean $replace + */ + public function addObjectTags($objectType, $objectID, array $tags, $languageID, $replace = true) { + $objectTypeID = $this->getObjectTypeID($objectType); + $tags = array_unique($tags); + + // remove tags prior to apply the new ones (prevents duplicate entries) + if ($replace) { + $sql = "DELETE FROM wcf".WCF_N."_tag_to_object + WHERE objectTypeID = ? + AND objectID = ? + AND languageID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(array( + $objectTypeID, + $objectID, + $languageID + )); + } + + // get tag ids + $tagIDs = array(); + foreach ($tags as $tag) { + if (empty($tag)) continue; + + // find existing tag + $tagObj = Tag::getTag($tag, $languageID); + if ($tagObj === null) { + // enforce max length + if (StringUtil::length($tag) > TAGGING_MAX_TAG_LENGTH) { + $tag = StringUtil::substring($tag, 0, TAGGING_MAX_TAG_LENGTH); + } + + // create new tag + $tagAction = new TagAction(array(), 'create', array('data' => array( + 'name' => $tag, + 'languageID' => $languageID + ))); + + $tagAction->executeAction(); + $returnValues = $tagAction->getReturnValues(); + $tagObj = $returnValues['returnValues']; + } + + if ($tagObj->synonymFor !== null) $tagIDs[$tagObj->synonymFor] = $tagObj->synonymFor; + else $tagIDs[$tagObj->tagID] = $tagObj->tagID; + } + + // save tags + $sql = "INSERT INTO wcf".WCF_N."_tag_to_object + (objectID, tagID, objectTypeID, languageID) + VALUES (?, ?, ?, ?)"; + WCF::getDB()->beginTransaction(); + $statement = WCF::getDB()->prepareStatement($sql); + foreach ($tagIDs as $tagID) { + $statement->execute(array($objectID, $tagID, $objectTypeID, $languageID)); + } + WCF::getDB()->commitTransaction(); + } + + /** + * Deletes all tags assigned to given tagged object. + * + * @param string $objectType + * @param integer $objectID + * @param integer $languageID + */ + public function deleteObjectTags($objectType, $objectID, $languageID = null) { + $objectTypeID = $this->getObjectTypeID($objectType); + + $sql = "DELETE FROM wcf".WCF_N."_tag_to_object + WHERE objectTypeID = ? + AND objectID = ? + ".($languageID !== null ? "AND languageID = ?" : ""); + $statement = WCF::getDB()->prepareStatement($sql); + $parameters = array( + $objectTypeID, + $objectID + ); + if ($languageID !== null) $parameters[] = $languageID; + $statement->execute($parameters); + } + + /** + * Deletes all tags assigned to given tagged objects. + * + * @param string $objectType + * @param array $objectIDs + */ + public function deleteObjects($objectType, array $objectIDs) { + $objectTypeID = $this->getObjectTypeID($objectType); + + $conditionsBuilder = new PreparedStatementConditionBuilder(); + $conditionsBuilder->add('objectTypeID = ?', array($objectTypeID)); + $conditionsBuilder->add('objectID IN (?)', array($objectIDs)); + + $sql = "DELETE FROM wcf".WCF_N."_tag_to_object + ".$conditionsBuilder; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute($conditionsBuilder->getParameters()); + } + + /** + * Returns all tags set for given object. + * + * @param string $objectType + * @param integer $objectID + * @param array $languageIDs + * @return array + */ + public function getObjectTags($objectType, $objectID, array $languageIDs = array()) { + $objectTypeID = $this->getObjectTypeID($objectType); + + // get tags + $conditions = new PreparedStatementConditionBuilder(); + $conditions->add("tag_to_object.objectTypeID = ?", array($objectTypeID)); + $conditions->add("tag_to_object.objectID = ?", array($objectID)); + if (!empty($languageIDs)) { + foreach ($languageIDs as $index => $languageID) { + if (!$languageID) unset($languageIDs[$index]); + } + + if (!empty($languageIDs)) { + $conditions->add("tag_to_object.languageID IN (?)", array($languageIDs)); + } + } + + $sql = "SELECT tag.* + FROM wcf".WCF_N."_tag_to_object tag_to_object + LEFT JOIN wcf".WCF_N."_tag tag + ON (tag.tagID = tag_to_object.tagID) + ".$conditions; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute($conditions->getParameters()); + + $tags = array(); + + while ($row = $statement->fetchArray()) { + $tags[$row['tagID']] = new Tag(null, $row); + } + + return $tags; + } + + /** + * Returns id of the object type with the given name. + * + * @param string $objectType + * @return integer + */ + public function getObjectTypeID($objectType) { + // get object type + $objectTypeObj = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.tagging.taggableObject', $objectType); + if ($objectTypeObj === null) { + throw new SystemException("Object type '".$objectType."' is not valid for definition 'com.woltlab.wcf.tagging.taggableObject'"); + } + + return $objectTypeObj->objectTypeID; + } +} diff --git a/wcfsetup/install/files/lib/system/tagging/TypedTagCloud.class.php b/wcfsetup/install/files/lib/system/tagging/TypedTagCloud.class.php new file mode 100644 index 0000000000..0e432e88f9 --- /dev/null +++ b/wcfsetup/install/files/lib/system/tagging/TypedTagCloud.class.php @@ -0,0 +1,45 @@ + + * @package com.woltlab.wcf.tagging + * @subpackage system.tagging + * @category Community Framework + */ +class TypedTagCloud extends TagCloud { + /** + * object type ids + * @var array + */ + protected $objectTypeIDs = array(); + + /** + * Contructs a new TypedTagCloud object. + * + * @param string $objectType + * @param array $languageIDs + */ + public function __construct($objectType, array $languageIDs = array()) { + $objectTypeObj = ObjectTypeCache::getInstance()->getObjectTypeByName('com.woltlab.wcf.tagging.taggableObject', $objectType); + $this->objectTypeIDs[] = $objectTypeObj->objectTypeID; + + parent::__construct($languageIDs); + } + + /** + * Loads the tag cloud cache. + */ + protected function loadCache() { + $this->tags = TypedTagCloudCacheBuilder::getInstance()->getData(array( + 'languageIDs' => $this->languageIDs, + 'objectTypeIDs' => $this->objectTypeIDs + )); + } +} diff --git a/wcfsetup/install/files/style/tagging.less b/wcfsetup/install/files/style/tagging.less new file mode 100644 index 0000000000..b6c86653d3 --- /dev/null +++ b/wcfsetup/install/files/style/tagging.less @@ -0,0 +1,59 @@ +.tagList { + > li { + display: inline-block; + } +} + +.tag { + font-weight: normal; + height: 13px; + margin-left: 6px; + padding-bottom: 2px; + padding-left: 10px; + + .borderRadius(0, 4px, 4px, 0); + + &:before{ + border-color: transparent @wcfColor transparent transparent; + border-style: inset solid inset inset; + border-width: 8px 8px 8px 0; + clip: rect(auto auto auto 2px); + content: ""; + height: 0; + left: -8px; + position: absolute; + top: 0; + width: 0; + } + + &:after{ + background: @wcfContentBackgroundColor; + content: ""; + height: 4px; + left: -2px; + position: absolute; + top: 6px; + width: 4px; + + .borderRadius(2px); + .boxShadow(0, -1px, rgba(0, 0, 0, .2), 2px); + } + + &:hover { + background-color: @wcfTabularBoxBackgroundColor; + color: @wcfTabularBoxColor; + + &:before{ + border-right-color: @wcfTabularBoxBackgroundColor; + } + } +} + +.editableItemList li.tag { + margin-bottom: 11px; + margin-left: 10px; +} + +.editableItemList li.tag:first-child { + margin-left: 6px; +} \ No newline at end of file diff --git a/wcfsetup/install/lang/de.xml b/wcfsetup/install/lang/de.xml index 5826fad394..ecdd50dd59 100644 --- a/wcfsetup/install/lang/de.xml +++ b/wcfsetup/install/lang/de.xml @@ -311,6 +311,8 @@ + +
@@ -485,6 +487,9 @@ + + + @@ -758,6 +763,9 @@ + + + @@ -1014,6 +1022,22 @@ + + + + + + + + + + + + + + + + @@ -1694,6 +1718,14 @@ Erlaubte Dateiendungen: {', '|implode:$attachmentHandler->getAllowedExtensions() + + + + + + + + diff --git a/wcfsetup/install/lang/en.xml b/wcfsetup/install/lang/en.xml index 193ba8c37c..8e58246be1 100644 --- a/wcfsetup/install/lang/en.xml +++ b/wcfsetup/install/lang/en.xml @@ -310,6 +310,8 @@ Examples for medium ID detection: + + @@ -484,6 +486,9 @@ Examples for medium ID detection: + + + @@ -757,6 +762,9 @@ Examples for medium ID detection: + + + @@ -1013,6 +1021,22 @@ Examples for medium ID detection: + + + + + + + + + + + + + + + + @@ -1692,6 +1716,14 @@ Allowed extensions: {', '|implode:$attachmentHandler->getAllowedExtensions()}]]> + + + + + + + + diff --git a/wcfsetup/setup/db/install.sql b/wcfsetup/setup/db/install.sql index 038e6d2e95..ed7b63cb48 100644 --- a/wcfsetup/setup/db/install.sql +++ b/wcfsetup/setup/db/install.sql @@ -798,6 +798,26 @@ CREATE TABLE wcf1_style_variable_value ( UNIQUE KEY (styleID, variableID) ); +DROP TABLE IF EXISTS wcf1_tag; +CREATE TABLE wcf1_tag ( + tagID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY, + languageID INT(10) NOT NULL DEFAULT 0, + name VARCHAR(255) NOT NULL, + synonymFor INT(10), + UNIQUE KEY (languageID, name) +); + +DROP TABLE IF EXISTS wcf1_tag_to_object; +CREATE TABLE wcf1_tag_to_object ( + objectID INT(10) NOT NULL, + tagID INT(10) NOT NULL, + objectTypeID INT(10) NOT NULL, + languageID INT(10) NOT NULL, + UNIQUE KEY (objectTypeID, languageID, objectID, tagID), + KEY (objectTypeID, languageID, tagID), + KEY (tagID, objectTypeID) +); + DROP TABLE IF EXISTS wcf1_template; CREATE TABLE wcf1_template ( templateID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY, @@ -1424,6 +1444,12 @@ ALTER TABLE wcf1_label_group_to_object ADD FOREIGN KEY (objectTypeID) REFERENCES ALTER TABLE wcf1_label_object ADD FOREIGN KEY (labelID) REFERENCES wcf1_label (labelID) ON DELETE CASCADE; ALTER TABLE wcf1_label_object ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE; +ALTER TABLE wcf1_tag ADD FOREIGN KEY (synonymFor) REFERENCES wcf1_tag (tagID) ON DELETE CASCADE; + +ALTER TABLE wcf1_tag_to_object ADD FOREIGN KEY (tagID) REFERENCES wcf1_tag (tagID) ON DELETE CASCADE; +ALTER TABLE wcf1_tag_to_object ADD FOREIGN KEY (languageID) REFERENCES wcf1_language (languageID) ON DELETE CASCADE; +ALTER TABLE wcf1_tag_to_object ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE; + /* default inserts */ -- default user groups