items.js (44951B)
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 items 29 */ 30 Zotero.Items = function() { 31 this.constructor = null; 32 33 this._ZDO_object = 'item'; 34 35 // This needs to wait until all Zotero components are loaded to initialize, 36 // but otherwise it can be just a simple property 37 Zotero.defineProperty(this, "_primaryDataSQLParts", { 38 get: function () { 39 return { 40 itemID: "O.itemID", 41 itemTypeID: "O.itemTypeID", 42 dateAdded: "O.dateAdded", 43 dateModified: "O.dateModified", 44 libraryID: "O.libraryID", 45 key: "O.key", 46 version: "O.version", 47 synced: "O.synced", 48 49 firstCreator: _getFirstCreatorSQL(), 50 sortCreator: _getSortCreatorSQL(), 51 52 deleted: "DI.itemID IS NOT NULL AS deleted", 53 inPublications: "PI.itemID IS NOT NULL AS inPublications", 54 55 parentID: "(CASE O.itemTypeID WHEN 14 THEN IAP.itemID WHEN 1 THEN INoP.itemID END) AS parentID", 56 parentKey: "(CASE O.itemTypeID WHEN 14 THEN IAP.key WHEN 1 THEN INoP.key END) AS parentKey", 57 58 attachmentCharset: "CS.charset AS attachmentCharset", 59 attachmentLinkMode: "IA.linkMode AS attachmentLinkMode", 60 attachmentContentType: "IA.contentType AS attachmentContentType", 61 attachmentPath: "IA.path AS attachmentPath", 62 attachmentSyncState: "IA.syncState AS attachmentSyncState", 63 attachmentSyncedModificationTime: "IA.storageModTime AS attachmentSyncedModificationTime", 64 attachmentSyncedHash: "IA.storageHash AS attachmentSyncedHash" 65 }; 66 } 67 }, {lazy: true}); 68 69 70 this._primaryDataSQLFrom = "FROM items O " 71 + "LEFT JOIN itemAttachments IA USING (itemID) " 72 + "LEFT JOIN items IAP ON (IA.parentItemID=IAP.itemID) " 73 + "LEFT JOIN itemNotes INo ON (O.itemID=INo.itemID) " 74 + "LEFT JOIN items INoP ON (INo.parentItemID=INoP.itemID) " 75 + "LEFT JOIN deletedItems DI ON (O.itemID=DI.itemID) " 76 + "LEFT JOIN publicationsItems PI ON (O.itemID=PI.itemID) " 77 + "LEFT JOIN charsets CS ON (IA.charsetID=CS.charsetID)"; 78 79 this._relationsTable = "itemRelations"; 80 81 82 /** 83 * @param {Integer} libraryID 84 * @return {Promise<Boolean>} - True if library has items in trash, false otherwise 85 */ 86 this.hasDeleted = Zotero.Promise.coroutine(function* (libraryID) { 87 var sql = "SELECT COUNT(*) > 0 FROM items JOIN deletedItems USING (itemID) WHERE libraryID=?"; 88 return !!(yield Zotero.DB.valueQueryAsync(sql, [libraryID])); 89 }); 90 91 92 /** 93 * Return items marked as deleted 94 * 95 * @param {Integer} libraryID - Library to search 96 * @param {Boolean} [asIDs] - Return itemIDs instead of Zotero.Item objects 97 * @return {Promise<Zotero.Item[]|Integer[]>} 98 */ 99 this.getDeleted = Zotero.Promise.coroutine(function* (libraryID, asIDs, days) { 100 var sql = "SELECT itemID FROM items JOIN deletedItems USING (itemID) " 101 + "WHERE libraryID=?"; 102 if (days) { 103 sql += " AND dateDeleted<=DATE('NOW', '-" + parseInt(days) + " DAYS')"; 104 } 105 var ids = yield Zotero.DB.columnQueryAsync(sql, [libraryID]); 106 if (!ids.length) { 107 return []; 108 } 109 if (asIDs) { 110 return ids; 111 } 112 return this.getAsync(ids); 113 }); 114 115 116 /** 117 * Returns all items in a given library 118 * 119 * @param {Integer} libraryID 120 * @param {Boolean} [onlyTopLevel=false] If true, don't include child items 121 * @param {Boolean} [includeDeleted=false] If true, include deleted items 122 * @param {Boolean} [asIDs=false] If true, resolves only with IDs 123 * @return {Promise<Array<Zotero.Item|Integer>>} 124 */ 125 this.getAll = Zotero.Promise.coroutine(function* (libraryID, onlyTopLevel, includeDeleted, asIDs=false) { 126 var sql = 'SELECT A.itemID FROM items A'; 127 if (onlyTopLevel) { 128 sql += ' LEFT JOIN itemNotes B USING (itemID) ' 129 + 'LEFT JOIN itemAttachments C ON (C.itemID=A.itemID) ' 130 + 'WHERE B.parentItemID IS NULL AND C.parentItemID IS NULL'; 131 } 132 else { 133 sql += " WHERE 1"; 134 } 135 if (!includeDeleted) { 136 sql += " AND A.itemID NOT IN (SELECT itemID FROM deletedItems)"; 137 } 138 sql += " AND libraryID=?"; 139 var ids = yield Zotero.DB.columnQueryAsync(sql, libraryID); 140 if (asIDs) { 141 return ids; 142 } 143 return this.getAsync(ids); 144 }); 145 146 147 /** 148 * Return item data in web API format 149 * 150 * var data = Zotero.Items.getAPIData(0, 'collections/NF3GJ38A/items'); 151 * 152 * @param {Number} libraryID 153 * @param {String} [apiPath='items'] - Web API style 154 * @return {Promise<String>}. 155 */ 156 this.getAPIData = Zotero.Promise.coroutine(function* (libraryID, apiPath) { 157 var gen = this.getAPIDataGenerator(...arguments); 158 var data = ""; 159 while (true) { 160 var result = gen.next(); 161 if (result.done) { 162 break; 163 } 164 var val = yield result.value; 165 if (typeof val == 'string') { 166 data += val; 167 } 168 else if (val === undefined) { 169 continue; 170 } 171 else { 172 throw new Error("Invalid return value from generator"); 173 } 174 } 175 return data; 176 }); 177 178 179 /** 180 * Zotero.Utilities.Internal.getAsyncInputStream-compatible generator that yields item data 181 * in web API format as strings 182 * 183 * @param {Object} params - Request parameters from Zotero.API.parsePath() 184 */ 185 this.apiDataGenerator = function* (params) { 186 Zotero.debug(params); 187 var s = new Zotero.Search; 188 s.addCondition('libraryID', 'is', params.libraryID); 189 if (params.scopeObject == 'collections') { 190 s.addCondition('collection', 'is', params.scopeObjectKey); 191 } 192 s.addCondition('title', 'contains', 'test'); 193 var ids = yield s.search(); 194 195 yield '[\n'; 196 197 for (let i=0; i<ids.length; i++) { 198 let prefix = i > 0 ? ',\n' : ''; 199 let item = yield this.getAsync(ids[i], { noCache: true }); 200 var json = item.toResponseJSON(); 201 yield prefix + JSON.stringify(json, null, 4); 202 } 203 204 yield '\n]'; 205 }; 206 207 208 // 209 // Bulk data loading functions 210 // 211 // These are called by Zotero.DataObjects.prototype._loadDataType(). 212 // 213 this._loadItemData = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { 214 var missingItems = {}; 215 var itemFieldsCached = {}; 216 217 var sql = "SELECT itemID, fieldID, value FROM items " 218 + "JOIN itemData USING (itemID) " 219 + "JOIN itemDataValues USING (valueID) WHERE libraryID=? AND itemTypeID!=?" + idSQL; 220 var params = [libraryID, Zotero.ItemTypes.getID('note')]; 221 yield Zotero.DB.queryAsync( 222 sql, 223 params, 224 { 225 noCache: ids.length != 1, 226 onRow: function (row) { 227 let itemID = row.getResultByIndex(0); 228 let fieldID = row.getResultByIndex(1); 229 let value = row.getResultByIndex(2); 230 231 //Zotero.debug('Setting field ' + fieldID + ' for item ' + itemID); 232 if (this._objectCache[itemID]) { 233 if (value === null) { 234 value = false; 235 } 236 this._objectCache[itemID].setField(fieldID, value, true); 237 } 238 else { 239 if (!missingItems[itemID]) { 240 missingItems[itemID] = true; 241 Zotero.logError("itemData row references nonexistent item " + itemID); 242 } 243 } 244 if (!itemFieldsCached[itemID]) { 245 itemFieldsCached[itemID] = {}; 246 } 247 itemFieldsCached[itemID][fieldID] = true; 248 }.bind(this) 249 } 250 ); 251 252 var sql = "SELECT itemID FROM items WHERE libraryID=?" + idSQL; 253 var params = [libraryID]; 254 var allItemIDs = []; 255 yield Zotero.DB.queryAsync( 256 sql, 257 params, 258 { 259 noCache: ids.length != 1, 260 onRow: function (row) { 261 let itemID = row.getResultByIndex(0); 262 let item = this._objectCache[itemID]; 263 264 // Set nonexistent fields in the cache list to false (instead of null) 265 let fieldIDs = Zotero.ItemFields.getItemTypeFields(item.itemTypeID); 266 for (let j=0; j<fieldIDs.length; j++) { 267 let fieldID = fieldIDs[j]; 268 if (!itemFieldsCached[itemID] || !itemFieldsCached[itemID][fieldID]) { 269 //Zotero.debug('Setting field ' + fieldID + ' to false for item ' + itemID); 270 item.setField(fieldID, false, true); 271 } 272 } 273 274 allItemIDs.push(itemID); 275 }.bind(this) 276 } 277 ); 278 279 280 var titleFieldID = Zotero.ItemFields.getID('title'); 281 282 // Note titles 283 var sql = "SELECT itemID, title FROM items JOIN itemNotes USING (itemID) " 284 + "WHERE libraryID=? AND itemID NOT IN (SELECT itemID FROM itemAttachments)" + idSQL; 285 var params = [libraryID]; 286 287 yield Zotero.DB.queryAsync( 288 sql, 289 params, 290 { 291 onRow: function (row) { 292 let itemID = row.getResultByIndex(0); 293 let title = row.getResultByIndex(1); 294 295 //Zotero.debug('Setting title for note ' + row.itemID); 296 if (this._objectCache[itemID]) { 297 this._objectCache[itemID].setField(titleFieldID, title, true); 298 } 299 else { 300 if (!missingItems[itemID]) { 301 missingItems[itemID] = true; 302 Zotero.logError("itemData row references nonexistent item " + itemID); 303 } 304 } 305 }.bind(this) 306 } 307 ); 308 309 for (let i=0; i<allItemIDs.length; i++) { 310 let itemID = allItemIDs[i]; 311 let item = this._objectCache[itemID]; 312 313 // Mark as loaded 314 item._loaded.itemData = true; 315 item._clearChanged('itemData'); 316 317 // Display titles 318 try { 319 item.updateDisplayTitle() 320 } 321 catch (e) { 322 // A few item types need creators to be loaded. Instead of making 323 // updateDisplayTitle() async and loading conditionally, just catch the error 324 // and load on demand 325 if (e instanceof Zotero.Exception.UnloadedDataException) { 326 yield item.loadDataType('creators'); 327 item.updateDisplayTitle() 328 } 329 else { 330 throw e; 331 } 332 } 333 } 334 }); 335 336 337 this._loadCreators = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { 338 var sql = 'SELECT itemID, creatorID, creatorTypeID, orderIndex ' 339 + 'FROM items LEFT JOIN itemCreators USING (itemID) ' 340 + 'WHERE libraryID=?' + idSQL + " ORDER BY itemID, orderIndex"; 341 var params = [libraryID]; 342 var rows = yield Zotero.DB.queryAsync(sql, params); 343 344 // Mark creator indexes above the number of creators as changed, 345 // so that they're cleared if the item is saved 346 var fixIncorrectIndexes = function (item, numCreators, maxOrderIndex) { 347 Zotero.debug("Fixing incorrect creator indexes for item " + item.libraryKey 348 + " (" + numCreators + ", " + maxOrderIndex + ")", 2); 349 var i = numCreators; 350 if (!item._changed.creators) { 351 item._changed.creators = {}; 352 } 353 while (i <= maxOrderIndex) { 354 item._changed.creators[i] = true; 355 i++; 356 } 357 }; 358 359 var lastItemID; 360 var item; 361 var index = 0; 362 var maxOrderIndex = -1; 363 for (let i = 0; i < rows.length; i++) { 364 let row = rows[i]; 365 let itemID = row.itemID; 366 367 if (itemID != lastItemID) { 368 if (!this._objectCache[itemID]) { 369 throw new Error("Item " + itemID + " not loaded"); 370 } 371 item = this._objectCache[itemID]; 372 373 item._creators = []; 374 item._creatorIDs = []; 375 item._loaded.creators = true; 376 item._clearChanged('creators'); 377 378 if (!row.creatorID) { 379 lastItemID = row.itemID; 380 continue; 381 } 382 383 if (index <= maxOrderIndex) { 384 fixIncorrectIndexes(item, index, maxOrderIndex); 385 } 386 387 index = 0; 388 maxOrderIndex = -1; 389 } 390 391 lastItemID = row.itemID; 392 393 if (row.orderIndex > maxOrderIndex) { 394 maxOrderIndex = row.orderIndex; 395 } 396 397 let creatorData = Zotero.Creators.get(row.creatorID); 398 creatorData.creatorTypeID = row.creatorTypeID; 399 item._creators[index] = creatorData; 400 item._creatorIDs[index] = row.creatorID; 401 index++; 402 } 403 404 if (index <= maxOrderIndex) { 405 fixIncorrectIndexes(item, index, maxOrderIndex); 406 } 407 }); 408 409 410 this._loadNotes = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { 411 var notesToUpdate = []; 412 413 var sql = "SELECT itemID, note FROM items " 414 + "JOIN itemNotes USING (itemID) " 415 + "WHERE libraryID=?" + idSQL; 416 var params = [libraryID]; 417 yield Zotero.DB.queryAsync( 418 sql, 419 params, 420 { 421 noCache: ids.length != 1, 422 onRow: function (row) { 423 let itemID = row.getResultByIndex(0); 424 let item = this._objectCache[itemID]; 425 if (!item) { 426 throw new Error("Item " + itemID + " not found"); 427 } 428 let note = row.getResultByIndex(1); 429 430 // Convert non-HTML notes on-the-fly 431 if (note !== "") { 432 if (typeof note == 'number') { 433 note = '' + note; 434 } 435 if (typeof note == 'string') { 436 if (!note.substr(0, 36).match(/^<div class="zotero-note znv[0-9]+">/)) { 437 note = Zotero.Utilities.htmlSpecialChars(note); 438 note = Zotero.Notes.notePrefix + '<p>' 439 + note.replace(/\n/g, '</p><p>') 440 .replace(/\t/g, ' ') 441 .replace(/ /g, ' ') 442 + '</p>' + Zotero.Notes.noteSuffix; 443 note = note.replace(/<p>\s*<\/p>/g, '<p> </p>'); 444 notesToUpdate.push([item.id, note]); 445 } 446 447 // Don't include <div> wrapper when returning value 448 let startLen = note.substr(0, 36).match(/^<div class="zotero-note znv[0-9]+">/)[0].length; 449 let endLen = 6; // "</div>".length 450 note = note.substr(startLen, note.length - startLen - endLen); 451 } 452 // Clear null notes 453 else { 454 note = ''; 455 notesToUpdate.push([item.id, '']); 456 } 457 } 458 459 item._noteText = note ? note : ''; 460 item._loaded.note = true; 461 item._clearChanged('note'); 462 }.bind(this) 463 } 464 ); 465 466 if (notesToUpdate.length) { 467 yield Zotero.DB.executeTransaction(function* () { 468 for (let i = 0; i < notesToUpdate.length; i++) { 469 let row = notesToUpdate[i]; 470 let sql = "UPDATE itemNotes SET note=? WHERE itemID=?"; 471 yield Zotero.DB.queryAsync(sql, [row[1], row[0]]); 472 } 473 }.bind(this)); 474 } 475 476 // Mark notes and attachments without notes as loaded 477 sql = "SELECT itemID FROM items WHERE libraryID=?" + idSQL 478 + " AND itemTypeID IN (?, ?) AND itemID NOT IN (SELECT itemID FROM itemNotes)"; 479 params = [libraryID, Zotero.ItemTypes.getID('note'), Zotero.ItemTypes.getID('attachment')]; 480 yield Zotero.DB.queryAsync( 481 sql, 482 params, 483 { 484 noCache: ids.length != 1, 485 onRow: function (row) { 486 let itemID = row.getResultByIndex(0); 487 let item = this._objectCache[itemID]; 488 if (!item) { 489 throw new Error("Item " + itemID + " not loaded"); 490 } 491 492 item._noteText = ''; 493 item._loaded.note = true; 494 item._clearChanged('note'); 495 }.bind(this) 496 } 497 ); 498 }); 499 500 501 this._loadChildItems = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { 502 var params = [libraryID]; 503 var rows = []; 504 var onRow = function (row, setFunc) { 505 var itemID = row.getResultByIndex(0); 506 507 // If we've finished a set of rows for an item, process them 508 if (lastItemID && itemID !== lastItemID) { 509 setFunc(lastItemID, rows); 510 rows = []; 511 } 512 513 lastItemID = itemID; 514 rows.push({ 515 itemID: row.getResultByIndex(1), 516 title: row.getResultByIndex(2), 517 trashed: row.getResultByIndex(3) 518 }); 519 }; 520 521 var sql = "SELECT parentItemID, A.itemID, value AS title, " 522 + "CASE WHEN DI.itemID IS NULL THEN 0 ELSE 1 END AS trashed " 523 + "FROM itemAttachments A " 524 + "JOIN items I ON (A.parentItemID=I.itemID) " 525 + "LEFT JOIN itemData ID ON (fieldID=110 AND A.itemID=ID.itemID) " 526 + "LEFT JOIN itemDataValues IDV USING (valueID) " 527 + "LEFT JOIN deletedItems DI USING (itemID) " 528 + "WHERE libraryID=?" 529 + (ids.length ? " AND parentItemID IN (" + ids.map(id => parseInt(id)).join(", ") + ")" : "") 530 + " ORDER BY parentItemID"; 531 // Since we do the sort here and cache these results, a restart will be required 532 // if this pref (off by default) is turned on, but that's OK 533 if (Zotero.Prefs.get('sortAttachmentsChronologically')) { 534 sql += ", dateAdded"; 535 } 536 var setAttachmentItem = function (itemID, rows) { 537 var item = this._objectCache[itemID]; 538 if (!item) { 539 throw new Error("Item " + itemID + " not loaded"); 540 } 541 542 item._attachments = { 543 rows, 544 chronologicalWithTrashed: null, 545 chronologicalWithoutTrashed: null, 546 alphabeticalWithTrashed: null, 547 alphabeticalWithoutTrashed: null 548 }; 549 }.bind(this); 550 var lastItemID = null; 551 yield Zotero.DB.queryAsync( 552 sql, 553 params, 554 { 555 noCache: ids.length != 1, 556 onRow: function (row) { 557 onRow(row, setAttachmentItem); 558 } 559 } 560 ); 561 // Process unprocessed rows 562 if (lastItemID) { 563 setAttachmentItem(lastItemID, rows); 564 } 565 // Otherwise clear existing entries for passed items 566 else if (ids.length) { 567 ids.forEach(id => setAttachmentItem(id, [])); 568 } 569 570 // 571 // Notes 572 // 573 sql = "SELECT parentItemID, N.itemID, title, " 574 + "CASE WHEN DI.itemID IS NULL THEN 0 ELSE 1 END AS trashed " 575 + "FROM itemNotes N " 576 + "JOIN items I ON (N.parentItemID=I.itemID) " 577 + "LEFT JOIN deletedItems DI USING (itemID) " 578 + "WHERE libraryID=?" 579 + (ids.length ? " AND parentItemID IN (" + ids.map(id => parseInt(id)).join(", ") + ")" : "") 580 + " ORDER BY parentItemID"; 581 if (Zotero.Prefs.get('sortNotesChronologically')) { 582 sql += ", dateAdded"; 583 } 584 var setNoteItem = function (itemID, rows) { 585 var item = this._objectCache[itemID]; 586 if (!item) { 587 throw new Error("Item " + itemID + " not loaded"); 588 } 589 590 item._notes = { 591 rows, 592 rowsEmbedded: null, 593 chronologicalWithTrashed: null, 594 chronologicalWithoutTrashed: null, 595 alphabeticalWithTrashed: null, 596 alphabeticalWithoutTrashed: null, 597 numWithTrashed: null, 598 numWithoutTrashed: null, 599 numWithTrashedWithEmbedded: null, 600 numWithoutTrashedWithoutEmbedded: null 601 }; 602 }.bind(this); 603 lastItemID = null; 604 rows = []; 605 yield Zotero.DB.queryAsync( 606 sql, 607 params, 608 { 609 noCache: ids.length != 1, 610 onRow: function (row) { 611 onRow(row, setNoteItem); 612 } 613 } 614 ); 615 // Process unprocessed rows 616 if (lastItemID) { 617 setNoteItem(lastItemID, rows); 618 } 619 // Otherwise clear existing entries for passed items 620 else if (ids.length) { 621 ids.forEach(id => setNoteItem(id, [])); 622 } 623 624 // Mark all top-level items as having child items loaded 625 sql = "SELECT itemID FROM items I WHERE libraryID=?" + idSQL + " AND itemID NOT IN " 626 + "(SELECT itemID FROM itemAttachments UNION SELECT itemID FROM itemNotes)"; 627 yield Zotero.DB.queryAsync( 628 sql, 629 params, 630 { 631 noCache: ids.length != 1, 632 onRow: function (row) { 633 var itemID = row.getResultByIndex(0); 634 var item = this._objectCache[itemID]; 635 if (!item) { 636 throw new Error("Item " + itemID + " not loaded"); 637 } 638 item._loaded.childItems = true; 639 item._clearChanged('childItems'); 640 }.bind(this) 641 } 642 ); 643 }); 644 645 646 this._loadTags = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { 647 var sql = "SELECT itemID, name, type FROM items " 648 + "LEFT JOIN itemTags USING (itemID) " 649 + "LEFT JOIN tags USING (tagID) WHERE libraryID=?" + idSQL; 650 var params = [libraryID]; 651 652 var lastItemID; 653 var rows = []; 654 var setRows = function (itemID, rows) { 655 var item = this._objectCache[itemID]; 656 if (!item) { 657 throw new Error("Item " + itemID + " not found"); 658 } 659 660 item._tags = []; 661 for (let i = 0; i < rows.length; i++) { 662 let row = rows[i]; 663 item._tags.push(Zotero.Tags.cleanData(row)); 664 } 665 666 item._loaded.tags = true; 667 }.bind(this); 668 669 yield Zotero.DB.queryAsync( 670 sql, 671 params, 672 { 673 noCache: ids.length != 1, 674 onRow: function (row) { 675 let itemID = row.getResultByIndex(0); 676 677 if (lastItemID && itemID !== lastItemID) { 678 setRows(lastItemID, rows); 679 rows = []; 680 } 681 682 lastItemID = itemID; 683 684 // Item has no tags 685 let tag = row.getResultByIndex(1); 686 if (tag === null) { 687 return; 688 } 689 690 rows.push({ 691 tag: tag, 692 type: row.getResultByIndex(2) 693 }); 694 }.bind(this) 695 } 696 ); 697 if (lastItemID) { 698 setRows(lastItemID, rows); 699 } 700 }); 701 702 703 this._loadCollections = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { 704 var sql = "SELECT itemID, collectionID FROM items " 705 + "LEFT JOIN collectionItems USING (itemID) " 706 + "WHERE libraryID=?" + idSQL; 707 var params = [libraryID]; 708 709 var lastItemID; 710 var rows = []; 711 var setRows = function (itemID, rows) { 712 var item = this._objectCache[itemID]; 713 if (!item) { 714 throw new Error("Item " + itemID + " not found"); 715 } 716 717 item._collections = rows; 718 item._loaded.collections = true; 719 item._clearChanged('collections'); 720 }.bind(this); 721 722 yield Zotero.DB.queryAsync( 723 sql, 724 params, 725 { 726 noCache: ids.length != 1, 727 onRow: function (row) { 728 let itemID = row.getResultByIndex(0); 729 730 if (lastItemID && itemID !== lastItemID) { 731 setRows(lastItemID, rows); 732 rows = []; 733 } 734 735 lastItemID = itemID; 736 let collectionID = row.getResultByIndex(1); 737 // No collections 738 if (collectionID === null) { 739 return; 740 } 741 rows.push(collectionID); 742 }.bind(this) 743 } 744 ); 745 if (lastItemID) { 746 setRows(lastItemID, rows); 747 } 748 }); 749 750 751 this.merge = function (item, otherItems) { 752 Zotero.debug("Merging items"); 753 754 return Zotero.DB.executeTransaction(function* () { 755 var otherItemIDs = []; 756 var itemURI = Zotero.URI.getItemURI(item); 757 758 var replPred = Zotero.Relations.replacedItemPredicate; 759 var toSave = {}; 760 toSave[item.id] = item; 761 762 for (let otherItem of otherItems) { 763 let otherItemURI = Zotero.URI.getItemURI(otherItem); 764 765 // Move child items to master 766 var ids = otherItem.getAttachments(true).concat(otherItem.getNotes(true)); 767 for (let id of ids) { 768 var attachment = yield this.getAsync(id); 769 770 // TODO: Skip identical children? 771 772 attachment.parentID = item.id; 773 yield attachment.save(); 774 } 775 776 // Add relations to master 777 let oldRelations = otherItem.getRelations(); 778 for (let pred in oldRelations) { 779 oldRelations[pred].forEach(obj => item.addRelation(pred, obj)); 780 } 781 782 // Remove merge-tracking relations from other item, so that there aren't two 783 // subjects for a given deleted object 784 let replItems = otherItem.getRelationsByPredicate(replPred); 785 for (let replItem of replItems) { 786 otherItem.removeRelation(replPred, replItem); 787 } 788 789 // Update relations on items in the library that point to the other item 790 // to point to the master instead 791 let rels = yield Zotero.Relations.getByObject('item', otherItemURI); 792 for (let rel of rels) { 793 // Skip merge-tracking relations, which are dealt with above 794 if (rel.predicate == replPred) continue; 795 // Skip items in other libraries. They might not be editable, and even 796 // if they are, merging items in one library shouldn't affect another library, 797 // so those will follow the merge-tracking relations and can optimize their 798 // path if they're resaved. 799 if (rel.subject.libraryID != item.libraryID) continue; 800 rel.subject.removeRelation(rel.predicate, otherItemURI); 801 rel.subject.addRelation(rel.predicate, itemURI); 802 if (!toSave[rel.subject.id]) { 803 toSave[rel.subject.id] = rel.subject; 804 } 805 } 806 807 // All other operations are additive only and do not affect the, 808 // old item, which will be put in the trash 809 810 // Add collections to master 811 otherItem.getCollections().forEach(id => item.addToCollection(id)); 812 813 // Add tags to master 814 var tags = otherItem.getTags(); 815 for (let j = 0; j < tags.length; j++) { 816 item.addTag(tags[j].tag); 817 } 818 819 // Add relation to track merge 820 item.addRelation(replPred, otherItemURI); 821 822 // Trash other item 823 otherItem.deleted = true; 824 yield otherItem.save(); 825 } 826 827 for (let i in toSave) { 828 yield toSave[i].save(); 829 } 830 831 // Hack to remove master item from duplicates view without recalculating duplicates 832 Zotero.Notifier.trigger('removeDuplicatesMaster', 'item', item.id); 833 }.bind(this)); 834 }; 835 836 837 this.trash = Zotero.Promise.coroutine(function* (ids) { 838 Zotero.DB.requireTransaction(); 839 840 var libraryIDs = new Set(); 841 ids = Zotero.flattenArguments(ids); 842 var items = []; 843 for (let id of ids) { 844 let item = this.get(id); 845 if (!item) { 846 Zotero.debug('Item ' + id + ' does not exist in Items.trash()!', 1); 847 Zotero.Notifier.queue('trash', 'item', id); 848 continue; 849 } 850 851 if (!item.isEditable()) { 852 throw new Error(item._ObjectType + " " + item.libraryKey + " is not editable"); 853 } 854 855 if (!Zotero.Libraries.get(item.libraryID).hasTrash) { 856 throw new Error(Zotero.Libraries.getName(item.libraryID) + " does not have a trash"); 857 } 858 859 items.push(item); 860 libraryIDs.add(item.libraryID); 861 } 862 863 var parentItemIDs = new Set(); 864 items.forEach(item => { 865 item.setDeleted(true); 866 item.synced = false; 867 if (item.parentItemID) { 868 parentItemIDs.add(item.parentItemID); 869 } 870 }); 871 yield Zotero.Utilities.Internal.forEachChunkAsync(ids, 250, Zotero.Promise.coroutine(function* (chunk) { 872 yield Zotero.DB.queryAsync( 873 "UPDATE items SET synced=0, clientDateModified=CURRENT_TIMESTAMP " 874 + `WHERE itemID IN (${chunk.map(id => parseInt(id)).join(", ")})` 875 ); 876 yield Zotero.DB.queryAsync( 877 "INSERT OR IGNORE INTO deletedItems (itemID) VALUES " 878 + chunk.map(id => "(" + id + ")").join(", ") 879 ); 880 }.bind(this))); 881 882 // Keep in sync with Zotero.Item::saveData() 883 for (let parentItemID of parentItemIDs) { 884 let parentItem = yield Zotero.Items.getAsync(parentItemID); 885 yield parentItem.reload(['primaryData', 'childItems'], true); 886 } 887 Zotero.Notifier.queue('modify', 'item', ids); 888 Zotero.Notifier.queue('trash', 'item', ids); 889 Array.from(libraryIDs).forEach(libraryID => { 890 Zotero.Notifier.queue('refresh', 'trash', libraryID); 891 }); 892 }); 893 894 895 this.trashTx = function (ids) { 896 return Zotero.DB.executeTransaction(function* () { 897 return this.trash(ids); 898 }.bind(this)); 899 } 900 901 902 /** 903 * @param {Integer} libraryID - Library to delete from 904 * @param {Object} [options] 905 * @param {Function} [options.onProgress] - fn(progress, progressMax) 906 * @param {Integer} [options.days] - Only delete items deleted more than this many days ago 907 * @param {Integer} [options.limit] - Number of items to delete 908 */ 909 this.emptyTrash = async function (libraryID, options = {}) { 910 if (arguments.length > 2 || typeof arguments[1] == 'number') { 911 Zotero.warn("Zotero.Items.emptyTrash() has changed -- update your code"); 912 options.days = arguments[1]; 913 options.limit = arguments[2]; 914 } 915 916 if (!libraryID) { 917 throw new Error("Library ID not provided"); 918 } 919 920 var t = new Date(); 921 922 var deleted = await this.getDeleted(libraryID, false, options.days); 923 924 if (options.limit) { 925 deleted = deleted.slice(0, options.limit); 926 } 927 928 var processed = 0; 929 if (deleted.length) { 930 let toDelete = { 931 top: [], 932 child: [] 933 }; 934 deleted.forEach((item) => { 935 item.isTopLevelItem() ? toDelete.top.push(item.id) : toDelete.child.push(item.id) 936 }); 937 938 // Show progress meter during deletions 939 let eraseOptions = options.onProgress 940 ? { 941 onProgress: function (progress, progressMax) { 942 options.onProgress(processed + progress, deleted.length); 943 } 944 } 945 : undefined; 946 for (let x of ['top', 'child']) { 947 await Zotero.Utilities.Internal.forEachChunkAsync( 948 toDelete[x], 949 1000, 950 async function (chunk) { 951 await this.erase(chunk, eraseOptions); 952 processed += chunk.length; 953 }.bind(this) 954 ); 955 } 956 Zotero.debug("Emptied " + deleted.length + " item(s) from trash in " + (new Date() - t) + " ms"); 957 } 958 959 return deleted.length; 960 }; 961 962 963 /** 964 * Start idle observer to delete trashed items older than a certain number of days 965 */ 966 this._emptyTrashIdleObserver = null; 967 this._emptyTrashTimeoutID = null; 968 this.startEmptyTrashTimer = function () { 969 this._emptyTrashIdleObserver = { 970 observe: (subject, topic, data) => { 971 if (topic == 'idle' || topic == 'timer-callback') { 972 var days = Zotero.Prefs.get('trashAutoEmptyDays'); 973 if (!days) { 974 return; 975 } 976 977 // TODO: empty group trashes if permissions 978 979 // Delete a few items a time 980 // 981 // TODO: increase number after dealing with slow 982 // tag.getLinkedItems() call during deletes 983 let num = 50; 984 this.emptyTrash( 985 Zotero.Libraries.userLibraryID, 986 { 987 days, 988 limit: num 989 } 990 ) 991 .then((deleted) => { 992 if (!deleted) { 993 this._emptyTrashTimeoutID = null; 994 return; 995 } 996 997 // Set a timer to do more every few seconds 998 this._emptyTrashTimeoutID = setTimeout(() => { 999 this._emptyTrashIdleObserver.observe(null, 'timer-callback', null); 1000 }, 2500); 1001 }); 1002 } 1003 // When no longer idle, cancel timer 1004 else if (topic == 'back') { 1005 if (this._emptyTrashTimeoutID) { 1006 clearTimeout(this._emptyTrashTimeoutID); 1007 this._emptyTrashTimeoutID = null; 1008 } 1009 } 1010 } 1011 }; 1012 1013 var idleService = Components.classes["@mozilla.org/widget/idleservice;1"]. 1014 getService(Components.interfaces.nsIIdleService); 1015 idleService.addIdleObserver(this._emptyTrashIdleObserver, 305); 1016 } 1017 1018 1019 this.addToPublications = function (items, options = {}) { 1020 if (!items.length) return; 1021 1022 return Zotero.DB.executeTransaction(function* () { 1023 var timestamp = Zotero.DB.transactionTimestamp; 1024 1025 var allItems = [...items]; 1026 1027 if (options.license) { 1028 for (let item of items) { 1029 if (!options.keepRights || !item.getField('rights')) { 1030 item.setField('rights', options.licenseName); 1031 } 1032 } 1033 } 1034 1035 if (options.childNotes) { 1036 for (let item of items) { 1037 item.getNotes().forEach(id => allItems.push(Zotero.Items.get(id))); 1038 } 1039 } 1040 1041 if (options.childFileAttachments || options.childLinks) { 1042 for (let item of items) { 1043 item.getAttachments().forEach(id => { 1044 var attachment = Zotero.Items.get(id); 1045 var linkMode = attachment.attachmentLinkMode; 1046 1047 if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) { 1048 Zotero.debug("Skipping child linked file attachment on drag"); 1049 return; 1050 } 1051 if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) { 1052 if (!options.childLinks) { 1053 Zotero.debug("Skipping child link attachment on drag"); 1054 return; 1055 } 1056 } 1057 else if (!options.childFileAttachments) { 1058 Zotero.debug("Skipping child file attachment on drag"); 1059 return; 1060 } 1061 allItems.push(attachment); 1062 }); 1063 } 1064 } 1065 1066 yield Zotero.Utilities.Internal.forEachChunkAsync(allItems, 250, Zotero.Promise.coroutine(function* (chunk) { 1067 for (let item of chunk) { 1068 item.setPublications(true); 1069 item.synced = false; 1070 } 1071 let ids = chunk.map(item => item.id); 1072 yield Zotero.DB.queryAsync( 1073 `UPDATE items SET synced=0, clientDateModified=? WHERE itemID IN (${ids.join(", ")})`, 1074 timestamp 1075 ); 1076 yield Zotero.DB.queryAsync( 1077 `INSERT OR IGNORE INTO publicationsItems VALUES (${ids.join("), (")})` 1078 ); 1079 }.bind(this))); 1080 Zotero.Notifier.queue('modify', 'item', allItems.map(item => item.id)); 1081 }.bind(this)); 1082 }; 1083 1084 1085 this.removeFromPublications = function (items) { 1086 return Zotero.DB.executeTransaction(function* () { 1087 let allItems = []; 1088 for (let item of items) { 1089 if (!item.inPublications) { 1090 throw new Error(`Item ${item.libraryKey} is not in My Publications`); 1091 } 1092 1093 // Remove all child items too 1094 if (item.isRegularItem()) { 1095 allItems.push(...this.get(item.getNotes(true).concat(item.getAttachments(true)))); 1096 } 1097 1098 allItems.push(item); 1099 } 1100 1101 allItems.forEach(item => { 1102 item.setPublications(false); 1103 item.synced = false; 1104 }); 1105 1106 var timestamp = Zotero.DB.transactionTimestamp; 1107 yield Zotero.Utilities.Internal.forEachChunkAsync(allItems, 250, Zotero.Promise.coroutine(function* (chunk) { 1108 let idStr = chunk.map(item => item.id).join(", "); 1109 yield Zotero.DB.queryAsync( 1110 `UPDATE items SET synced=0, clientDateModified=? WHERE itemID IN (${idStr})`, 1111 timestamp 1112 ); 1113 yield Zotero.DB.queryAsync(`DELETE FROM publicationsItems WHERE itemID IN (${idStr})`); 1114 }.bind(this))); 1115 Zotero.Notifier.queue('modify', 'item', items.map(item => item.id)); 1116 }.bind(this)); 1117 }; 1118 1119 1120 /** 1121 * Purge unused data values 1122 */ 1123 this.purge = Zotero.Promise.coroutine(function* () { 1124 Zotero.DB.requireTransaction(); 1125 1126 if (!Zotero.Prefs.get('purge.items')) { 1127 return; 1128 } 1129 1130 var sql = "DELETE FROM itemDataValues WHERE valueID NOT IN " 1131 + "(SELECT valueID FROM itemData)"; 1132 yield Zotero.DB.queryAsync(sql); 1133 1134 Zotero.Prefs.set('purge.items', false) 1135 }); 1136 1137 1138 1139 /** 1140 * Given API JSON for an item, return the best single first creator, regardless of creator order 1141 * 1142 * Note that this is just a single creator, not the firstCreator field return from the 1143 * Zotero.Item::firstCreator property or this.getFirstCreatorFromData() 1144 * 1145 * @return {Object|false} - Creator in API JSON format, or false 1146 */ 1147 this.getFirstCreatorFromJSON = function (json) { 1148 var primaryCreatorType = Zotero.CreatorTypes.getName( 1149 Zotero.CreatorTypes.getPrimaryIDForType( 1150 Zotero.ItemTypes.getID(json.itemType) 1151 ) 1152 ); 1153 let firstCreator = json.creators.find(creator => { 1154 return creator.creatorType == primaryCreatorType || creator.creatorType == 'author'; 1155 }); 1156 if (!firstCreator) { 1157 firstCreator = json.creators.find(creator => creator.creatorType == 'editor'); 1158 } 1159 if (!firstCreator) { 1160 return false; 1161 } 1162 return firstCreator; 1163 }; 1164 1165 1166 /** 1167 * Return a firstCreator string from internal creators data (from Zotero.Item::getCreators()). 1168 * 1169 * Used in Zotero.Item::getField() for unsaved items 1170 * 1171 * @param {Integer} itemTypeID 1172 * @param {Object} creatorData 1173 * @return {String} 1174 */ 1175 this.getFirstCreatorFromData = function (itemTypeID, creatorsData) { 1176 if (creatorsData.length === 0) { 1177 return ""; 1178 } 1179 1180 var validCreatorTypes = [ 1181 Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID), 1182 Zotero.CreatorTypes.getID('editor'), 1183 Zotero.CreatorTypes.getID('contributor') 1184 ]; 1185 1186 for (let creatorTypeID of validCreatorTypes) { 1187 let matches = creatorsData.filter(data => data.creatorTypeID == creatorTypeID) 1188 if (!matches.length) { 1189 continue; 1190 } 1191 if (matches.length === 1) { 1192 return matches[0].lastName; 1193 } 1194 if (matches.length === 2) { 1195 let a = matches[0]; 1196 let b = matches[1]; 1197 return a.lastName + " " + Zotero.getString('general.and') + " " + b.lastName; 1198 } 1199 if (matches.length >= 3) { 1200 return matches[0].lastName + " " + Zotero.getString('general.etAl'); 1201 } 1202 } 1203 1204 return ""; 1205 }; 1206 1207 1208 /** 1209 * Returns an array of items with children of selected parents removed 1210 * 1211 * @return {Zotero.Item[]} 1212 */ 1213 this.keepParents = function (items) { 1214 var parentItems = new Set( 1215 items 1216 .filter(item => item.isTopLevelItem()) 1217 .map(item => item.id) 1218 ); 1219 return items.filter(item => { 1220 var parentItemID = item.parentItemID; 1221 // Not a child item or not a child of one of the passed items 1222 return !parentItemID || !parentItems.has(parentItemID); 1223 }); 1224 } 1225 1226 1227 /* 1228 * Generate SQL to retrieve firstCreator field 1229 * 1230 * Why do we do this entirely in SQL? Because we're crazy. Crazy like foxes. 1231 */ 1232 var _firstCreatorSQL = ''; 1233 function _getFirstCreatorSQL() { 1234 if (_firstCreatorSQL) { 1235 return _firstCreatorSQL; 1236 } 1237 1238 /* This whole block is to get the firstCreator */ 1239 var localizedAnd = Zotero.getString('general.and'); 1240 var localizedEtAl = Zotero.getString('general.etAl'); 1241 var sql = "COALESCE(" + 1242 // First try for primary creator types 1243 "CASE (" + 1244 "SELECT COUNT(*) FROM itemCreators IC " + 1245 "LEFT JOIN itemTypeCreatorTypes ITCT " + 1246 "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " + 1247 "WHERE itemID=O.itemID AND primaryField=1" + 1248 ") " + 1249 "WHEN 0 THEN NULL " + 1250 "WHEN 1 THEN (" + 1251 "SELECT lastName FROM itemCreators IC NATURAL JOIN creators " + 1252 "LEFT JOIN itemTypeCreatorTypes ITCT " + 1253 "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " + 1254 "WHERE itemID=O.itemID AND primaryField=1" + 1255 ") " + 1256 "WHEN 2 THEN (" + 1257 "SELECT " + 1258 "(SELECT lastName FROM itemCreators IC NATURAL JOIN creators " + 1259 "LEFT JOIN itemTypeCreatorTypes ITCT " + 1260 "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " + 1261 "WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1)" + 1262 " || ' " + localizedAnd + " ' || " + 1263 "(SELECT lastName FROM itemCreators IC NATURAL JOIN creators " + 1264 "LEFT JOIN itemTypeCreatorTypes ITCT " + 1265 "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " + 1266 "WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1,1)" + 1267 ") " + 1268 "ELSE (" + 1269 "SELECT " + 1270 "(SELECT lastName FROM itemCreators IC NATURAL JOIN creators " + 1271 "LEFT JOIN itemTypeCreatorTypes ITCT " + 1272 "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " + 1273 "WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1)" + 1274 " || ' " + localizedEtAl + "' " + 1275 ") " + 1276 "END, " + 1277 1278 // Then try editors 1279 "CASE (" + 1280 "SELECT COUNT(*) FROM itemCreators WHERE itemID=O.itemID AND creatorTypeID IN (3)" + 1281 ") " + 1282 "WHEN 0 THEN NULL " + 1283 "WHEN 1 THEN (" + 1284 "SELECT lastName FROM itemCreators NATURAL JOIN creators " + 1285 "WHERE itemID=O.itemID AND creatorTypeID IN (3)" + 1286 ") " + 1287 "WHEN 2 THEN (" + 1288 "SELECT " + 1289 "(SELECT lastName FROM itemCreators NATURAL JOIN creators " + 1290 "WHERE itemID=O.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 1)" + 1291 " || ' " + localizedAnd + " ' || " + 1292 "(SELECT lastName FROM itemCreators NATURAL JOIN creators " + 1293 "WHERE itemID=O.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 1,1) " + 1294 ") " + 1295 "ELSE (" + 1296 "SELECT " + 1297 "(SELECT lastName FROM itemCreators NATURAL JOIN creators " + 1298 "WHERE itemID=O.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 1)" + 1299 " || ' " + localizedEtAl + "' " + 1300 ") " + 1301 "END, " + 1302 1303 // Then try contributors 1304 "CASE (" + 1305 "SELECT COUNT(*) FROM itemCreators WHERE itemID=O.itemID AND creatorTypeID IN (2)" + 1306 ") " + 1307 "WHEN 0 THEN NULL " + 1308 "WHEN 1 THEN (" + 1309 "SELECT lastName FROM itemCreators NATURAL JOIN creators " + 1310 "WHERE itemID=O.itemID AND creatorTypeID IN (2)" + 1311 ") " + 1312 "WHEN 2 THEN (" + 1313 "SELECT " + 1314 "(SELECT lastName FROM itemCreators NATURAL JOIN creators " + 1315 "WHERE itemID=O.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 1)" + 1316 " || ' " + localizedAnd + " ' || " + 1317 "(SELECT lastName FROM itemCreators NATURAL JOIN creators " + 1318 "WHERE itemID=O.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 1,1) " + 1319 ") " + 1320 "ELSE (" + 1321 "SELECT " + 1322 "(SELECT lastName FROM itemCreators NATURAL JOIN creators " + 1323 "WHERE itemID=O.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 1)" + 1324 " || ' " + localizedEtAl + "' " + 1325 ") " + 1326 "END" + 1327 ") AS firstCreator"; 1328 1329 _firstCreatorSQL = sql; 1330 return sql; 1331 } 1332 1333 1334 /* 1335 * Generate SQL to retrieve sortCreator field 1336 */ 1337 var _sortCreatorSQL = ''; 1338 function _getSortCreatorSQL() { 1339 if (_sortCreatorSQL) { 1340 return _sortCreatorSQL; 1341 } 1342 1343 var nameSQL = "lastName || ' ' || firstName "; 1344 1345 var sql = "COALESCE(" + 1346 // First try for primary creator types 1347 "CASE (" + 1348 "SELECT COUNT(*) FROM itemCreators IC " + 1349 "LEFT JOIN itemTypeCreatorTypes ITCT " + 1350 "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " + 1351 "WHERE itemID=O.itemID AND primaryField=1" + 1352 ") " + 1353 "WHEN 0 THEN NULL " + 1354 "WHEN 1 THEN (" + 1355 "SELECT " + nameSQL + "FROM itemCreators IC NATURAL JOIN creators " + 1356 "LEFT JOIN itemTypeCreatorTypes ITCT " + 1357 "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " + 1358 "WHERE itemID=O.itemID AND primaryField=1" + 1359 ") " + 1360 "WHEN 2 THEN (" + 1361 "SELECT " + 1362 "(SELECT " + nameSQL + " FROM itemCreators IC NATURAL JOIN creators " + 1363 "LEFT JOIN itemTypeCreatorTypes ITCT " + 1364 "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " + 1365 "WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1)" + 1366 " || ' ' || " + 1367 "(SELECT " + nameSQL + " FROM itemCreators IC NATURAL JOIN creators " + 1368 "LEFT JOIN itemTypeCreatorTypes ITCT " + 1369 "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " + 1370 "WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1,1)" + 1371 ") " + 1372 "ELSE (" + 1373 "SELECT " + 1374 "(SELECT " + nameSQL + " FROM itemCreators IC NATURAL JOIN creators " + 1375 "LEFT JOIN itemTypeCreatorTypes ITCT " + 1376 "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " + 1377 "WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1)" + 1378 " || ' ' || " + 1379 "(SELECT " + nameSQL + " FROM itemCreators IC NATURAL JOIN creators " + 1380 "LEFT JOIN itemTypeCreatorTypes ITCT " + 1381 "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " + 1382 "WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 1,1)" + 1383 " || ' ' || " + 1384 "(SELECT " + nameSQL + " FROM itemCreators IC NATURAL JOIN creators " + 1385 "LEFT JOIN itemTypeCreatorTypes ITCT " + 1386 "ON (IC.creatorTypeID=ITCT.creatorTypeID AND ITCT.itemTypeID=O.itemTypeID) " + 1387 "WHERE itemID=O.itemID AND primaryField=1 ORDER BY orderIndex LIMIT 2,1)" + 1388 ") " + 1389 "END, " + 1390 1391 // Then try editors 1392 "CASE (" + 1393 "SELECT COUNT(*) FROM itemCreators WHERE itemID=O.itemID AND creatorTypeID IN (3)" + 1394 ") " + 1395 "WHEN 0 THEN NULL " + 1396 "WHEN 1 THEN (" + 1397 "SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators " + 1398 "WHERE itemID=O.itemID AND creatorTypeID IN (3)" + 1399 ") " + 1400 "WHEN 2 THEN (" + 1401 "SELECT " + 1402 "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators " + 1403 "WHERE itemID=O.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 1)" + 1404 " || ' ' || " + 1405 "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators " + 1406 "WHERE itemID=O.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 1,1) " + 1407 ") " + 1408 "ELSE (" + 1409 "SELECT " + 1410 "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators " + 1411 "WHERE itemID=O.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 1)" + 1412 " || ' ' || " + 1413 "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators " + 1414 "WHERE itemID=O.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 1,1)" + 1415 " || ' ' || " + 1416 "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators " + 1417 "WHERE itemID=O.itemID AND creatorTypeID IN (3) ORDER BY orderIndex LIMIT 2,1)" + 1418 ") " + 1419 "END, " + 1420 1421 // Then try contributors 1422 "CASE (" + 1423 "SELECT COUNT(*) FROM itemCreators WHERE itemID=O.itemID AND creatorTypeID IN (2)" + 1424 ") " + 1425 "WHEN 0 THEN NULL " + 1426 "WHEN 1 THEN (" + 1427 "SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators " + 1428 "WHERE itemID=O.itemID AND creatorTypeID IN (2)" + 1429 ") " + 1430 "WHEN 2 THEN (" + 1431 "SELECT " + 1432 "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators " + 1433 "WHERE itemID=O.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 1)" + 1434 " || ' ' || " + 1435 "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators " + 1436 "WHERE itemID=O.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 1,1) " + 1437 ") " + 1438 "ELSE (" + 1439 "SELECT " + 1440 "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators " + 1441 "WHERE itemID=O.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 1)" + 1442 " || ' ' || " + 1443 "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators " + 1444 "WHERE itemID=O.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 1,1)" + 1445 " || ' ' || " + 1446 "(SELECT " + nameSQL + " FROM itemCreators NATURAL JOIN creators " + 1447 "WHERE itemID=O.itemID AND creatorTypeID IN (2) ORDER BY orderIndex LIMIT 2,1)" + 1448 ") " + 1449 "END" + 1450 ") AS sortCreator"; 1451 1452 _sortCreatorSQL = sql; 1453 return sql; 1454 } 1455 1456 1457 this.getSortTitle = function(title) { 1458 if (title === false || title === undefined) { 1459 return ''; 1460 } 1461 if (typeof title == 'number') { 1462 return title + ''; 1463 } 1464 return title.replace(/^[\[\'\"](.*)[\'\"\]]?$/, '$1') 1465 } 1466 1467 1468 Zotero.DataObjects.call(this); 1469 1470 return this; 1471 }.bind(Object.create(Zotero.DataObjects.prototype))();