diff --git a/CollectionTreePlugin.php b/CollectionTreePlugin.php index 38e8b96..11ee5c6 100644 --- a/CollectionTreePlugin.php +++ b/CollectionTreePlugin.php @@ -13,6 +13,9 @@ */ class CollectionTreePlugin extends Omeka_Plugin_AbstractPlugin { + /** + * @var array Hooks for the plugin. + */ protected $_hooks = array( 'install', 'uninstall', @@ -23,17 +26,35 @@ class CollectionTreePlugin extends Omeka_Plugin_AbstractPlugin 'before_save_collection', 'after_save_collection', 'after_delete_collection', - 'collection_browse_sql', + 'collections_browse_sql', + 'items_browse_sql', + 'admin_items_search', + 'public_items_search', 'admin_collections_show', 'public_collections_show', ); + /** + * @var array Filters for the plugin. + */ protected $_filters = array( 'admin_navigation_main', 'public_navigation_main', 'admin_collections_form_tabs', + 'items_browse_params', + 'collections_select_options', ); - + + /** + * @var array Options and their default values. + */ + protected $_options = array( + 'collection_tree_alpha_order' => 0, + 'collection_tree_browse_only_root' => 0, + 'collection_tree_show_subcollections' => 0, + 'collection_tree_search_descendant' => 0, + ); + /** * Install the plugin. * @@ -54,9 +75,9 @@ public function hookInstall() UNIQUE KEY `collection_id` (`collection_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;"; $this->_db->query($sql); - - set_option('collection_tree_alpha_order', '0'); - + + $this->_installOptions(); + // Save all collections in the collection_trees table. $collectionTable = $this->_db->getTable('Collection'); $collections = $this->_db->fetchAll("SELECT id FROM {$this->_db->Collection}"); @@ -77,8 +98,8 @@ public function hookUninstall() { $sql = "DROP TABLE IF EXISTS {$this->_db->CollectionTree}"; $this->_db->query($sql); - - delete_option('collection_tree_alpha_order'); + + $this->_uninstallOptions(); } /** @@ -122,35 +143,42 @@ public function hookUpgrade($args) } } } - + /** * Display the config form. */ - public function hookConfigForm() + public function hookConfigForm($args) { - echo get_view()->partial('plugins/collection-tree-config-form.php'); + $view = get_view(); + echo $view->partial('plugins/collection-tree-config-form.php'); } - + /** * Handle the config form. */ - public function hookConfig() + public function hookConfig($args) { - set_option('collection_tree_alpha_order', $_POST['collection_tree_alpha_order']); + $post = $args['post']; + foreach ($this->_options as $optionKey => $optionValue) { + if (isset($post[$optionKey])) { + set_option($optionKey, $post[$optionKey]); + } + } } - + public function hookBeforeSaveCollection($args) { - $collectionTree = $this->_db->getTable('CollectionTree')->findByCollectionId($args['record']->id); + $collection = $args['record']; + $collectionTree = $this->_db->getTable('CollectionTree')->findByCollectionId($collection->id); if (!$collectionTree) { return; } - + // Only validate the relationship during a form submission. if (isset($args['post']['collection_tree_parent_collection_id'])) { $collectionTree->parent_collection_id = $args['post']['collection_tree_parent_collection_id']; if (!$collectionTree->isValid()) { - $args['record']->addErrorsFrom($collectionTree); + $collection->addErrorsFrom($collectionTree); } } } @@ -160,11 +188,12 @@ public function hookBeforeSaveCollection($args) */ public function hookAfterSaveCollection($args) { - $collectionTree = $this->_db->getTable('CollectionTree')->findByCollectionId($args['record']->id); - + $collection = $args['record']; + $collectionTree = $this->_db->getTable('CollectionTree')->findByCollectionId($collection->id); + if (!$collectionTree) { $collectionTree = new CollectionTree; - $collectionTree->collection_id = $args['record']->id; + $collectionTree->collection_id = $collection->id; $collectionTree->parent_collection_id = 0; } @@ -189,16 +218,17 @@ public function hookAfterSaveCollection($args) */ public function hookAfterDeleteCollection($args) { + $collection = $args['record']; $collectionTreeTable = $this->_db->getTable('CollectionTree'); // Delete the relationship with the parent collection. - $collectionTree = $collectionTreeTable->findByCollectionId($args['record']->id); + $collectionTree = $collectionTreeTable->findByCollectionId($collection->id); if ($collectionTree) { $collectionTree->delete(); } // Move child collections to root level by deleting their relationships. - $collectionTrees = $collectionTreeTable->findByParentCollectionId($args['record']->id); + $collectionTrees = $collectionTreeTable->findByParentCollectionId($collection->id); foreach ($collectionTrees as $collectionTree) { $collectionTree->parent_collection_id = 0; $collectionTree->save(); @@ -206,20 +236,112 @@ public function hookAfterDeleteCollection($args) } /** - * Omit all child collections from the collection browse. + * Hook for collections browse: omit all child collections from the collection + * browse. */ - public function hookCollectionBrowseSql($args) + public function hookCollectionsBrowseSql($args) { if (!is_admin_theme()) { + if (!get_option('collection_tree_browse_only_root')) { + return; + } + $select = $args['select']; $sql = " - c.id NOT IN ( - SELECT ct.collection_id - FROM {$this->_db->CollectionTree} ct + collections.id NOT IN ( + SELECT collection_trees.collection_id + FROM {$this->_db->CollectionTree} collection_trees + WHERE collection_trees.parent_collection_id != 0 )"; - $args['select']->where($sql); + $select->where($sql); } } - + + /** + * Hook for items browse: search in collection's children and selected one. + * + * @param Omeka_Db_Select $select + * @param array $params + */ + public function hookItemsBrowseSql($args) + { + if (is_admin_theme()) { + return; + } + + $params = $args['params']; + if (empty($params['descendant_or_self'])) { + return; + } + + $collection = $params['descendant_or_self'] instanceof Collection + // Collection can be an object when not called from search form. + ? $params['descendant_or_self']->id + // Else this should be an integer. + : (integer) $params['descendant_or_self']; + + if (empty($collection)) { + return; + } + + $select = $args['select']; + + $collections = $this->_db->getTable('CollectionTree') + ->getDescendantOrSelfCollections($collection); + $collections = array_keys($collections); + + $select->joinInner( + array('collection_tree_collections' => $this->_db->Collection), + 'items.collection_id = collection_tree_collections.id', + array()); + + // There are descendants. + if (count($collections) > 1) { + $select->where('collection_tree_collections.id IN (?)', $collections); + } + // There is only the collection itself or no collection. + else { + $select->where('collection_tree_collections.id = ?', reset($collections)); + } + } + + /** + * Hook for admin advanced search. + * + * @return string HTML + */ + public function hookAdminItemsSearch($args) + { + echo $this->_itemsSearch($args); + } + + /** + * Hook for public advanced search. + * + * @return string HTML + */ + public function hookPublicItemsSearch($args) + { + echo $this->_itemsSearch($args); + } + + /** + * Append items search checkbox to the advanced search page. + * + * @return string HTML + */ + protected function _itemsSearch($args) + { + $view = $args['view']; + $html = '
'; + $html .= $view->formLabel('subcollections', __('Broaden to the sub-collections')); + $html .= '
'; + $html .= $view->formCheckbox('subcollections', null, + array('checked' => (bool) get_option('collection_tree_search_descendant'))); + $html .= '
'; + $html .= '
'; + return $html; + } + /** * Display the collection's parent collection and child collections. */ @@ -227,13 +349,13 @@ public function hookAdminCollectionsShow($args) { $this->_appendToCollectionsShow($args['collection']); } - + /** * Display the collection's parent collection and child collections. */ - public function hookPublicCollectionsShow() + public function hookPublicCollectionsShow($args) { - $this->_appendToCollectionsShow(get_current_record('collection')); + $this->_appendToCollectionsShow($args['collection']); } protected function _appendToCollectionsShow($collection) @@ -268,12 +390,13 @@ public function filterPublicNavigationMain($nav) */ public function filterAdminCollectionsFormTabs($tabs, $args) { + $collection = $args['collection']; $collectionTreeTable = $this->_db->getTable('CollectionTree'); $options = $collectionTreeTable->findPairsForSelectForm(); $options = array('0' => __('No parent collection')) + $options; - - $collectionTree = $collectionTreeTable->findByCollectionId($args['collection']->id); + + $collectionTree = $collectionTreeTable->findByCollectionId($collection->id); if ($collectionTree) { $parentCollectionId = $collectionTree->parent_collection_id; } else { @@ -285,4 +408,53 @@ public function filterAdminCollectionsFormTabs($tabs, $args) ); return $tabs; } + + + /** + * Filter items browse params to broaden the search to subcollections. + * + * @param array $params + * @return array + */ + public function filterItemsBrowseParams($params) + { + // Check if this is a direct query (not from advanced search). + if (!is_admin_theme() + && !isset($params['subcollections']) + && get_option('collection_tree_show_subcollections') + ) { + $params['subcollections'] = 1; + } + + if (!empty($params['subcollections'])) { + $collection = 0; + if (!empty($params['collection_id'])) { + $collection = $params['collection_id']; + $params['collection_id'] = ''; + } + if (!empty($params['collection'])) { + $collection = $params['collection']; + $params['collection'] = ''; + } + if ($collection) { + $params['descendant_or_self'] = $collection; + } + } + + return $params; + } + + + /** + * Manage search options for collections. + * + * @param array Search options for collections. + * @return array Filtered search options for collections. + */ + public function filterCollectionsSelectOptions($options) + { + $treeOptions = $this->_db->getTable('CollectionTree')->findPairsForSelectForm(); + // Keep only chosen collections, in case another filter removed some. + return array_intersect_key($treeOptions, $options); + } } diff --git a/README.md b/README.md new file mode 100644 index 0000000..1db8f1a --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +Collection Tree (plugin for Omeka) +================================== + + +Summary +------- + +This plugin for [Omeka] gives administrators the ability to create a +hierarchical tree of their collections. + + +Installation +------------ + +Uncompress files and rename plugin folder "CollectionTree". + +Then install it like any other Omeka plugin and follow the config instructions. + + +Warning +------- + +Use it at your own risk. + +It's always recommended to backup your files and database so you can roll back +if needed. + + +Troubleshooting +--------------- + +See online issues on [Collection Tree issues] page on GitHub. + + +License +------- + +This plugin is published under [GNU/GPL]. + +This program is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation; either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +details. + +You should have received a copy of the GNU General Public License along with +this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +Contact +------- + +Current maintainers: + +* Roy Rosenzweig Center for History and New Media + + +Copyright +--------- + +* Copyright Roy Rosenzweig Center for History and New Media, 2007-2013 +* Copyright Daniel Berthereau, 2014 (improvements) + + +[Omeka]: https://omeka.org +[Collection Tree]: https://github.com/Omeka/plugin-CollectionTree +[Collection Tree issues]: https://github.com/Omeka/plugin-CollectionTree/issues +[GNU/GPL]: https://www.gnu.org/licenses/gpl-3.0.html "GNU/GPL v3" +[Daniel-KM]: https://github.com/Daniel-KM "Daniel Berthereau" diff --git a/models/Table/CollectionTree.php b/models/Table/CollectionTree.php index 862c8d4..f52624f 100644 --- a/models/Table/CollectionTree.php +++ b/models/Table/CollectionTree.php @@ -44,33 +44,36 @@ class Table_CollectionTree extends Omeka_Db_Table */ public function fetchAssignableParentCollections($collectionId) { - $db = $this->getDb(); - // Must cast null collection ID to 0 to properly bind. $collectionId = (int) $collectionId; - $sql = " - SELECT c.*, ct.name - FROM {$db->Collection} c - LEFT JOIN {$db->CollectionTree} ct - ON c.id = ct.collection_id - WHERE c.id != ?"; + $table = $this->_db->getTable('Collection'); + $alias = $this->getTableAlias(); + $aliasCollection = $table->getTableAlias(); + + // Access rights to collections are automatically managed. + $select = $table->getSelect(); + $select->joinLeft( + array($alias => $this->getTableName()), + "$aliasCollection.id = $alias.collection_id", + array('name')); + $select->where("$aliasCollection.id != ?", $collectionId); // If not a new collection, cache descendant collection IDs and exclude // those collections from the result. if ($collectionId) { $unassignableCollectionIds = $this->getUnassignableCollectionIds(); if ($unassignableCollectionIds) { - $sql .= " AND c.id NOT IN (" . implode(', ', $unassignableCollectionIds) . ")"; + $select->where("$aliasCollection.id NOT IN (?)", $unassignableCollectionIds); } } // Order alphabetically if configured to do so. if (get_option('collection_tree_alpha_order')) { - $sql .= ' ORDER BY ct.name'; + $select->order("$alias.name ASC"); } - return $db->fetchAll($sql, array((int) $collectionId)); + return $this->fetchAssoc($select); } /** @@ -81,15 +84,16 @@ public function fetchAssignableParentCollections($collectionId) */ public function findByCollectionId($collectionId) { - $db = $this->getDb(); - - $sql = " - SELECT * - FROM {$db->CollectionTree} - WHERE collection_id = ?"; + // Cast to integer to prevent SQL injection. + $collectionId = (int) $collectionId; + $alias = $this->getTableAlias(); + $select = $this->getSelect(); + $select->where("$alias.collection_id = ?", $collectionId); + $select->limit(1); + $select->reset(Zend_Db_Select::ORDER); // Child collection IDs are unique, so only fetch one row. - return $this->fetchObject($sql, array($collectionId)); + return $this->fetchObject($select); } /** @@ -100,59 +104,40 @@ public function findByCollectionId($collectionId) */ public function findByParentCollectionId($parentCollectionId) { - $db = $this->getDb(); - - $sql = " - SELECT * - FROM {$db->CollectionTree} - WHERE parent_collection_id = ?"; - - return $this->fetchObjects($sql, array($parentCollectionId)); - } - - /** - * Cache collection data. - */ - public function cacheCollections() - { - $db = $this->getDb(); - $sql = " - SELECT c.*, ct.parent_collection_id, ct.name - FROM {$db->Collection} c - LEFT JOIN {$db->CollectionTree} ct - ON c.id = ct.collection_id"; - - // check whether the acl exists -- it doesn't within a background process - $acl = get_acl(); - // Cache only those collections to which the current user has access. - if ($acl && ! $acl->isAllowed(current_user(), 'Collections', 'showNotPublic')) { - $sql .= ' WHERE c.public = 1'; - } - - // Order alphabetically if configured to do so. - if (get_option('collection_tree_alpha_order')) { - $sql .= ' ORDER BY ct.name'; - } - - $this->_collections = $db->fetchAll($sql); + // Cast to integer to prevent SQL injection. + $parentCollectionId = (int) $parentCollectionId; + + $alias = $this->getTableAlias(); + $select = $this->getSelect(); + $select->where("$alias.parent_collection_id = ?", $parentCollectionId); + $select->reset(Zend_Db_Select::ORDER); + return $this->fetchObjects($select); } /** * Return the collection tree hierarchy as a one-dimensional array. * + * @param array $options (optional) Set of parameters for searching/ + * filtering results. * @param string $padding The string representation of the collection depth. * @return array */ - public function findPairsForSelectForm($padding = '-') + public function findPairsForSelectForm(array $options = array(), $padding = '-') { + if (isset($params['padding'])) { + $padding = $params['padding']; + } else { + $padding = '-'; + } + $options = array(); - foreach ($this->getRootCollections() as $rootCollection) { + foreach ($this->getRootCollections() as $rootCollectionId => $rootCollection) { - $options[$rootCollection['id']] = $rootCollection['name'] ? $rootCollection['name'] : __('[Untitled]'); + $options[$rootCollectionId] = $rootCollection['name'] ? $rootCollection['name'] : __('[Untitled]'); $this->_resetCache(); - $this->getDescendantTree($rootCollection['id'], true); + $this->getDescendantTree($rootCollectionId, true); foreach ($this->_cache as $collectionId => $collectionDepth) { $collection = $this->getCollection($collectionId); $options[$collectionId] = str_repeat($padding, $collectionDepth) . ' '; @@ -241,7 +226,7 @@ public function getDescendantTree($collectionId, $cacheDescendantInfo = false, $ $collectionDepth++; // Iterate the child collections. - $descendantTree = $this->getChildCollections($collectionId); + $descendantTree = array_values($this->getChildCollections($collectionId)); for ($i = 0; $i < count($descendantTree); $i++) { if ($cacheDescendantInfo) { @@ -249,9 +234,10 @@ public function getDescendantTree($collectionId, $cacheDescendantInfo = false, $ } // Recurse the child collections, getting their children. - $children = $this->getDescendantTree($descendantTree[$i]['id'], - $cacheDescendantInfo, - $collectionDepth); + $children = $this->getDescendantTree( + $descendantTree[$i]['id'], + $cacheDescendantInfo, + $collectionDepth); // Assign the child collections to the descendant tree. if ($children) { @@ -272,57 +258,62 @@ public function getDescendantTree($collectionId, $cacheDescendantInfo = false, $ */ public function getCollection($collectionId) { - // Cache collections in not already. - if (!$this->_collections) { - $this->cacheCollections(); - } - - foreach ($this->_collections as $collection) { - if ($collectionId == $collection['id']) { - return $collection; - } - } - return false; + $collections = $this->_getCollections(); + return isset($collections[$collectionId]) ? $collections[$collectionId] : false; } /** * Get the child collections of the specified collection. * * @param int $collectionId - * @return array + * @return array Associative array of collections, by id. */ public function getChildCollections($collectionId) { - // Cache collections if not already. - if (!$this->_collections) { - $this->cacheCollections(); - } - $childCollections = array(); - foreach ($this->_collections as $collection) { + $collections = $this->_getCollections(); + foreach ($collections as $collection) { if ($collectionId == $collection['parent_collection_id']) { - $childCollections[] = $collection; + $childCollections[$collection['id']] = $collection; } } return $childCollections; } /** - * Get all root collections, i.e. those without parent collections. + * Get the list of descendant collections and the selected one. * - * @return array + * @param int $collectionId + * @return array Associative array of collections. */ - public function getRootCollections() + public function getDescendantOrSelfCollections($collectionId) { - // Cache collections if not already. - if (!$this->_collections) { - $this->cacheCollections(); + $collections = array(); + + $rootCollection = $this->getCollection($collectionId); + if ($rootCollection) { + $this->_resetCache(); + $this->getDescendantTree($collectionId, true); + $collections[$collectionId] = $rootCollection; + $collections += array_intersect_key($this->_getCollections(), $this->_cache); + $this->_resetCache(); } + return $collections; + } + + /** + * Get all root collections, i.e. those without parent collections. + * + * @return array Associative array of root collections, by id. + */ + public function getRootCollections() + { $rootCollections = array(); - foreach ($this->_collections as $collection) { + $collections = $this->_getCollections(); + foreach ($collections as $collection) { if (!$collection['parent_collection_id']) { - $rootCollections[] = $collection; + $rootCollections[$collection['id']] = $collection; } } return $rootCollections; @@ -346,6 +337,34 @@ public function getUnassignableCollectionIds($collectionId) return $unassignableCollections; } + /** + * Cache collection data with name and parent id in an associative array. + */ + protected function _getCollections() + { + if (is_null($this->_collections)) { + $table = $this->_db->getTable('Collection'); + $alias = $this->getTableAlias(); + $aliasCollection = $table->getTableAlias(); + + // Access rights to collections are automatically managed. + $select = $table->getSelect(); + $select->joinLeft( + array($alias => $this->getTableName()), + "$aliasCollection.id = $alias.collection_id", + array('parent_collection_id', 'name')); + + // Order alphabetically if configured to do so. + if (get_option('collection_tree_alpha_order')) { + $select->order("$alias.name ASC"); + } + + $this->_collections = $this->fetchAssoc($select); + } + + return $this->_collections; + } + /** * Reset the cache property. */ diff --git a/plugin.ini b/plugin.ini index 270a54b..a45ec7b 100644 --- a/plugin.ini +++ b/plugin.ini @@ -3,8 +3,8 @@ name="Collection Tree" author="Roy Rosenzweig Center for History and New Media" description="Gives administrators the ability to create a hierarchical tree of their collections." license="GPLv3" -link="http://omeka.org/codex/Plugins/CollectionTree_2.0" -support_link="http://omeka.org/forums/forum/plugins" +link="https://omeka.org/codex/Plugins/CollectionTree_2.0" +support_link="https://omeka.org/forums/forum/plugins" version="2.0.2" omeka_minimum_version="2.0" omeka_target_version="2.0" diff --git a/views/admin/plugins/collection-tree-config-form.php b/views/admin/plugins/collection-tree-config-form.php index 772c4bc..710f24e 100644 --- a/views/admin/plugins/collection-tree-config-form.php +++ b/views/admin/plugins/collection-tree-config-form.php @@ -1,11 +1,50 @@
-
- +
+ formLabel('collection_tree_alpha_order', __('Order alphabetically')); ?>
-

- formCheckbox('collection_tree_alpha_order', null, - array('checked' => (bool) get_option('collection_tree_alpha_order'))); ?> +

+ formCheckbox('collection_tree_alpha_order', null, + array('checked' => (bool) get_option('collection_tree_alpha_order'))); ?> +
+
+
+
+ formLabel('collection_tree_browse_only_root', __('Browse collections')); ?> +
+
+

+ formCheckbox('collection_tree_browse_only_root', null, + array('checked' => (bool) get_option('collection_tree_browse_only_root'))); ?> +
+
+
+
+ formLabel('collection_tree_show_subcollections', __('Show collection')); ?> +
+
+

+ formCheckbox('collection_tree_show_subcollections', null, + array('checked' => (bool) get_option('collection_tree_show_subcollections'))); ?> +
+
+
+
+ formLabel('collection_tree_search_descendant', __('Advanced search')); ?> +
+
+

+ formCheckbox('collection_tree_search_descendant', null, + array('checked' => (bool) get_option('collection_tree_search_descendant'))); ?>
diff --git a/views/helpers/CollectionTreeList.php b/views/helpers/CollectionTreeList.php index 160f355..729f993 100644 --- a/views/helpers/CollectionTreeList.php +++ b/views/helpers/CollectionTreeList.php @@ -1,7 +1,7 @@ getTable('Collection'); $html = '