From 517d58fc1ec2409ce45e6d9f45069febd2fc611e Mon Sep 17 00:00:00 2001 From: "Michael J. Rubinsky" Date: Sat, 31 Jul 2010 10:30:41 -0400 Subject: [PATCH] Move Ansel's tag functionality to content tagger. Need to run the (non-destructive) 2010-07-30_migrate_tags_to_content.php script to move existing ansel tags to content. --- ansel/browse.php | 2 +- ansel/lib/Ajax/Imple/EditGalleryFaces.php | 125 ++++ ansel/lib/Ajax/Imple/TagActions.php | 25 +- ansel/lib/Ansel.php | 31 + ansel/lib/Application.php | 8 + ansel/lib/Block/cloud.php | 4 +- ansel/lib/Gallery.php | 19 +- ansel/lib/Image.php | 6 +- ansel/lib/Search/Tag.php | 300 +++++++++ ansel/lib/Storage.php | 3 - ansel/lib/Tagger.php | 349 +++++++++++ ansel/lib/Tags.php | 675 --------------------- ansel/lib/View/GalleryRenderer/Base.php | 6 +- ansel/lib/View/Results.php | 36 +- ansel/lib/Widget/ImageFaces.php | 1 - ansel/lib/Widget/SimilarPhotos.php | 9 +- ansel/lib/Widget/Tags.php | 14 +- ansel/rss.php | 8 +- .../2010-07-30_migrate_tags_to_content.php | 39 ++ ansel/templates/view/results.inc | 2 +- content/lib/Exception.php | 3 + content/lib/Tagger.php | 77 ++- 22 files changed, 981 insertions(+), 761 deletions(-) create mode 100644 ansel/lib/Ajax/Imple/EditGalleryFaces.php create mode 100644 ansel/lib/Search/Tag.php create mode 100644 ansel/lib/Tagger.php delete mode 100644 ansel/lib/Tags.php create mode 100644 ansel/scripts/upgrades/2010-07-30_migrate_tags_to_content.php create mode 100644 content/lib/Exception.php diff --git a/ansel/browse.php b/ansel/browse.php index 10bdcc52e..4be576bb2 100644 --- a/ansel/browse.php +++ b/ansel/browse.php @@ -17,7 +17,7 @@ $layout = new Horde_Block_Layout_View( $layout_html = $layout->toHtml(); $title = _("Photo Galleries"); -Ansel_Tags::clearSearch(); +Ansel_Search_Tag::clearSearch(); require ANSEL_BASE . '/templates/common-header.inc'; require ANSEL_TEMPLATES . '/menu.inc'; echo '
 
