collections.js (9584B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2009 Center for History and New Media 5 George Mason University, Fairfax, Virginia, USA 6 http://zotero.org 7 8 This file is part of Zotero. 9 10 Zotero is free software: you can redistribute it and/or modify 11 it under the terms of the GNU Affero General Public License as published by 12 the Free Software Foundation, either version 3 of the License, or 13 (at your option) any later version. 14 15 Zotero is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 GNU Affero General Public License for more details. 19 20 You should have received a copy of the GNU Affero General Public License 21 along with Zotero. If not, see <http://www.gnu.org/licenses/>. 22 23 ***** END LICENSE BLOCK ***** 24 */ 25 26 27 /* 28 * Primary interface for accessing Zotero collection 29 */ 30 Zotero.Collections = function() { 31 this.constructor = null; 32 33 this._ZDO_object = 'collection'; 34 35 this._primaryDataSQLParts = { 36 collectionID: "O.collectionID", 37 name: "O.collectionName AS name", 38 libraryID: "O.libraryID", 39 key: "O.key", 40 version: "O.version", 41 synced: "O.synced", 42 43 parentID: "O.parentCollectionID AS parentID", 44 parentKey: "CP.key AS parentKey", 45 46 hasChildCollections: "(SELECT COUNT(*) FROM collections WHERE " 47 + "parentCollectionID=O.collectionID) != 0 AS hasChildCollections", 48 hasChildItems: "(SELECT COUNT(*) FROM collectionItems WHERE " 49 + "collectionID=O.collectionID) != 0 AS hasChildItems" 50 }; 51 52 53 this._primaryDataSQLFrom = "FROM collections O " 54 + "LEFT JOIN collections CP ON (O.parentCollectionID=CP.collectionID)"; 55 56 this._relationsTable = "collectionRelations"; 57 58 /** 59 * Get collections within a library 60 * 61 * Either libraryID or parentID must be provided 62 * 63 * @param {Integer} libraryID 64 * @param {Boolean} [recursive=false] 65 * @return {Zotero.Collection[]} 66 */ 67 this.getByLibrary = function (libraryID, recursive) { 68 return _getByContainer(libraryID, null, recursive); 69 } 70 71 72 /** 73 * Get collections that are subcollection of a given collection 74 * 75 * @param {Integer} parentCollectionID 76 * @param {Boolean} [recursive=false] 77 * @return {Zotero.Collection[]} 78 */ 79 this.getByParent = function (parentCollectionID, recursive) { 80 return _getByContainer(null, parentCollectionID, recursive); 81 } 82 83 84 var _getByContainer = function (libraryID, parentID, recursive) { 85 let children = []; 86 87 if (parentID) { 88 let parent = Zotero.Collections.get(parentID); 89 children = parent.getChildCollections(); 90 } else if (libraryID) { 91 for (let id in this._objectCache) { 92 let c = this._objectCache[id]; 93 if (c.libraryID == libraryID && !c.parentKey) { 94 c.level = 0; 95 children.push(c); 96 } 97 } 98 } else { 99 throw new Error("Either library ID or parent collection ID must be provided"); 100 } 101 102 if (!children.length) { 103 return children; 104 } 105 106 // Do proper collation sort 107 children.sort((a, b) => Zotero.localeCompare(a.name, b.name)); 108 109 if (!recursive) return children; 110 111 let toReturn = []; 112 for (var i=0, len=children.length; i<len; i++) { 113 var obj = children[i]; 114 toReturn.push(obj); 115 116 var descendants = obj.getDescendents(false, 'collection'); 117 for (let d of descendants) { 118 var obj2 = this.get(d.id); 119 if (!obj2) { 120 throw new Error('Collection ' + d.id + ' not found'); 121 } 122 123 // TODO: This is a quick hack so that we can indent subcollections 124 // in the search dialog -- ideally collections would have a 125 // getLevel() method, but there's no particularly quick way 126 // of calculating that without either storing it in the DB or 127 // changing the schema to Modified Preorder Tree Traversal, 128 // and I don't know if we'll actually need it anywhere else. 129 obj2.level = d.level; 130 131 toReturn.push(obj2); 132 } 133 } 134 135 return toReturn; 136 }.bind(this); 137 138 139 this.getCollectionsContainingItems = function (itemIDs, asIDs) { 140 var sql = "SELECT collectionID FROM collections WHERE "; 141 var sqlParams = []; 142 for (let id of itemIDs) { 143 sql += "collectionID IN (SELECT collectionID FROM collectionItems " 144 + "WHERE itemID=?) AND " 145 sqlParams.push(id); 146 } 147 sql = sql.substring(0, sql.length - 5); 148 return Zotero.DB.columnQueryAsync(sql, sqlParams) 149 .then(collectionIDs => { 150 return asIDs ? collectionIDs : this.get(collectionIDs); 151 }); 152 153 } 154 155 156 /** 157 * Sort an array of collectionIDs from top-level to deepest 158 * 159 * Order within each level is undefined. 160 * 161 * This is used to sort higher-level collections first in upload JSON, since otherwise the API 162 * would reject lower-level collections for having missing parents. 163 */ 164 this.sortByLevel = function (ids) { 165 let levels = {}; 166 167 // Get objects from ids 168 let objs = {}; 169 ids.forEach(id => objs[id] = Zotero.Collections.get(id)); 170 171 // Get top-level collections 172 let top = ids.filter(id => !objs[id].parentID); 173 levels["0"] = top.slice(); 174 ids = Zotero.Utilities.arrayDiff(ids, top); 175 176 // For each collection in list, walk up its parent tree. If a parent is present in the 177 // list of ids, add it to the appropriate level bucket and remove it. 178 while (ids.length) { 179 let tree = [ids[0]]; 180 let keep = [ids[0]]; 181 let id = ids.shift(); 182 while (true) { 183 let c = Zotero.Collections.get(id); 184 let parentID = c.parentID; 185 if (!parentID) { 186 break; 187 } 188 tree.push(parentID); 189 // If parent is in list, remove it 190 let pos = ids.indexOf(parentID); 191 if (pos != -1) { 192 keep.push(parentID); 193 ids.splice(pos, 1); 194 } 195 id = parentID; 196 } 197 let level = tree.length - 1; 198 for (let i = 0; i < tree.length; i++) { 199 let currentLevel = level - i; 200 for (let j = 0; j < keep.length; j++) { 201 if (tree[i] != keep[j]) continue; 202 203 if (!levels[currentLevel]) { 204 levels[currentLevel] = []; 205 } 206 levels[currentLevel].push(keep[j]); 207 } 208 } 209 } 210 211 var orderedIDs = []; 212 for (let level in levels) { 213 orderedIDs = orderedIDs.concat(levels[level]); 214 } 215 return orderedIDs; 216 }; 217 218 219 this._loadChildCollections = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { 220 var sql = "SELECT C1.collectionID, C2.collectionID AS childCollectionID " 221 + "FROM collections C1 LEFT JOIN collections C2 ON (C1.collectionID=C2.parentCollectionID) " 222 + "WHERE C1.libraryID=?" 223 + (ids.length ? " AND C1.collectionID IN (" + ids.map(id => parseInt(id)).join(", ") + ")" : ""); 224 var params = [libraryID]; 225 var lastID; 226 var rows = []; 227 var setRows = function (collectionID, rows) { 228 var collection = this._objectCache[collectionID]; 229 if (!collection) { 230 throw new Error("Collection " + collectionID + " not found"); 231 } 232 233 collection._childCollections = new Set(rows); 234 collection._loaded.childCollections = true; 235 collection._clearChanged('childCollections'); 236 }.bind(this); 237 238 yield Zotero.DB.queryAsync( 239 sql, 240 params, 241 { 242 noCache: ids.length != 1, 243 onRow: function (row) { 244 let collectionID = row.getResultByIndex(0); 245 246 if (lastID && collectionID !== lastID) { 247 setRows(lastID, rows); 248 rows = []; 249 } 250 251 lastID = collectionID; 252 253 let childCollectionID = row.getResultByIndex(1); 254 // No child collections 255 if (childCollectionID === null) { 256 return; 257 } 258 rows.push(childCollectionID); 259 } 260 } 261 ); 262 if (lastID) { 263 setRows(lastID, rows); 264 } 265 }); 266 267 268 this._loadChildItems = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { 269 var sql = "SELECT collectionID, itemID FROM collections " 270 + "LEFT JOIN collectionItems USING (collectionID) " 271 + "WHERE libraryID=?" + idSQL; 272 var params = [libraryID]; 273 var lastID; 274 var rows = []; 275 var setRows = function (collectionID, rows) { 276 var collection = this._objectCache[collectionID]; 277 if (!collection) { 278 throw new Error("Collection " + collectionID + " not found"); 279 } 280 281 collection._childItems = new Set(rows); 282 collection._loaded.childItems = true; 283 collection._clearChanged('childItems'); 284 }.bind(this); 285 286 yield Zotero.DB.queryAsync( 287 sql, 288 params, 289 { 290 noCache: ids.length != 1, 291 onRow: function (row) { 292 let collectionID = row.getResultByIndex(0); 293 294 if (lastID && collectionID !== lastID) { 295 setRows(lastID, rows); 296 rows = []; 297 } 298 299 lastID = collectionID; 300 301 let itemID = row.getResultByIndex(1); 302 // No child items 303 if (itemID === null) { 304 return; 305 } 306 rows.push(itemID); 307 } 308 } 309 ); 310 if (lastID) { 311 setRows(lastID, rows); 312 } 313 }); 314 315 316 this.registerChildCollection = function (collectionID, childCollectionID) { 317 if (this._objectCache[collectionID]) { 318 this._objectCache[collectionID]._registerChildCollection(childCollectionID); 319 } 320 } 321 322 323 this.unregisterChildCollection = function (collectionID, childCollectionID) { 324 if (this._objectCache[collectionID]) { 325 this._objectCache[collectionID]._unregisterChildCollection(childCollectionID); 326 } 327 } 328 329 330 this.registerChildItem = function (collectionID, itemID) { 331 if (this._objectCache[collectionID]) { 332 this._objectCache[collectionID]._registerChildItem(itemID); 333 } 334 } 335 336 337 this.unregisterChildItem = function (collectionID, itemID) { 338 if (this._objectCache[collectionID]) { 339 this._objectCache[collectionID]._unregisterChildItem(itemID); 340 } 341 } 342 343 Zotero.DataObjects.call(this); 344 345 return this; 346 }.bind(Object.create(Zotero.DataObjects.prototype))();