Move Ansel's tag functionality to content tagger.
authorMichael J. Rubinsky <mrubinsk@horde.org>
Sat, 31 Jul 2010 14:30:41 +0000 (10:30 -0400)
committerMichael J. Rubinsky <mrubinsk@horde.org>
Sat, 31 Jul 2010 14:30:41 +0000 (10:30 -0400)
Need to run the (non-destructive) 2010-07-30_migrate_tags_to_content.php script to move
existing ansel tags to content.

22 files changed:
ansel/browse.php
ansel/lib/Ajax/Imple/EditGalleryFaces.php [new file with mode: 0644]
ansel/lib/Ajax/Imple/TagActions.php
ansel/lib/Ansel.php
ansel/lib/Application.php
ansel/lib/Block/cloud.php
ansel/lib/Gallery.php
ansel/lib/Image.php
ansel/lib/Search/Tag.php [new file with mode: 0644]
ansel/lib/Storage.php
ansel/lib/Tagger.php [new file with mode: 0644]
ansel/lib/Tags.php [deleted file]
ansel/lib/View/GalleryRenderer/Base.php
ansel/lib/View/Results.php
ansel/lib/Widget/ImageFaces.php
ansel/lib/Widget/SimilarPhotos.php
ansel/lib/Widget/Tags.php
ansel/rss.php
ansel/scripts/upgrades/2010-07-30_migrate_tags_to_content.php [new file with mode: 0644]
ansel/templates/view/results.inc
content/lib/Exception.php [new file with mode: 0644]
content/lib/Tagger.php

