www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

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))();