'; diff --git a/ansel/lib/Ajax/Imple/EditGalleryFaces.php b/ansel/lib/Ajax/Imple/EditGalleryFaces.php new file mode 100644 index 000000000..3662ecd0e --- /dev/null +++ b/ansel/lib/Ajax/Imple/EditGalleryFaces.php @@ -0,0 +1,125 @@ + + * @author Michael J. Rubinsky + * + * @package Ansel + */ +class Ansel_Ajax_Imple_EditGalleryFaces extends Horde_Ajax_Imple_Base +{ + /** + * Attach these actions to the view + * + */ + public function attach() + { + Horde::addScriptFile('editfaces.js'); + $url = $this->_getUrl('EditFaces', 'ansel'); + $js = array(); + $js[] = "Ansel.ajax['editFaces'] = {'url':'" . $url . "', text: {loading:'" . _("Loading...") . "'}};"; + $image_id = $this->_params['image_id']; + /* Start by getting the faces */ + $faces = $GLOBALS['injector']->getInstance('Ansel_Faces'); + $name = ''; + $autocreate = true; + $result = $faces->getImageFacesData($image_id); + if (empty($result)) { + $image = $GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getImage($this->_params['image_id']); + $image->createView('screen'); + $result = $faces->getFromPicture($this->_params['image_id'], $autocreate); + } + if (!empty($result)) { + $customurl = Horde::applicationUrl('faces/custom.php'); + $url = (!empty($args['url']) ? urldecode($args['url']) : ''); + Horde::startBuffer(); + require_once ANSEL_TEMPLATES . '/faces/image.inc'; + return Horde::endBuffer(); + } else { + return _("No faces found"); + } + + Horde::addInlineScript($js, 'dom'); + } + + function handle($args, $post) + { + if (Horde_Auth::getAuth()) { + $action = $args['action']; + $image_id = (int)$post['image']; + $reload = empty($post['reload']) ? 0 : $post['reload']; + + if (empty($action)) { + return array('response' => 0); + } + + $faces = $GLOBALS['injector']->getInstance('Ansel_Faces'); + switch($action) { + case 'process': + // process - detects all faces in the image. + $name = ''; + $autocreate = true; + $result = $faces->getImageFacesData($image_id); + // Attempt to get faces from the picture if we don't already have results, + // or if we were asked to explicitly try again. + if (($reload || empty($result))) { + $image = $GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getImage($image_id); + $image->createView('screen'); + $result = $faces->getFromPicture($image_id, $autocreate); + } + if (!empty($result)) { + $imgdir = Horde_Themes::img(null, 'horde'); + $customurl = Horde::applicationUrl('faces/custom.php'); + $url = (!empty($args['url']) ? urldecode($args['url']) : ''); + Horde::startBuffer(); + require_once ANSEL_TEMPLATES . '/faces/image.inc'; + $html = Horde::endBuffer(); + return array('response' => 1, + 'message' => $html); + } else { + return array('response' => 1, + 'message' => _("No faces found")); + } + break; + + case 'delete': + // delete - deletes a single face from an image. + $face_id = (int)$post['face']; + $image = &$GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getImage($image_id); + $gallery = &$GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getGallery($image->gallery); + if (!$gallery->hasPermission(Horde_Auth::getAuth(), Horde_Perms::EDIT)) { + throw new Horde_Exception('Access denied editing the photo.'); + } + + $faces = $GLOBALS['injector']->getInstance('Ansel_Faces'); + $faces->delete($image, $face_id); + break; + + case 'setname': + // setname - sets the name of a single image. + $face_id = (int)$post['face']; + if (!$face_id) { + return array('response' => 0); + } + + $name = $post['facename']; + $image = &$GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getImage($image_id); + $gallery = &$GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getGallery($image->gallery); + if (!$gallery->hasPermission(Horde_Auth::getAuth(), Horde_Perms::EDIT)) { + throw new Horde_Exception('You are not allowed to edit this photo'); + } + + $faces = $GLOBALS['injector']->getInstance('Ansel_Faces'); + $result = $faces->setName($face_id, $name); + return array('response' => 1, + 'message' => Ansel_Faces::getFaceTile($face_id)); + break; + } + } + } + +} diff --git a/ansel/lib/Ajax/Imple/TagActions.php b/ansel/lib/Ajax/Imple/TagActions.php index 69b08fa33..1d4cbaca2 100644 --- a/ansel/lib/Ajax/Imple/TagActions.php +++ b/ansel/lib/Ajax/Imple/TagActions.php @@ -66,16 +66,12 @@ class Ansel_Ajax_Imple_TagActions extends Horde_Core_Ajax_Imple if (!empty($tags)) { $tags = rawurldecode($post['tags']); $tags = explode(',', $tags); - - /* Get current tags so we don't overwrite them */ - $etags = Ansel_Tags::readTags($id, $type); - $tags = array_keys(array_flip(array_merge($tags, array_values($etags)))); - $resource->setTags($tags); + $GLOBALS['injector']->getInstance('Ansel_Tagger')->tag($id, $tags, $GLOBALS['registry']->getAuth(), $type); /* Get the tags again since we need the newly added tag_ids */ - $newTags = $resource->getTags(); + $newTags = $GLOBALS['injector']->getInstance('Ansel_Tagger')->getTags($id, $type); if (count($newTags)) { - $newTags = Ansel_Tags::listTagInfo(array_keys($newTags)); + $newTags = $GLOBALS['injector']->getInstance('Ansel_Tagger')->getTagInfo(array_keys($newTags)); } return array('response' => 1, @@ -85,15 +81,13 @@ class Ansel_Ajax_Imple_TagActions extends Horde_Core_Ajax_Imple break; case 'remove': - $existingTags = $resource->getTags(); - unset($existingTags[$tags]); - $resource->setTags($existingTags); + $GLOBALS['injector']->getInstance('Ansel_Tagger')->untag($resource->id, (int)$tags, $type); + $existingTags = $GLOBALS['injector']->getInstance('Ansel_Tagger')->getTags($resource->id, $type); if (count($existingTags)) { - $newTags = Ansel_Tags::listTagInfo(array_keys($existingTags)); + $newTags = $GLOBALS['injector']->getInstance('Ansel_Tagger')->getTagInfo(array_keys($existingTags)); } else { $newTags = array(); } - return array('response' => 1, 'message' => $this->_getTagHtml($newTags, $parent->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::EDIT))); @@ -105,10 +99,11 @@ class Ansel_Ajax_Imple_TagActions extends Horde_Core_Ajax_Imple private function _getTagHtml($tags, $hasEdit) { global $registry; - $links = Ansel_Tags::getTagLinks($tags, 'add'); + $links = Ansel::getTagLinks($tags, 'add'); $html = ''; return $html; diff --git a/ansel/lib/Ansel.php b/ansel/lib/Ansel.php index 07780e01c..5b222c3a3 100644 --- a/ansel/lib/Ansel.php +++ b/ansel/lib/Ansel.php @@ -1011,4 +1011,35 @@ class Ansel return '
'; } + /** + * Get the URL for a tag search link + * + * @TODO: Move this to Tagger + * + * @param array $tags The tag ids to link to + * @param string $action The action we want to perform with this tag. + * @param string $owner The owner we want to filter the results by + * + * @return string The URL for this tag and action + */ + static public function getTagLinks($tags, $action = 'add', $owner = null) + { + + $results = array(); + foreach ($tags as $id => $taginfo) { + $params = array('view' => 'Results', + 'tag' => $taginfo['tag_name']); + if (!empty($owner)) { + $params['owner'] = $owner; + } + if ($action != 'add') { + $params['actionID'] = $action; + } + $link = Ansel::getUrlFor('view', $params, true); + $results[$id] = $link; + } + + return $results; + } + } diff --git a/ansel/lib/Application.php b/ansel/lib/Application.php index 38f54076a..8b9f3936e 100644 --- a/ansel/lib/Application.php +++ b/ansel/lib/Application.php @@ -57,6 +57,14 @@ class Ansel_Application extends Horde_Registry_Application throw new Horde_Exception('You must configure a Horde_Image driver to use Ansel'); } + /* For now, autoloading the Content_* classes depend on there being a + * registry entry for the 'content' application that contains at least + * the fileroot entry. */ + $GLOBALS['injector']->getInstance('Horde_Autoloader')->addClassPathMapper(new Horde_Autoloader_ClassPathMapper_Prefix('/^Content_/', $GLOBALS['registry']->get('fileroot', 'content') . '/lib/')); + if (!class_exists('Content_Tagger')) { + throw new Horde_Exception('The Content_Tagger class could not be found. Make sure the registry entry for the Content system is present.'); + } + $binders = array( 'Ansel_Styles' => new Ansel_Injector_Binder_Styles(), 'Ansel_Faces' => new Ansel_Injector_Binder_Faces(), diff --git a/ansel/lib/Block/cloud.php b/ansel/lib/Block/cloud.php index d2a2be409..f5c669e6e 100644 --- a/ansel/lib/Block/cloud.php +++ b/ansel/lib/Block/cloud.php @@ -53,13 +53,13 @@ class Horde_Block_ansel_cloud extends Horde_Block global $registry; /* Get the tags */ - $tags = Ansel_Tags::listTagInfo(null, $this->_params['count']); + $tags = $GLOBALS['injector']->getInstance('Ansel_Tagger')->getCloud(null, $this->_params['count']); if (count($tags)) { $cloud = new Horde_Core_Ui_TagCloud(); foreach ($tags as $id => $tag) { $link = Ansel::getUrlFor('view', array('view' => 'Results', 'tag' => $tag['tag_name'])); - $cloud->addElement($tag['tag_name'], $link, $tag['total']); + $cloud->addElement($tag['tag_name'], $link, $tag['count']); } $html = $cloud->buildHTML(); } else { diff --git a/ansel/lib/Gallery.php b/ansel/lib/Gallery.php index 0252bba66..9c2b9c812 100644 --- a/ansel/lib/Gallery.php +++ b/ansel/lib/Gallery.php @@ -372,21 +372,8 @@ class Ansel_Gallery extends Horde_Share_Object_Sql_Hierarchical 'image_type' => $img->getType(), 'image_uploaded_date' => $img->uploaded)); /* Copy any tags */ - // Since we know that the tags already exist, no need to - // go through Ansel_Tags::writeTags() - this saves us a SELECT query - // for each tag - just write the data into the DB ourselves. $tags = $img->getTags(); - $query = $GLOBALS['ansel_db']->prepare('INSERT INTO ansel_images_tags (image_id, tag_id) VALUES(' . $newId . ',?);'); - if ($query instanceof PEAR_Error) { - throw new Ansel_Exception($query); - } - foreach ($tags as $tag_id => $tag_name) { - $result = $query->execute($tag_id); - if ($result instanceof PEAR_Error) { - throw new Ansel_Exception($result); - } - } - $query->free(); + $GLOBALS['injector']->getInstance('Ansel_Tagger')->tag($newId, $tags, $gallery->get('owner'), 'image'); /* exif data */ // First check to see if the exif data was present in the raw data. @@ -688,7 +675,7 @@ class Ansel_Gallery extends Horde_Share_Object_Sql_Hierarchical */ public function getTags() { if ($this->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::READ)) { - return Ansel_Tags::readTags($this->id, 'gallery'); + return $GLOBALS['injector']->getInstance('Ansel_Tagger')->getTags($this->id, 'gallery'); } else { throw new Horde_Exception(_("Access denied viewing this gallery.")); } @@ -705,7 +692,7 @@ class Ansel_Gallery extends Horde_Share_Object_Sql_Hierarchical public function setTags($tags) { if ($this->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::EDIT)) { - return Ansel_Tags::writeTags($this->id, $tags, 'gallery'); + return $GLOBALS['injector']->getInstance('Ansel_Tagger')->tag($this->id, $tags, $this->get('owner'), 'gallery'); } else { throw new Horde_Exception(_("Access denied adding tags to this gallery.")); } diff --git a/ansel/lib/Image.php b/ansel/lib/Image.php index a5a0c7d82..1407110f6 100644 --- a/ansel/lib/Image.php +++ b/ansel/lib/Image.php @@ -511,8 +511,6 @@ class Ansel_Image Implements Iterator /** * Save image details to storage. * - * @TODO: Move all SQL queries to Ansel_Storage:: - * * @return integer image id * @throws Ansel_Exception */ @@ -1121,7 +1119,7 @@ class Ansel_Image Implements Iterator } $gallery = $GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getGallery($this->gallery); if ($gallery->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::READ)) { - return Ansel_Tags::readTags($this->id); + return $GLOBALS['injector']->getInstance('Ansel_Tagger')->getTags($this->id, 'image'); } else { throw new Horde_Exception_PermissionDenied(_("Access denied viewing this photo.")); } @@ -1141,7 +1139,7 @@ class Ansel_Image Implements Iterator if ($gallery->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::EDIT)) { // Clear the local cache. $this->_tags = array(); - Ansel_Tags::writeTags($this->id, $tags); + $GLOBALS['injector']->getInstance('Ansel_Tagger')->tag($this->id, $tags, $gallery->get('owner'), 'image'); } else { throw new Horde_Exception_PermissionDenied(_("Access denied adding tags to this photo.")); } diff --git a/ansel/lib/Search/Tag.php b/ansel/lib/Search/Tag.php new file mode 100644 index 000000000..6a2e9f0dd --- /dev/null +++ b/ansel/lib/Search/Tag.php @@ -0,0 +1,300 @@ + + * @category Horde + * @license http://www.fsf.org/copyleft/gpl.html GPL + * @package Ansel + */ +class Ansel_Search_Tag +{ + /** + * Array of tag_name => tag_id hashes for the current search. + * Tags are always added to the search by name and stored by name=>id. + * + * @var array + */ + protected $_tags = array(); + + /** + * Total count of all resources that match (both Galleries and Images). + * + * @var integer + */ + protected $_totalCount = null; + + /** + * The user whose resources we are searching. + * + * @var string + */ + protected $_owner = ''; + + /** + * Dirty flag + * + * @var boolean + */ + protected $_dirty = false; + + /** + * Results cache. Holds the results of the current search. + * + * @var array + */ + protected $_results = array(); + + /** + * The Ansel_Tagger object. + * + * @var Ansel_Tagger + */ + protected $_tagger; + + /** + * Constructor + * + * @param array $tags An array of tag names to match. If null is passed + * then the tags will be loaded from the session. + * @param string $owner Restrict search to resources owned by specified + * owner. + * + * @return Ansel_Search_Tag + */ + public function __construct(Ansel_Tagger $tagger, $tags = null, $owner = null) + { + $this->_tagger = $tagger; + if (!empty($tags)) { + $this->_tags = $this->_tagger->getTagIds($tags); + } else { + $this->_tags = (!empty($_SESSION['ansel_tags_search']) ? $_SESSION['ansel_tags_search'] : array()); + } + + $this->_owner = $owner; + + } + + /** + * Save the current search to the session + * + */ + public function save() + { + $_SESSION['ansel_tags_search'] = $this->_tags; + $this->_dirty = false; + } + + /** + * Fetch the matching resources that should appear on the current page + * + * @TODO: Implement an Interface that Ansel_Gallery and Ansel_Image should + * implement that the client search code will use. + * + * @return Array of Ansel_Images and Ansel_Galleries + */ + public function getSlice($page, $perpage) + { + global $conf, $registry; + + /* Refresh the search */ + $this->runSearch(); + $totals = $this->count(); + + /* First, the galleries */ + $gstart = $page * $perpage; + $gresults = array_slice($this->_results['galleries'], $gstart, $perpage); + + /* Instantiate the Gallery objects */ + $galleries = array(); + foreach ($gresults as $gallery) { + $galleries[] = $GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getGallery($gallery); + } + + /* Do we need to get images? */ + $istart = max(0, $page * $perpage - $totals['galleries']); + $count = $perpage - count($galleries); + if ($count > 0) { + $iresults = array_slice($this->_results['images'], $istart, $count); + $images = count($iresults) ? array_values($GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getImages(array('ids' => $iresults))) : array(); + if (($conf['comments']['allow'] == 'all' || ($conf['comments']['allow'] == 'authenticated' && $GLOBALS['registry']->getAuth())) && + $registry->hasMethod('forums/numMessagesBatch')) { + + $ids = array_keys($images); + $ccounts = $GLOBALS['registry']->call('forums/numMessagesBatch', array($ids, 'ansel')); + if (!($ccounts instanceof PEAR_Error)) { + foreach ($images as $image) { + $image->commentCount = (!empty($ccounts[$image->id]) ? $ccounts[$image->id] : 0); + } + } + } + } else { + $images = array(); + } + + return array_merge($galleries, $images); + } + + /** + * Add a tag to the cumulative tag search + * + * @param string $tag The tag name to add. + * + * @return void + */ + public function addTag($tag) + { + $tag_id = (int)current($this->_tagger->getTagIds($tag)); + if (array_search($tag_id, $this->_tags) === false) { + $this->_tags[$tag] = $tag_id; + $this->_dirty = true; + } + } + + /** + * Remove a tag from the cumulative search + * + * @param string $tag The tag name to remove. + * + * @return void + */ + public function removeTag($tag) + { + if (!empty($this->_tags[$tag])) { + unset($this->_tags[$tag]); + $this->_dirty = true; + } + } + + /** + * Get the list of currently choosen tags + * + * @return array An array of selected tag_name => tag_id hashes. + */ + public function getTags() + { + return $this->_tags; + } + + /** + * Get breadcrumb style navigation html for choosen tags + * + * @TODO: Remove the html generation to the view class + * + * @return string The html representing the tag trail for browsing tags. + */ + public function getTagTrail() + { + global $registry; + + $html = ''; + } + + /** + * Get the total number of tags included in this search. + * + * @return integer The number of tags used in the current search. + */ + public function tagCount() + { + return count($this->_tags); + } + + /** + * Get the total number of resources that match. + * + * @return array Hash containing totals for both 'galleries' and 'images'. + */ + public function count() + { + if (!is_array($this->_tags) || !count($this->_tags)) { + return 0; + } + + $count = array('galleries' => count($this->_results['galleries']), 'images' => count($this->_results['images'])); + $this->_totalCount = $count; + + return $count; + } + + /** + * Get a list of tags related to this search + * + * @return array An array tag_id => {tag_name, total} + */ + public function getRelatedTags() + { + $tags = $this->_tagger->browseTags($this->getTags(), $this->_owner); + $search = new Ansel_Search_Tag($this->_tagger, null, $this->_owner); + $results = array(); + foreach ($tags as $id => $tag) { + $search->addTag($tag); + $search->runSearch(); + $count = $search->count(); + if ($count['images'] + $count['galleries'] > 0) { + $results[$id] = array('tag_name' => $tag, 'total' => $count['images'] + $count['galleries']); + } + $search->removeTag($tag); + } + + /* Get the results sorted by available totals for this user */ + uasort($results, array($this, '_sortTagInfo')); + return $results; + } + + /** + * Perform, and cache the search. + * + */ + public function runSearch() + { + if (!empty($this->_owner)) { + $filter = array('user' => $this->_owner); + } else { + $filter = array(); + } + if (empty($this->_results) || $this->_dirty) { + $this->_results = $this->_tagger + ->search($this->_tags, $filter); + } + } + + /** + * Clears the session cache of tags currently included in the search. + */ + static public function clearSearch() + { + unset($_SESSION['ansel_tags_search']); + } + + /** + * Helper for uasort. Sorts the results by count. + * + */ + private function _sortTagInfo($a, $b) + { + return $a['total'] < $b['total']; + } + +} \ No newline at end of file diff --git a/ansel/lib/Storage.php b/ansel/lib/Storage.php index f59da143f..670cc1e8b 100644 --- a/ansel/lib/Storage.php +++ b/ansel/lib/Storage.php @@ -146,9 +146,6 @@ class Ansel_Storage } /* Fill up the new gallery */ - // TODO: New private method to bulk load these (it's done this way - // since the data is stored in the Share_Object class keyed by the - // DB specific fields and set() translates them. foreach ($attributes as $key => $value) { $gallery->set($key, $value); } diff --git a/ansel/lib/Tagger.php b/ansel/lib/Tagger.php new file mode 100644 index 000000000..e679e84dd --- /dev/null +++ b/ansel/lib/Tagger.php @@ -0,0 +1,349 @@ + + * @license http://www.fsf.org/copyleft/gpl.html GPL + * @package Ansel + */ +class Ansel_Tagger +{ + /** + * Local cache of the type name => ids from Content, so we don't have to + * query for them each time. + * + * @var array + */ + protected $_type_ids = array(); + + /** + * Local reference to the tagger. + * + * @var Content_Tagger + */ + protected $_tagger; + + /** + * Constructor. + * + * @return Ansel_Tagger + */ + public function __construct(Content_Tagger $tagger) + { + /* Remember the types to avoid having Content query them again. */ + $key = 'ansel.tagger.type_ids'; + $ids = $GLOBALS['injector']->getInstance('Horde_Cache')->get($key, 360); + if ($ids) { + $this->_type_ids = unserialize($ids); + } else { + $type_mgr = $GLOBALS['injector']->getInstance('Content_Types_Manager'); + $types = $type_mgr->ensureTypes(array('gallery', 'image')); + $this->_type_ids = array('gallery' => (int)$types[0], + 'image' => (int)$types[1]); + $GLOBALS['injector']->getInstance('Horde_Cache')->set($key, serialize($this->_type_ids)); + } + + $this->_tagger = $tagger; + } + + /** + * Tags an ansel object with any number of tags. + * + * @param string $localId The identifier of the ansel object. + * @param string|array $tags Either a single tag string or an array of + * tags. + * @param string $owner The tag owner (should normally be the owner + * of the resource). + * @param string $content_type The type of object we are tagging + * (gallery/image). + * + * @return void + * @throws Ansel_Exception + */ + public function tag($localId, $tags, $owner, $content_type = 'image') + { + if (!is_array($tags)) { + $tags = $this->_tagger->splitTags($tags); + } + + try { + $this->_tagger->tag( + $owner, + array('object' => $localId, + 'type' => $this->_type_ids[$content_type]), + $tags); + } catch (Content_Exception $e) { + throw new Ansel_Exception($e); + } + } + + /** + * Retrieves the tags on given object(s). + * + * @param mixed $localId Either the identifier of the ansel object or + * an array of identifiers. + * @param string $type The type of object $localId represents. + * + * @return array A tag_id => tag_name hash, possibly wrapped in a localid hash. + * @throws Ansel_Exception + */ + public function getTags($localId, $type = 'image') + { + try { + if (is_array($localId)) { + return $this->_tagger->getTagsByObjects($localId, $type); + } + + return $this->_tagger->getTags(array('objectId' => array('object' => $localId, 'type' => $this->_type_ids[$type]))); + } catch (Content_Exception $e) { + throw new Ansel_Exception($e); + } + } + + /** + * Retrieve a set of tags that are related to the specifed set. A tag is + * related if resources tagged with the specified set are also tagged with + * the tag being considered. Used to "browse" tagged resources. + * + * @param array $tags An array of tags to check. This would represent the + * current "directory" of tags while browsing. + * @param string $user The resource must be owned by this user. + * + * @return array An tag_id => tag_name hash + */ + public function browseTags($tags, $user) + { + try { + $tags = array_values($this->_tagger->getTagIds($tags)); + $gtags = $this->_tagger->browseTags($tags, $this->_type_ids['gallery'], $user); + $itags = $this->_tagger->browseTags($tags, $this->_type_ids['image'], $user); + } catch (Content_Exception $e) { + throw new Ansel_Exception($e); + } + /* Can't use array_merge here since it would renumber the array keys */ + foreach ($gtags as $id => $name) { + if (empty($itags[$id])) { + $itags[$id] = $name; + } + } + + return $itags; + } + + /** + * Get tag ids for the specified tag names. + * + * @param string|array $tags Either a tag_name or array of tag_names. + * + * @return array A tag_id => tag_name hash. + * @throws Ansel_Exception + */ + public function getTagIds($tags) + { + try { + return $this->_tagger->getTagIds($tags); + } catch (Content_Exception $e) { + throw new Ansel_Exception($e); + } + } + + /** + * Untag a resource. + * + * Removes the tag regardless of the user that added the tag. + * + * @param string $localId The ansel object identifier. + * @param mixed $tags Either a tag_id, tag_name or an array. + * @param string $content_type The type of object that $localId represents. + * + * @return void + */ + public function untag($localId, $tags, $content_type = 'image') + { + try { + $this->_tagger->removeTagFromObject( + array('object' => $localId, 'type' => $this->_type_ids[$content_type]), $tags); + } catch (Content_Exception $e) { + throw new Ansel_Exception($e); + } + } + + /** + * Tags the given resource with *only* the tags provided, removing any + * tags that are already present but not in the list. + * + * @param string $localId The identifier for the ansel object. + * @param mixed $tags Either a tag_id, tag_name, or array of tag_ids. + * @param string $owner The tag owner - should normally be the resource + * owner. + * @param $content_type The type of object that $localId represents. + */ + public function replaceTags($localId, $tags, $owner, $content_type = 'image') + { + /* First get a list of existing tags. */ + $existing_tags = $this->getTags($localId, $content_type); + + if (!is_array($tags)) { + $tags = $this->_tagger->splitTags($tags); + } + $remove = array(); + foreach ($existing_tags as $tag_id => $existing_tag) { + $found = false; + foreach ($tags as $tag_text) { + if ($existing_tag == $tag_text) { + $found = true; + break; + } + } + /* Remove any tags that were not found in the passed in list. */ + if (!$found) { + $remove[] = $tag_id; + } + } + + $this->untag($localId, $remove, $content_type); + $add = array(); + foreach ($tags as $tag_text) { + $found = false; + foreach ($existing_tags as $existing_tag) { + if ($tag_text == $existing_tag) { + $found = true; + break; + } + } + if (!$found) { + $add[] = $tag_text; + } + } + + $this->tag($localId, $add, $owner, $content_type); + } + + /** + * Returns tags belonging to the current user beginning with $token. + * + * Used for autocomplete code. + * + * @param string $token The token to match the start of the tag with. + * + * @return A tag_id => tag_name hash + * @throws Ansel_Exception + */ + public function listTags($token) + { + try { + return $this->_tagger->getTags(array('q' => $token, 'userId' => $GLOBALS['registry']->getAuth())); + } catch (Content_Tagger $e) { + throw new Ansel_Exception($e); + } + } + + /** + * Returns the data needed to build a tag cloud based on the specified + * user's tag dataset. + * + * @param string $user The user whose tags should be included. + * If null, all users' tags are returned. + * @param integer $limit The maximum number of tags to include. + * + * @return Array An array of hashes, each containing tag_id, tag_name, and count. + * @throws Ansel_Exception + */ + public function getCloud($user, $limit = 5) + { + $filter = array('limit' => $limit, + 'typeId' => array_values($this->_type_ids)); + if (!empty($user)) { + $filter['userId'] = $user; + } + try { + return $this->_tagger->getTagCloud($filter); + } catch (Content_Exception $e) { + throw new Ansel_Exception($e); + } + } + + /** + * Returns cloud-like information, but only for a specified set of tags. + * Useful for displaying the counts of other images tagged with the same + * tag as the currently displayed image. + * + * @param array $tags An array of either tag names or ids. + * @param integer $limit Limit results to this many. + * + * @return array An array of hashes, tag_id, tag_name, and count. + * @throws Ansel_Exception + */ + public function getTagInfo($tags, $limit = 500, $type = null) + { + $filter = array('typeId' => empty($type) ? array_values($this->_type_ids) : $this->_type_ids[$type], + 'tagIds' => $tags, + 'limit' => $limit); + + try { + return $this->_tagger->getTagCloud($filter); + } catch (Content_Exception $e) { + throw new Ansel_Exception($e); + } + } + + /** + * Searches for resources that are tagged with all of the requested tags. + * + * @param array $tags Either a tag_id, tag_name or an array. + * @param array $filter Array of filter parameters. + * - type (string) - 'gallery' or 'image' + * - user (array) - only include objects owned by + * these users. + * + * @return A hash of 'calendars' and 'events' that each contain an array + * of calendar_ids and event_uids respectively. + * @throws Ansel_Exception + */ + public function search($tags, $filter = array()) + { + $args = array(); + + /* These filters are mutually exclusive */ + if (array_key_exists('user', $filter)) { + $args['userId'] = $filter['user']; + } elseif (!empty($filter['gallery'])) { + // Only events located in specific calendar(s) + if (!is_array($filter['gallery'])) { + $filter['gallry'] = array($filter['gallery']); + } + $args['gallery'] = $filter['gallery']; + } + + try { + /* Add the tags to the search */ + $args['tagId'] = $this->_tagger->getTagIds($tags); + + /* Restrict to images or galleries */ + $gal_results = $image_results = array(); + if (empty($filter['type']) || $filter['type'] == 'gallery') { + $args['typeId'] = $this->_type_ids['gallery']; + $gal_results = $this->_tagger->getObjects($args); + } + + if (empty($filter['type']) || $filter['type'] == 'image') { + $args['typeId'] = $this->_type_ids['image']; + $image_results = $this->_tagger->getObjects($args); + } + } catch (Content_Exception $e) { + throw new Ansel_Exception($e); + } + + /* TODO: Filter out images whose gallery has already matched? */ + $results = array('galleries' => array_values($gal_results), + 'images' => array_values($image_results)); + + return $results; + } + +} diff --git a/ansel/lib/Tags.php b/ansel/lib/Tags.php deleted file mode 100644 index 8d8a00409..000000000 --- a/ansel/lib/Tags.php +++ /dev/null @@ -1,675 +0,0 @@ - - * @category Horde - * @license http://www.fsf.org/copyleft/gpl.html GPL - * @package Ansel - */ - -/** - * Static helper class for writing/reading tag values - * - * @TODO: Move tag storage to Content_Tagger - * @static - */ -class Ansel_Tags -{ - /** - * Write out the tags for a specific resource. - * - * @param int $resource_id The resource we are tagging. - * @param array $tags An array of tags. - * @param string $resource_type The type of resource (image or gallery) - * - * @return boolean True on success - * @throws InvalidArgumentException - * @throws Ansel_Exception - */ - static public function writeTags($resource_id, $tags, $resource_type = 'image') - { - // First, make sure all tag names exist in the DB. - $tagkeys = array(); - $insert = $GLOBALS['ansel_db']->prepare('INSERT INTO ansel_tags (tag_id, tag_name) VALUES(?, ?)'); - foreach ($tags as $tag) { - if (!empty($tag)) { - if (!preg_match("/^[a-zA-Z0-9%_+.!*',()~-]*$/", $tag)) { - throw new InvalidArgumentException('Invalid characters in tag.'); - } - $tag = Horde_String::lower(trim($tag)); - $sql = $GLOBALS['ansel_db']->prepare('SELECT tag_id FROM ansel_tags WHERE tag_name = ?'); - $result = $sql->execute(Horde_String::convertCharset($tag, $GLOBALS['registry']->getCharset(), $GLOBALS['conf']['sql']['charset'])); - $results = $result->fetchRow(MDB2_FETCHMODE_ASSOC); - $result->free(); - - if (empty($results)) { - $id = $GLOBALS['ansel_db']->nextId('ansel_tags'); - $result = $insert->execute(array($id, Horde_String::convertCharset($tag, $GLOBALS['registry']->getCharset(), $GLOBALS['conf']['sql']['charset']))); - $tagkeys[] = $id; - } elseif ($results instanceof PEAR_Error) { - Horde::logMessage($results->getMessage(), 'ERR'); - throw new Ansel_Exception($results); - } else { - $tagkeys[] = $results['tag_id']; - } - } - } - $insert->free(); - - if ($resource_type == 'image') { - $delete = $GLOBALS['ansel_db']->prepare('DELETE FROM ansel_images_tags WHERE image_id = ?'); - $query = $GLOBALS['ansel_db']->prepare('INSERT INTO ansel_images_tags (image_id, tag_id) VALUES(?, ?)'); - } else { - $delete = $GLOBALS['ansel_db']->prepare('DELETE FROM ansel_galleries_tags WHERE gallery_id = ?'); - $query = $GLOBALS['ansel_db']->prepare('INSERT INTO ansel_galleries_tags (gallery_id, tag_id) VALUES(?, ?)'); - } - Horde::logMessage('SQL query by Ansel_Tags::writeTags: ' . $query->query, 'DEBUG'); - $delete->execute(array($resource_id)); - foreach ($tagkeys as $key) { - $query->execute(array($resource_id, $key)); - } - - $delete->free(); - $query->free(); - - /* We should clear at least any of our cached counts */ - Ansel_Tags::clearCache(); - return true; - } - - /** - * Retrieve the tags for a specified resource. - * - * @param integer $resource_id The resource to get tags for. - * @param string $resource_type The type of resource (gallery or image) - * - * @return mixed An array of tags - */ - static public function readTags($resource_id, $resource_type = 'image') - { - if ($resource_type == 'image') { - $stmt = $GLOBALS['ansel_db']->prepare('SELECT ansel_tags.tag_id, tag_name FROM ansel_tags INNER JOIN ansel_images_tags ON ansel_images_tags.tag_id = ansel_tags.tag_id WHERE ansel_images_tags.image_id = ?'); - } else { - $stmt = $GLOBALS['ansel_db']->prepare('SELECT ansel_tags.tag_id, tag_name FROM ansel_tags INNER JOIN ansel_galleries_tags ON ansel_galleries_tags.tag_id = ansel_tags.tag_id WHERE ansel_galleries_tags.gallery_id = ?'); - } - if ($stmt instanceof PEAR_Error) { - throw new Ansel_Exception($stmt); - } - Horde::logMessage('SQL query by Ansel_Tags::readTags ' . $stmt->query, 'DEBUG'); - $result = $stmt->execute((int)$resource_id); - $tags = $result->fetchAll(MDB2_FETCHMODE_ASSOC, true); - foreach ($tags as $id => $tag) { - $tags[$id] = Horde_String::convertCharset( - $tag, $GLOBALS['conf']['sql']['charset']); - } - $stmt->free(); - $result->free(); - - return $tags; - } - - /** - * Retrieve the list of used tag_names, tag_ids and the total number - * of resources that are linked to that tag. - * - * @param array $tags An optional array of tag_ids. If omitted, all tags - * will be included. - * @param integer $limit Limit the number of tags returned to this value. - * - * @return Array An array containing tag_name, and total - */ - static public function listTagInfo($tags = null, $limit = 500) - { - global $conf; - // Only return the full list if $tags is omitted, not if - // an empty array is passed - if (is_array($tags) && count($tags) == 0) { - return array(); - } - if ($GLOBALS['conf']['ansel_cache']['usecache']) { - $cache_key = 'ansel_taginfo_' . (!is_null($tags) ? md5(serialize($tags) . $limit) : $limit); - $cvalue = $GLOBALS['injector']->getInstance('Horde_Cache')->get($cache_key, $conf['cache']['default_lifetime']); - if ($cvalue) { - return unserialize($cvalue); - } - } - - $sql = 'SELECT tn.tag_id, tag_name, COUNT(tag_name) as total FROM ansel_tags as tn INNER JOIN (SELECT tag_id FROM ansel_galleries_tags UNION ALL SELECT tag_id FROM ansel_images_tags) as t ON t.tag_id = tn.tag_id '; - if (!is_null($tags) && is_array($tags)) { - $sql .= 'WHERE tn.tag_id IN (' . implode(',', $tags) . ') '; - } - $sql .= 'GROUP BY tn.tag_id, tag_name ORDER BY total DESC'; - if ($limit > 0) { - $GLOBALS['ansel_db']->setLimit((int)$limit); - } - - $results = $GLOBALS['ansel_db']->queryAll($sql, null, MDB2_FETCHMODE_ASSOC, true); - foreach ($results as $id => $taginfo) { - $results[$id]['tag_name'] = Horde_String::convertCharset( - $taginfo['tag_name'], $GLOBALS['conf']['sql']['charset']); - } - if ($GLOBALS['conf']['ansel_cache']['usecache']) { - $GLOBALS['injector']->getInstance('Horde_Cache')->set($cache_key, serialize($results)); - } - - return $results; - } - - /** - * Search for resources matching the specified criteria - * - * @param array $ids An array of tag_ids to search for. - * @param int $max The maximum number of resources to return. - * @param int $from The number to start from - * @param string $resource_type Either 'images', 'galleries', or 'all'. - * @param string $user Limit the result set to resources - * owned by this user. - * - * @return array An array of image_ids and gallery_ids - */ - static public function searchTagsById($ids, $max = 0, $from = 0, - $resource_type = 'all', $user = null) - { - if (!is_array($ids) || !count($ids)) { - return array('galleries' => array(), 'images' => array()); - } - - $skey = md5(serialize($ids) . $from . $resource_type . $max . $user); - - if ($GLOBALS['conf']['ansel_cache']['usecache']) { - $key = $GLOBALS['registry']->getAuth() . '__anseltagsearches'; - $cvalue = $GLOBALS['injector']->getInstance('Horde_Cache')->get($key, 300); - $cvalue = @unserialize($cvalue); - if (!$cvalue) { - $cvalue = array(); - } - if (!empty($cvalue[$skey])) { - return $cvalue[$skey]; - } - } - - $ids = array_values($ids); - $results = array(); - /* Retrieve any images that match */ - if ($resource_type != 'galleries') { - $sql = 'SELECT image_id, count(tag_id) FROM ansel_images_tags ' - . 'WHERE tag_id IN (' . implode(',', $ids) . ') GROUP BY ' - . 'image_id HAVING count(tag_id) = ' . count($ids); - - Horde::logMessage('SQL query by Ansel_Tags::searchTags: ' . $sql, 'DEBUG'); - $GLOBALS['ansel_db']->setLimit($max, $from); - $images = $GLOBALS['ansel_db']->queryCol($sql); - if ($images instanceof PEAR_Error) { - Horde::logMessage($images, 'ERR'); - $results['images'] = array(); - } else { - /* Check permissions and filter on $user if required */ - $imgs = array(); - foreach ($images as $id) { - try { - $img = $GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getImage($id); - $gal = $GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getGallery($img->gallery); - $owner = $gal->get('owner'); - if ($gal->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::SHOW) && - (!isset($user) || (isset($user) && $owner && $owner == $user))) { - $imgs[] = $id; - } - } catch (Ansel_Exception $e) { - Horde::logMessage($e->getMessage(), 'ERR'); - break; - } - } - $results['images'] = $imgs; - } - } - - /* Now get the galleries that match */ - if ($resource_type != 'images') { - $results['galleries'] = array(); - $sql = 'SELECT gallery_id, count(tag_id) FROM ansel_galleries_tags ' - . 'WHERE tag_id IN (' . implode(',', $ids) . ') GROUP BY ' - . 'gallery_id HAVING count(tag_id) = ' . count($ids); - - Horde::logMessage('SQL query by Ansel_Tags::searchTags: ' . $sql, 'DEBUG'); - $GLOBALS['ansel_db']->setLimit($max, $from); - - $galleries = $GLOBALS['ansel_db']->queryCol($sql); - if ($galleries instanceof PEAR_Error) { - Horde::logMessage($galleries, 'ERR'); - } else { - /* Check perms */ - foreach ($galleries as $id) { - try { - $gallery = $GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getGallery($id); - } catch (Ansel_Exception $e) { - Horde::logMessage($e->getMessage(), 'ERR'); - continue; - } - if ($gallery->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::SHOW) && (!isset($user) || (isset($user) && $gallery->get('owner') && $gallery->get('owner') == $user))) { - $results['galleries'][] = $id; - } - } - } - } - - if ($GLOBALS['conf']['ansel_cache']['usecache']) { - $cvalue[$skey] = $results; - $GLOBALS['injector']->getInstance('Horde_Cache')->set($key, serialize($cvalue)); - } - - return $results; - } - - /** - * Search for resources matching a specified set of tags - * and optionally limit the result set to resources owned by - * a specific user. - * - * @param array $names An array of tag strings to search for. - * @param int $max The maximum number of resources to return. - * @param int $from The resource to start at. - * @param string $resource_type Either 'images', 'galleries', or 'all'. - * @param string $user Limit the result set to resources owned by - * specified user. - * - * @return mixed An array of image_ids and gallery_ids - */ - static public function searchTags($names = array(), $max = 10, $from = 0, - $resource_type = 'all', $user = null) - { - /* Get the tag_ids */ - $ids = Ansel_Tags::getTagIds($names); - return Ansel_Tags::searchTagsbyId($ids, $max, $from, $resource_type, - $user); - } - - /** - * Retrieve a set of tags with relationships to the specified set - * of tags. - * - * @param array $tags An array of tag_ids - * - * @return mixed A hash of tag_id -> tag_name | PEAR_Error - */ - static public function getRelatedTags($ids) - { - if (!count($ids)) { - return array(); - } - - /* Build the monster SQL statement.*/ - $sql = 'SELECT DISTINCT t.tag_id, t.tag_name FROM ansel_images_tags as r, ansel_images as i, ansel_tags as t'; - for ($i = 0; $i < count($ids); $i++) { - $sql .= ',ansel_images_tags as r' . $i; - } - $sql .= ' WHERE r.tag_id = t.tag_id AND r.image_id = i.image_id'; - for ($i = 0; $i < count($ids); $i++) { - $sql .= ' AND r' . $i . '.image_id = r.image_id AND r.tag_id != ' . (int)$ids[$i] . ' AND r' . $i . '.tag_id = ' . (int)$ids[$i]; - } - - /* Note that we don't convertCharset here, it's done in listTagInfo */ - $imgtags = $GLOBALS['ansel_db']->queryAll($sql, null, MDB2_FETCHMODE_ASSOC, true); - - /* Now get the galleries. */ - $table = 'ansel_shares'; - $sql = 'SELECT DISTINCT t.tag_id, t.tag_name FROM ansel_galleries_tags as r, ' . $table . ' AS i, ansel_tags as t'; - for ($i = 0; $i < count($ids); $i++) { - $sql .= ', ansel_galleries_tags as r' . $i; - } - $sql .= ' WHERE r.tag_id = t.tag_id AND r.gallery_id = i.share_id'; - for ($i = 0; $i < count($ids); $i++) { - for ($i = 0; $i < count($ids); $i++) { - $sql .= ' AND r' . $i . '.gallery_id = r.gallery_id AND r.tag_id != ' . (int)$ids[$i] . ' AND r' . $i . '.tag_id = ' . (int)$ids[$i]; - } - } - $galtags = $GLOBALS['ansel_db']->queryAll($sql, null, MDB2_FETCHMODE_ASSOC, true); - - /* Can't use array_merge here since it would renumber the array keys */ - foreach ($galtags as $id => $name) { - if (empty($imgtags[$id])) { - $imgtags[$id] = $name; - } - } - - /* Get an array of tag info sorted by total */ - $tagids = array_keys($imgtags); - if (count($tagids)) { - $imgtags = Ansel_Tags::listTagInfo($tagids); - } - - return $imgtags; - } - - /** - * Get the URL for a tag link - * - * @param array $tags The tag ids to link to - * @param string $action The action we want to perform with this tag. - * @param string $owner The owner we want to filter the results by - * - * @return string The URL for this tag and action - */ - static public function getTagLinks($tags, $action = 'add', $owner = null) - { - $results = array(); - foreach ($tags as $id => $taginfo) { - $params = array('view' => 'Results', - 'tag' => $taginfo['tag_name']); - if (!empty($owner)) { - $params['owner'] = $owner; - } - if ($action != 'add') { - $params['actionID'] = $action; - } - $link = Ansel::getUrlFor('view', $params, true); - $results[$id] = $link; - } - - return $results; - } - - /** - * Get a list of tag_ids from a list of tag_names - * - * @param array $tags An array of tag_names - * - * @return array An array of tag_names => tag_ids - */ - static public function getTagIds($tags) - { - if (!count($tags)) { - return array(); - } - $stmt = $GLOBALS['ansel_db']->prepare('SELECT ansel_tags.tag_name, ansel_tags.tag_id FROM ansel_tags WHERE ansel_tags.tag_name IN (' . str_repeat('?, ', count($tags) - 1) . '?)'); - $result = $stmt->execute(array_values($tags)); - $ids = $result->fetchAll(MDB2_FETCHMODE_ASSOC, true); - $newIds = array(); - foreach ($ids as $tag => $id) { - $newIds[Horde_String::convertCharset($tag, $GLOBALS['conf']['sql']['charset'])] = $id; - } - $result->free(); - $stmt->free(); - - return $newIds; - } - - /** - * Get the tag names from ids - * - * @param array $ids An array of tag ids - * - * @return array A hash of tag_id => tag_names - */ - static public function getTagNames($ids) - { - if (!count($ids)) { - return array(); - } - $stmt = $GLOBALS['ansel_db']->prepare('SELECT t.tag_id, t.tag_name FROM ansel_tags as t WHERE t.tag_id IN(' . str_repeat('?, ', count($ids) - 1) . '?)'); - $result = $stmt->execute(array_values($ids)); - $tags = $result->fetchAll(MDB2_FETCHMODE_ASSOC, true); - foreach ($tags as $id => $tag) { - $tags[$id] = Horde_String::convertCharset( - $tag, $GLOBALS['conf']['sql']['charset']); - } - $result->free(); - $stmt->free(); - - return $tags; - } - - /** - * Retrieve an Ansel_Tags_Search object - * - * @return Ansel_Tags_Search - * @TODO: refactor into Ansel_Search - */ - static public function getSearch($tags = null, $owner = null) - { - return new Ansel_Tags_Search($tags, $owner); - } - - /** - * Clear the session cache - */ - static public function clearSearch() - { - unset($_SESSION['ansel_tags_search']); - } - - static public function clearCache() - { - if ($GLOBALS['conf']['ansel_cache']['usecache']) { - $GLOBALS['injector']->getInstance('Horde_Cache')->expire($GLOBALS['registry']->getAuth() . '__anseltagsearches'); - } - } -} - -/** - * Class that represents a slice of a tag search - * - * @TODO: Move this to Ansel_Search_Tags - */ -class Ansel_Tags_Search { - - var $_tags = array(); - var $_totalCount = null; - var $_owner = null; - var $_dirty = false; - - /** - * Constructor - * - * @param array $tags An array of tag_ids to match. If null is passed then - * the tags will be loaded from the session. - */ - function Ansel_Tags_Search($tags = null, $owner = null) - { - if (!empty($tags)) { - $this->_tags = $tags; - } else { - $this->_tags = (!empty($_SESSION['ansel_tags_search']) ? $_SESSION['ansel_tags_search'] : array()); - } - - $this->_owner = $owner; - } - - /** - * Save the current search to the session - */ - function save() - { - $_SESSION['ansel_tags_search'] = $this->_tags; - $this->_dirty = false; - } - - /** - * Fetch the matching resources that should appear on the current page - * - * @return Array of Ansel_Images and Ansel_Galleries - */ - function getSlice($page, $perpage) - { - global $conf, $registry; - - $results = array(); - $totals = $this->count(); - - /* First, the galleries */ - $gstart = $page * $perpage; - $gresults = Ansel_Tags::searchTagsById($this->_tags, - $perpage, - $gstart, - 'galleries', - $this->_owner); - $galleries = array(); - foreach ($gresults['galleries'] as $gallery) { - $galleries[] = $GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getGallery($gallery); - } - - /* Do we need to get images? */ - $istart = max(0, $page * $perpage - $totals['galleries']); - $count = $perpage - count($galleries); - if ($count > 0) { - $iresults = Ansel_Tags::searchTagsById($this->_tags, - $count, - $istart, - 'images', - $this->_owner); - - $images = count($iresults['images']) ? array_values($GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getImages(array('ids' => $iresults['images']))) : array(); - if (($conf['comments']['allow'] == 'all' || ($conf['comments']['allow'] == 'authenticated' && $GLOBALS['registry']->getAuth())) && - $registry->hasMethod('forums/numMessagesBatch')) { - - $ids = array_keys($images); - $ccounts = $GLOBALS['registry']->call('forums/numMessagesBatch', array($ids, 'ansel')); - if (!($ccounts instanceof PEAR_Error)) { - foreach ($images as $image) { - $image->commentCount = (!empty($ccounts[$image->id]) ? $ccounts[$image->id] : 0); - } - } - } - } else { - $images = array(); - } - return array_merge($galleries, $images); - } - - /** - * Add a tag to the cumulative tag search - */ - function addTag($tag_id) - { - if (array_search($tag_id, $this->_tags) === false) { - $this->_tags[] = $tag_id; - $this->_dirty = true; - } - } - - /** - * Remove a tag from the cumulative search - */ - function removeTag($tag_id) - { - $key = array_search($tag_id, $this->_tags); - if ($tag_id === false) { - return; - } else { - unset($this->_tags[$key]); - $this->_tags = array_values($this->_tags); - $this->_dirty = true; - } - } - - /** - * Get the list of currently choosen tags - */ - function getTags() - { - return $this->_tags; - } - - /** - * Get breadcrumb style navigation html for choosen tags - * - */ - function getTagTrail() - { - global $registry; - - $tags = Ansel_Tags::getTagNames($this->_tags); - $html = ''; - } - - /** - * Get the total number of tags included in this search. - */ - function tagCount() - { - return count($this->_tags); - } - - /** - * Get the total number of resources that match. - * - * @return array Hash containing totals for both 'galleries' and 'images'. - */ - function count() - { - if (!is_array($this->_tags) || !count($this->_tags)) { - return 0; - } - - /* First see if we already calculated for the current page load */ - if ($this->_totalCount && !$this->_dirty) { - return $this->_totalCount; - } - - /* Can't perform a COUNT query since we have to check perms */ - $results = Ansel_Tags::searchTagsById($this->_tags, 0, 0, 'all', - $this->_owner); - $count = array('galleries' => count($results['galleries']), 'images' => count($results['images'])); - $this->_totalCount = $count; - return $count; - } - - /** - * Get a list of tags related to this search - */ - function getRelatedTags() - { - $tags = Ansel_Tags::getRelatedTags($this->getTags()); - /* Make sure that we have actual results for each tag since - * some tags may exist on only images/galleries to which we - * have no perms */ - $search = Ansel_Tags::getSearch(null, $this->_owner); - $results = array(); - foreach ($tags as $id => $tag) { - $search->addTag($id); - $count = $search->count(); - if ($count['images'] + $count['galleries'] > 0) { - $results[$id] = array('tag_name' => $tag['tag_name'], 'total' => $count['images'] + $count['galleries']); - } - $search->removeTag($id); - } - - /* Get the results sorted by available totals for this user */ - uasort($results, array($this, '_sortTagInfo')); - return $results; - } - - /** - */ - function _sortTagInfo($a, $b) - { - return $a['total'] < $b['total']; - } - -} diff --git a/ansel/lib/View/GalleryRenderer/Base.php b/ansel/lib/View/GalleryRenderer/Base.php index 164e3cd30..386fc333d 100644 --- a/ansel/lib/View/GalleryRenderer/Base.php +++ b/ansel/lib/View/GalleryRenderer/Base.php @@ -21,11 +21,11 @@ abstract class Ansel_View_GalleryRenderer_Base /** * The gallery id for this view's gallery - * + * (Convienience instead of $this->view->gallery->id) + * * @var integer */ - public $galleryId; // TODO: probably can remove this (get the id from the view's gallery) - + public $galleryId; /** * Gallery slug for current gallery. * diff --git a/ansel/lib/View/Results.php b/ansel/lib/View/Results.php index 2275552df..a25b33983 100644 --- a/ansel/lib/View/Results.php +++ b/ansel/lib/View/Results.php @@ -3,8 +3,15 @@ * The Ansel_View_Results:: class wraps display of images/galleries from * multiple parent sources.. * - * @author Michael Rubinsky - * @package Ansel + * Copyright 2007-2010 The Horde Project (http://www.horde.org/) + * + * See the enclosed file COPYING for license information (GPL). If you + * did not receive this file, see http://www.fsf.org/copyleft/gpl.html. + * + * @author Michael J. Rubinsky + * @category Horde + * @license http://www.fsf.org/copyleft/gpl.html GPL + * @package Ansel */ class Ansel_View_Results extends Ansel_View_Base { @@ -22,7 +29,18 @@ class Ansel_View_Results extends Ansel_View_Base */ protected $_owner; + /** + * The current page + * + * @var integer + */ private $_page; + + /** + * Number of resources per page. + * + * @var integer + */ private $_perPage; /** @@ -39,8 +57,9 @@ class Ansel_View_Results extends Ansel_View_Base $notification = $GLOBALS['injector']->getInstance('Horde_Notification'); $ansel_storage = $GLOBALS['injector']->getInstance('Ansel_Storage')->getScope(); - $this->_owner = Horde_Util::getFormData('owner', null); - $this->_search = Ansel_Tags::getSearch(null, $this->_owner); + $this->_owner = Horde_Util::getFormData('owner', ''); + //@TODO: Inject the search object when we have more then just a tag search + $this->_search = new Ansel_Search_Tag($GLOBALS['injector']->getInstance('Ansel_Tagger'), null, $this->_owner); $this->_page = Horde_Util::getFormData('page', 0); $action = Horde_Util::getFormData('actionID', ''); $image_id = Horde_Util::getFormData('image'); @@ -68,7 +87,6 @@ class Ansel_View_Results extends Ansel_View_Base try { $result = $gallery->removeImage($image); $notification->push(_("Deleted the photo."), 'horde.success'); - Ansel_Tags::clearCache(); } catch (Ansel_Exception $e) { $notification->push( sprintf(_("There was a problem deleting photos: %s"), $e->getMessage()), 'horde.error'); @@ -164,8 +182,6 @@ class Ansel_View_Results extends Ansel_View_Base case 'remove': $tag = Horde_Util::getFormData('tag'); if (isset($tag)) { - $tag = Ansel_Tags::getTagIds(array($tag)); - $tag = array_pop($tag); $this->_search->removeTag($tag); $this->_search->save(); } @@ -175,8 +191,6 @@ class Ansel_View_Results extends Ansel_View_Base default: $tag = Horde_Util::getFormData('tag'); if (isset($tag)) { - $tag = Ansel_Tags::getTagIds(array($tag)); - $tag = array_pop($tag); $this->_search->addTag($tag); $this->_search->save(); } @@ -227,7 +241,8 @@ class Ansel_View_Results extends Ansel_View_Base if ($conf['tags']['relatedtags']) { $rtags = $this->_search->getRelatedTags(); $rtaghtml = '
    '; - $links = Ansel_Tags::getTagLinks($rtags, 'add'); + + $links = Ansel::getTagLinks($rtags, 'add'); foreach ($rtags as $id => $taginfo) { if (!empty($this->_owner)) { $links[$id]->add('owner', $this->_owner); @@ -244,7 +259,6 @@ class Ansel_View_Results extends Ansel_View_Base $vars = Horde_Variables::getDefaultVariables(); $option_move = $option_copy = $ansel_storage->countGalleries(Horde_Perms::EDIT); - $this->_pagestart = ($this->_page * $this->_perPage) + 1; $this->_pageend = min($this->_pagestart + $numimages - 1, $this->_pagestart + $this->_perPage - 1); $this->_pager = new Horde_Core_Ui_Pager('page', $vars, array('num' => $total, diff --git a/ansel/lib/Widget/ImageFaces.php b/ansel/lib/Widget/ImageFaces.php index e585d1de3..727d0fee5 100644 --- a/ansel/lib/Widget/ImageFaces.php +++ b/ansel/lib/Widget/ImageFaces.php @@ -62,7 +62,6 @@ class Ansel_Widget_ImageFaces extends Ansel_Widget_Base // Generate the top ajax action links and attach the edit actions. Falls // back on going to the find all faces in gallery page if no js... // although, currently, *that* page requires js as well so... - // TODO: A way to 'close', or go back to, the normal widget view. if ($this->_view->gallery->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::EDIT)) { $link_text = (empty($images) ? _("Find faces") : _("Edit faces")); $html .= Horde::applicationUrl('faces/gallery.php')->add('gallery', $this->_view->gallery->id)->link( diff --git a/ansel/lib/Widget/SimilarPhotos.php b/ansel/lib/Widget/SimilarPhotos.php index 08637800a..8cdad5d64 100755 --- a/ansel/lib/Widget/SimilarPhotos.php +++ b/ansel/lib/Widget/SimilarPhotos.php @@ -9,7 +9,7 @@ class Ansel_Widget_SimilarPhotos extends Ansel_Widget_Base { /** - * @TODO + * Array of views that this widget may appear in. * * @var unknown_type */ @@ -46,7 +46,7 @@ class Ansel_Widget_SimilarPhotos extends Ansel_Widget_Base * * @TODO Rethink the way we determine if an image is related. This one is * not ideal, as it just pops tags off the tag list until all the tags - * match. This could miss many related images. + * match. This could miss many related images. Maybe make this random? * * @return string The HTML */ @@ -56,11 +56,10 @@ class Ansel_Widget_SimilarPhotos extends Ansel_Widget_Base $html = ''; $tags = array_values($this->_view->resource->getTags()); - $imgs = Ansel_Tags::searchTags($tags); - + $imgs = $GLOBALS['injector']->getInstance('Ansel_Tagger')->search($tags); while (count($imgs['images']) <= 5 && count($tags)) { array_pop($tags); - $newImgs = Ansel_Tags::searchTags($tags); + $newImgs =$GLOBALS['injector']->getInstance('Ansel_Tagger')->search($tags); $imgs['images'] = array_merge($imgs['images'], $newImgs['images']); } if (count($imgs['images'])) { diff --git a/ansel/lib/Widget/Tags.php b/ansel/lib/Widget/Tags.php index dc8cfbf39..edbfdda09 100644 --- a/ansel/lib/Widget/Tags.php +++ b/ansel/lib/Widget/Tags.php @@ -68,21 +68,23 @@ class Ansel_Widget_Tags extends Ansel_Widget_Base /* Clear the tag cache? */ if (Horde_Util::getFormData('havesearch', 0) == 0) { - Ansel_Tags::clearSearch(); + Ansel_Search_Tag::clearSearch(); } + $tagger = $GLOBALS['injector']->getInstance('Ansel_Tagger'); $hasEdit = $this->_view->gallery->hasPermission($GLOBALS['registry']->getAuth(), Horde_Perms::EDIT); $owner = $this->_view->gallery->get('owner'); - $tags = $this->_view->resource->getTags(); + $tags = $tagger->getTags((int)$this->_view->resource->id, $this->_resourceType); if (count($tags)) { - $tags = Ansel_Tags::listTagInfo(array_keys($tags)); + $tags = $tagger->getTagInfo(array_keys($tags), 500, $this->_resourceType); } - $links = Ansel_Tags::getTagLinks($tags, 'add', $owner); + $links = Ansel::getTagLinks($tags, 'add', $owner); $html = '
      '; - foreach ($tags as $tag_id => $taginfo) { - $html .= '
    • ' . $links[$tag_id]->link(array('title' => sprintf(ngettext("%d photo", "%d photos", $taginfo['total']), $taginfo['total']))) . htmlspecialchars($taginfo['tag_name']) . '' . ($hasEdit ? '' . Horde::img('delete-small.png', _("Remove Tag")) . '' : '') . '
    • '; + foreach ($tags as $taginfo) { + $tag_id = $taginfo['tag_id']; + $html .= '
    • ' . $links[$tag_id]->link(array('title' => sprintf(ngettext("%d photo", "%d photos", $taginfo['count']), $taginfo['count']))) . htmlspecialchars($taginfo['tag_name']) . '' . ($hasEdit ? '' . Horde::img('delete-small.png', _("Remove Tag")) . '' : '') . '
    • '; } $html .= '
    '; diff --git a/ansel/rss.php b/ansel/rss.php index 43abc55df..b4ca043e6 100644 --- a/ansel/rss.php +++ b/ansel/rss.php @@ -179,9 +179,10 @@ if (empty($rss)) { break; case 'tag': - $tag_id = array_values(Ansel_Tags::getTagIds(array($id))); - $images = Ansel_Tags::searchTagsById($tag_id, 10, 0, 'images'); - $tag_id = array_pop($tag_id); + $filter = array('typeId' => 'image', + 'limit' => 10); + $images = $GLOBALS['injector']->getInstance('Ansel_Tagger')->search(array($id), $filter); + try { $images = $GLOBALS['injector']->getInstance('Ansel_Storage')->getScope()->getImages(array('ids' => $images['images'])); } catch (Ansel_Exception $e) { @@ -189,7 +190,6 @@ if (empty($rss)) { $images = array(); } if (count($images)) { - $tag_id = $tag_id[0]; $images = array_values($images); $params = array('last_modified' => $images[0]->uploaded, 'name' => sprintf(_("Photos tagged with %s on %s"), diff --git a/ansel/scripts/upgrades/2010-07-30_migrate_tags_to_content.php b/ansel/scripts/upgrades/2010-07-30_migrate_tags_to_content.php new file mode 100644 index 000000000..d46c562e3 --- /dev/null +++ b/ansel/scripts/upgrades/2010-07-30_migrate_tags_to_content.php @@ -0,0 +1,39 @@ +#!/usr/bin/env php + + */ +require_once dirname(__FILE__) . '/../../lib/Application.php'; +Horde_Registry::appInit('ansel', array('authentication' => 'none', 'cli' => true)); + +/* Gallery tags */ +$sql = 'SELECT gallery_id, tag_name, share_owner FROM ansel_shares RIGHT JOIN ' + . 'ansel_galleries_tags ON ansel_shares.share_id = ansel_galleries_tags.gallery_id ' + . 'LEFT JOIN ansel_tags ON ansel_tags.tag_id = ansel_galleries_tags.tag_id;'; + +// Maybe iterate over results and aggregate them by user and gallery so we can +// tag all tags for a single gallery at once. Probably not worth it for a one +// time upgrade script. +$cli->message('Migrating gallery tags. This may take a while.', 'cli.message'); +$rows = $ansel_db->queryAll($sql); +foreach ($rows as $row) { + $GLOBALS['injector']->getInstance('Ansel_Tagger')->tag($row[0], $row[1], $row[2], 'gallery'); +} +$cli->message('Gallery tags finished.', 'cli.success'); + +$sql = 'SELECT ansel_images.image_id, tag_name, share_owner FROM ansel_images ' + . 'RIGHT JOIN ansel_images_tags ON ansel_images.image_id = ansel_images_tags.image_id ' + . 'LEFT JOIN ansel_shares ON ansel_shares.share_id = ansel_images.gallery_id ' + . 'LEFT JOIN ansel_tags ON ansel_tags.tag_id = ansel_images_tags.tag_id'; +$cli->message('Migrating image tags. This may take even longer...', 'cli.message'); +$rows = $ansel_db->queryAll($sql); +foreach ($rows as $row) { + $GLOBALS['injector']->getInstance('Ansel_Tagger')->tag($row[0], $row[1], $row[2], 'image'); +} +$cli->message('Image tags finished.', 'cli.success'); + diff --git a/ansel/templates/view/results.inc b/ansel/templates/view/results.inc index 9bee44dfa..0b2266baf 100644 --- a/ansel/templates/view/results.inc +++ b/ansel/templates/view/results.inc @@ -122,7 +122,7 @@ echo htmlspecialchars($this->getTitle(), ENT_COMPAT, $GLOBALS['registry']->getCh
    - +

    diff --git a/content/lib/Exception.php b/content/lib/Exception.php new file mode 100644 index 000000000..834a7f5b3 --- /dev/null +++ b/content/lib/Exception.php @@ -0,0 +1,3 @@ +ensureTags($tags) as $tagId) { try { - $this->_db->insert('INSERT INTO ' . $this->_t('tagged') . ' (user_id, object_id, tag_id, created) + $this->_db->insert('INSERT INTO ' . $this->_t('tagged') . ' (user_id, object_id, tag_id, created) VALUES (' . (int)$userId . ',' . (int)$objectId . ',' . (int)$tagId . ',' . $this->_db->quote($created) . ')'); } catch (Horde_Db_Exception $e) { // @TODO should make sure it's a duplicate and re-throw if not @@ -174,7 +174,6 @@ class Content_Tagger if (!is_array($tags)) { $tags = array($tags); } - foreach ($this->ensureTags($tags) as $tagId) { // Get the users who have tagged this so we can update the stats $users = $this->_db->selectValues('SELECT user_id, tag_id FROM ' . $this->_t('tagged') . ' WHERE object_id = ? AND tag_id = ?', array($objectId, $tagId)); @@ -255,6 +254,7 @@ class Content_Tagger if (!$args['objectId']) { return array(); } + $sql = 'SELECT DISTINCT t.tag_id AS tag_id, tag_name FROM ' . $this->_t('tags') . ' AS t INNER JOIN ' . $this->_t('tagged') . ' AS tagged ON t.tag_id = tagged.tag_id AND tagged.object_id = ' . (int)$args['objectId']; } elseif (isset($args['userId']) && isset($args['typeId'])) { $args['userId'] = current($this->_userManager->ensureUsers($args['userId'])); @@ -298,8 +298,9 @@ class Content_Tagger * limit Maximum number of tags to return. * offset Offset the results. Only useful for paginating, and not recommended. * userId Only return tags that have been applied by a specific user. - * typeId Only return tags that have been applied by a specific object type. + * typeId Only return tags that have been applied by specific object types. * objectId Only return tags that have been applied to a specific object. + * tagIds Only return information on specific tag (an array of tag names or tag ids) * * @return array An array of hashes, each containing tag_id, tag_name, and count. */ @@ -310,25 +311,41 @@ class Content_Tagger $sql = 'SELECT t.tag_id AS tag_id, tag_name, COUNT(*) AS count FROM ' . $this->_t('tagged') . ' AS tagged INNER JOIN ' . $this->_t('tags') . ' AS t ON tagged.tag_id = t.tag_id WHERE tagged.object_id = ' . (int)$args['objectId'] . ' GROUP BY t.tag_id'; } elseif (isset($args['userId']) && isset($args['typeId'])) { $args['userId'] = current($this->_userManager->ensureUsers($args['userId'])); - $args['typeId'] = current($this->_typeManager->ensureTypes($args['typeId'])); + $args['typeId'] = $this->_typeManager->ensureTypes($args['typeId']); // This doesn't use a stat table, so may be slow. - $sql = 'SELECT t.tag_id AS tag_id, tag_name, COUNT(*) AS count FROM ' . $this->_t('tagged') . ' AS tagged INNER JOIN ' . $this->_t('objects') . ' AS objects ON tagged.object_id = objects.object_id AND objects.type_id = ' . (int)$args['typeId'] . ' INNER JOIN ' . $this->_t('tags') . ' AS t ON tagged.tag_id = t.tag_id WHERE tagged.user_id = ' . (int)$args['user_id'] . ' GROUP BY t.tag_id'; + $sql = 'SELECT t.tag_id AS tag_id, tag_name, COUNT(*) AS count FROM ' . $this->_t('tagged') . ' AS tagged INNER JOIN ' . $this->_t('objects') . ' AS objects ON tagged.object_id = objects.object_id AND objects.type_id IN (' . implode(',', $args['typeId']) . ') INNER JOIN ' . $this->_t('tags') . ' AS t ON tagged.tag_id = t.tag_id WHERE tagged.user_id = ' . (int)$args['user_id'] . ' GROUP BY t.tag_id'; } elseif (isset($args['userId'])) { $args['userId'] = current($this->_userManager->ensureUsers($args['userId'])); $sql = 'SELECT t.tag_id AS tag_id, tag_name, count FROM ' . $this->_t('tagged') . ' AS tagged INNER JOIN ' . $this->_t('tags') . ' AS t ON tagged.tag_id = t.tag_id INNER JOIN ' . $this->_t('user_tag_stats') . ' AS uts ON t.tag_id = uts.tag_id AND uts.user_id = ' . (int)$args['userId'] . ' GROUP BY t.tag_id, tag_name, count'; + } elseif (isset($args['tagIds']) && isset($args['typeId'])) { + $args['typeId'] = $this->_typeManager->ensureTypes($args['typeId']); + // This doesn't use a stat table, so may be slow. + $sql = 'SELECT t.tag_id AS tag_id, tag_name, COUNT(*) AS count FROM ' . $this->_t('tagged') . ' AS tagged INNER JOIN ' . $this->_t('objects') . ' AS objects ON tagged.object_id = objects.object_id AND objects.type_id IN(' . implode(',', $args['typeId']) . ') INNER JOIN ' . $this->_t('tags') . ' AS t ON tagged.tag_id = t.tag_id AND t.tag_id IN (' . implode(', ', $args['tagIds']) . ') GROUP BY t.tag_id'; } elseif (isset($args['typeId'])) { - $args['typeId'] = current($this->_typeManager->ensureTypes($args['typeId'])); + $args['typeId'] = $this->_typeManager->ensureTypes($args['typeId']); // This doesn't use a stat table, so may be slow. - $sql = 'SELECT t.tag_id AS tag_id, tag_name, COUNT(*) AS count FROM ' . $this->_t('tagged') . ' AS tagged INNER JOIN ' . $this->_t('objects') . ' AS objects ON tagged.object_id = objects.object_id AND objects.type_id = ' . (int)$args['typeId'] . ' INNER JOIN ' . $this->_t('tags') . ' AS t ON tagged.tag_id = t.tag_id GROUP BY t.tag_id'; + $sql = 'SELECT t.tag_id AS tag_id, tag_name, COUNT(*) AS count FROM ' . $this->_t('tagged') . ' AS tagged INNER JOIN ' . $this->_t('objects') . ' AS objects ON tagged.object_id = objects.object_id AND objects.type_id IN(' . implode(',', $args['typeId']) . ') INNER JOIN ' . $this->_t('tags') . ' AS t ON tagged.tag_id = t.tag_id GROUP BY t.tag_id'; + } elseif (isset($args['tagIds'])) { + $ids = $this->_checkTags($args['tagIds'], false); + $sql = 'SELECT t.tag_id AS tag_id, tag_name, COUNT(*) AS count FROM ' . $this->_t('tagged') . ' AS tagged INNER JOIN ' . $this->_t('tags') . ' AS t ON tagged.tag_id = t.tag_id INNER JOIN ' . $this->_t('tag_stats') . ' AS ts ON t.tag_id = ts.tag_id WHERE t.tag_id IN (' . implode(', ', $ids) . ') GROUP BY t.tag_id'; } else { - $sql = 'SELECT t.tag_id AS tag_id, tag_name, count FROM ' . $this->_t('tagged') . ' AS tagged INNER JOIN ' . $this->_t('tags') . ' AS t ON tagged.tag_id = t.tag_id INNER JOIN ' . $this->_t('tag_stats') . ' AS ts ON t.tag_id = ts.tag_id GROUP BY t.tag_id'; + $sql = 'SELECT t.tag_id AS tag_id, tag_name, COUNT(*) AS count FROM ' . $this->_t('tagged') . ' AS tagged INNER JOIN ' . $this->_t('tags') . ' AS t ON tagged.tag_id = t.tag_id INNER JOIN ' . $this->_t('tag_stats') . ' AS ts ON t.tag_id = ts.tag_id GROUP BY t.tag_id'; } if (isset($args['limit'])) { $sql = $this->_db->addLimitOffset($sql . ' ORDER BY count DESC', array('limit' => $args['limit'], 'offset' => isset($args['offset']) ? $args['offset'] : 0)); } - return $this->_db->selectAll($sql); + try { + $rows = $this->_db->selectAll($sql); + $results = array(); + foreach ($rows as $row) { + $results[$row['tag_id']] = $row; + } + return $results; + } catch (Exception $e) { + throw new Content_Exception($e); + } } /** @@ -431,7 +448,7 @@ class Content_Tagger if (array_key_exists('userId', $args)) { $args['userId'] = $this->_userManager->ensureUsers($args['userId']); - $sql .= ' AND tagged.user_id IN ( ' . implode(', ', $args['userId']) . ')'; + $sql .= ' AND tagged.user_id IN (' . implode(', ', $args['userId']) . ')'; } } @@ -644,10 +661,11 @@ class Content_Tagger /** * Check if tags exists, optionally create then if they don't and return * ids for all that exist (including those that are optionally created). - * - * @param $tags - * @param $create - * @return \ + * + * @param string|array $tags The tag names to check. + * @param boolean $create If true, create the tag in the tags table. + * + * @return array */ protected function _checkTags($tags, $create = true) { @@ -743,6 +761,37 @@ class Content_Tagger } /** + * Retrieve a set of tags with relationships to the specified set + * of tags. + * + * @param array $ids An array of tag_ids. + * @param integer $object The object type to limit to. + * @param string $user The user to limit to. + * + * @return array A hash of tag_id -> tag_name + */ + public function browseTags($ids, $object_type, $user) + { + if (!count($ids)) { + return array(); + } + + $sql = 'SELECT DISTINCT t.tag_id, t.tag_name FROM ' . $this->_t('tagged') . ' as r, ' . $this->_t('objects') . ' as i, ' . $this->_t('tags') . ' as t'; + for ($i = 0; $i < count($ids); $i++) { + $sql .= ',' . $this->_t('tagged') . ' as r' . $i; + } + $sql .= ' WHERE r.tag_id = t.tag_id AND r.object_id = i.object_id'; + for ($i = 0; $i < count($ids); $i++) { + $sql .= ' AND r' . $i . '.object_id = r.object_id AND r.tag_id != ' . (int)$ids[$i] . ' AND r' . $i . '.tag_id = ' . (int)$ids[$i]; + } + + /* Note that we don't convertCharset here, it's done in listTagInfo */ + $tags = $GLOBALS['ansel_db']->queryAll($sql, null, MDB2_FETCHMODE_ASSOC, true); + + return $tags; + } + + /** * Convenience method - if $object is an array, it is taken as an array of * 'object' and 'type' to pass to objectManager::ensureObjects() if it's a * scalar value, it's taken as the object_id and simply returned. -- 2.11.0