index 10bdcc5..4be576b 100644 (file)
@@ -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 '<div id="menuBottom"><a href="' . Horde::applicationUrl('browse_edit.php') . '">' . _("Add Content") . '</a></div><div class="clear">&nbsp;</div>';
diff --git a/ansel/lib/Ajax/Imple/EditGalleryFaces.php b/ansel/lib/Ajax/Imple/EditGalleryFaces.php
new file mode 100644 (file)
index 0000000..3662ecd
--- /dev/null
@@ -0,0 +1,125 @@
+<?php
+/**
+ * Ansel_Ajax_Imple_EditGalleryFaces:: class for performing Ajax discovery of
+ * an entire gallery's images.
+ *
+ * Copyright 2008-2010 The Horde Project (http://www.horde.org/)
+ *
+ * @author Duck <duck@obala.net>
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ *
+ * @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;
+            }
+        }
+    }
+
+}
index 69b08fa..1d4cbac 100644 (file)
@@ -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 = '<ul>';
-        foreach ($tags as $tag_id => $taginfo) {
-            $html .= '<li>' . $links[$tag_id]->link(array('title' => sprintf(ngettext("%d photo", "%d photos", $taginfo['total']), $taginfo['total']))) . htmlspecialchars($taginfo['tag_name']) . '</a>' . ($hasEdit ? '<a href="#" onclick="removeTag(' . $tag_id . ');">' . Horde::img('delete-small.png', _("Remove Tag")) . '</a>' : '') . '</li>';
+        foreach ($tags as $taginfo) {
+            $tag_id = $taginfo['tag_id'];
+            $html .= '<li>' . $links[$tag_id]->link(array('title' => sprintf(ngettext("%d photo", "%d photos", $taginfo['count']), $taginfo['count']))) . htmlspecialchars($taginfo['tag_name']) . '</a>' . ($hasEdit ? '<a href="#" onclick="removeTag(' . $tag_id . ');">' . Horde::img('delete-small.png', _("Remove Tag")) . '</a>' : '') . '</li>';
         }
         $html .= '</ul>';
         return $html;
index 07780e0..5b222c3 100644 (file)
@@ -1011,4 +1011,35 @@ class Ansel
        return '<script type="text/javascript" src="' . $imple->getUrl() . '"></script><div id="' . $domid . '"></div>';
     }
 
+    /**
+     * 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;
+    }
+
 }
index 38f5407..8b9f393 100644 (file)
@@ -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(),
index d2a2be4..f5c669e 100644 (file)
@@ -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 {
index 0252bba..9c2b9c8 100644 (file)
@@ -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."));
         }
index a5a0c7d..1407110 100644 (file)
@@ -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 (file)
index 0000000..6a2e9f0
--- /dev/null
@@ -0,0 +1,300 @@
+<?php
+/**
+ * Ansel_Search_Tags:: class provides logic for dealing with tag searching.
+ *
+ * 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 <mrubinsk@horde.org>
+ * @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 = '<ul class="tag-list">';
+
+        /* Use the local cache to preserve the order */
+        $count = 0;
+        foreach ($this->_tags as $tagname => $tagid) {
+            $remove_url = Horde::applicationUrl('view.php', true)->add(
+                    array('view' => 'Results',
+                          'tag' => $tagname,
+                          'actionID' => 'remove'));
+            if (!empty($this->_owner)) {
+                $remove_url->add('owner', $this->_owner);
+            }
+            $delete_label = sprintf(_("Remove %s from search"), htmlspecialchars($tagname));
+            $html .= '<li>' . htmlspecialchars($tagname) . $remove_url->link(array('title' => $delete_label)) . Horde::img('delete-small.png', $delete_label) . '</a></li>';
+        }
+
+        return $html . '</ul>';
+    }
+
+    /**
+     * 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
index f59da14..670cc1e 100644 (file)
@@ -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 (file)
index 0000000..e679e84
--- /dev/null
@@ -0,0 +1,349 @@
+<?php
+/**
+ * The Ansel_Tagger:: class provides logic for dealing with tags within Ansel
+ *
+ * Copyright 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 <mrubinsk@horde.org>
+ * @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 (file)
index 8d8a004..0000000
+++ /dev/null
@@ -1,675 +0,0 @@
-<?php
-/**
- * Classes for dealing with tags within 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.
- *
- * 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 <mrubinsk@horde.org>
- * @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 = '<ul class="tag-list">';
-
-        /* Use the local cache to preserve the order */
-        $count = 0;
-        foreach ($this->_tags as $tagid) {
-            $remove_url = Horde::applicationUrl('view.php', true)->add(
-                    array('view' => 'Results',
-                          'tag' => $tags[$tagid],
-                          'actionID' => 'remove'));
-            if (!empty($this->_owner)) {
-                $remove_url->add('owner', $this->_owner);
-            }
-            $delete_label = sprintf(_("Remove %s from search"), htmlspecialchars($tags[$tagid]));
-            $html .= '<li>' . htmlspecialchars($tags[$tagid]) . $remove_url->link(array('title' => $delete_label)) . Horde::img('delete-small.png', $delete_label) . '</a></li>';
-        }
-
-        return $html . '</ul>';
-    }
-
-    /**
-     * 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'];
-    }
-
-}
index 164e3cd..386fc33 100644 (file)
@@ -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.
      *
index 2275552..a25b339 100644 (file)
@@ -3,8 +3,15 @@
  * The Ansel_View_Results:: class wraps display of images/galleries from
  * multiple parent sources..
  *
- * @author  Michael Rubinsky <mrubinsk@horde.org>
- * @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 <mrubinsk@horde.org>
+ * @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 = '<ul>';
-            $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,
index e585d1d..727d0fe 100644 (file)
@@ -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(
index 0863780..8cdad5d 100755 (executable)
@@ -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'])) {
index dc8cfbf..edbfdda 100644 (file)
@@ -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 = '<ul>';
-        foreach ($tags as $tag_id => $taginfo) {
-            $html .= '<li>' . $links[$tag_id]->link(array('title' => sprintf(ngettext("%d photo", "%d photos", $taginfo['total']), $taginfo['total']))) . htmlspecialchars($taginfo['tag_name']) . '</a>' . ($hasEdit ? '<a href="#" onclick="removeTag(' . $tag_id . ');">' . Horde::img('delete-small.png', _("Remove Tag")) . '</a>' : '') . '</li>';
+        foreach ($tags as $taginfo) {
+            $tag_id = $taginfo['tag_id'];
+            $html .= '<li>' . $links[$tag_id]->link(array('title' => sprintf(ngettext("%d photo", "%d photos", $taginfo['count']), $taginfo['count']))) . htmlspecialchars($taginfo['tag_name']) . '</a>' . ($hasEdit ? '<a href="#" onclick="removeTag(' . $tag_id . ');">' . Horde::img('delete-small.png', _("Remove Tag")) . '</a>' : '') . '</li>';
         }
         $html .= '</ul>';
 
index 43abc55..b4ca043 100644 (file)
@@ -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 (file)
index 0000000..d46c562
--- /dev/null
@@ -0,0 +1,39 @@
+#!/usr/bin/env php
+<?php
+/**
+ * Script for migrating Ansel 1.x tags to the Content_Tagger system in Ansel 2.
+ *
+ * Warning: This script may take a LONG time, depending on the number of users
+ * and images.
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ */
+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');
+
index 9bee44d..0b2266b 100644 (file)
@@ -122,7 +122,7 @@ echo htmlspecialchars($this->getTitle(), ENT_COMPAT, $GLOBALS['registry']->getCh
    </td>
    <td width="20%" valign="top">
     <div id="anselWidgets">
-     <?php /* Tags if we are using related */ if ($conf['tags']['relatedtags']): ?>
+     <?php if ($conf['tags']['relatedtags']): ?>
       <div style="background-color:<?php echo $styleDef['background'] ?>;">
        <h2 class="header tagTitle"><?php echo _("Related Tags") ?></h2>
        <div id="tags"><?php echo $rtaghtml ?></div>
diff --git a/content/lib/Exception.php b/content/lib/Exception.php
new file mode 100644 (file)
index 0000000..834a7f5
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+class Content_Exception extends Horde_Exception_Prior {
+}
\ No newline at end of file
index 102b01e..d8ae4f4 100644 (file)
@@ -115,7 +115,7 @@ class Content_Tagger
 
         foreach ($this->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 <type> $tags
-     * @param <type> $create
-     * @return <type> \
+     *
+     * @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.