feedItem.js (10251B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2015 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 * Constructor for FeedItem object 29 */ 30 Zotero.FeedItem = function(itemTypeOrID, params = {}) { 31 Zotero.FeedItem._super.call(this, itemTypeOrID); 32 33 this._feedItemReadTime = null; 34 this._feedItemTranslatedTime = null; 35 36 Zotero.Utilities.assignProps(this, params, ['guid']); 37 }; 38 39 Zotero.extendClass(Zotero.Item, Zotero.FeedItem); 40 41 Zotero.FeedItem.prototype._objectType = 'feedItem'; 42 Zotero.FeedItem.prototype._containerObject = 'feed'; 43 44 Zotero.defineProperty(Zotero.FeedItem.prototype, 'isFeedItem', { 45 value: true 46 }); 47 48 Zotero.defineProperty(Zotero.FeedItem.prototype, 'guid', { 49 get: function() { return this._feedItemGUID; }, 50 set: function(val) { 51 if (this.id) throw new Error('Cannot set GUID after item ID is already set'); 52 if (typeof val != 'string') throw new Error('GUID must be a non-empty string'); 53 this._feedItemGUID = val; 54 } 55 }); 56 57 Zotero.defineProperty(Zotero.FeedItem.prototype, 'isRead', { 58 get: function() { 59 return !!this._feedItemReadTime; 60 }, 61 set: function(read) { 62 if (!read != !this._feedItemReadTime) { 63 // changed 64 if (read) { 65 this._feedItemReadTime = Zotero.Date.dateToSQL(new Date(), true); 66 } else { 67 this._feedItemReadTime = null; 68 } 69 this._changed.feedItemData = true; 70 } 71 } 72 }); 73 // 74 Zotero.defineProperty(Zotero.FeedItem.prototype, 'isTranslated', { 75 get: function() { 76 return !!this._feedItemTranslatedTime; 77 }, 78 set: function(state) { 79 if (state != !!this._feedItemTranslatedTime) { 80 if (state) { 81 this._feedItemTranslatedTime = Zotero.Date.dateToSQL(new Date(), true); 82 } else { 83 this._feedItemTranslatedTime = null; 84 } 85 this._changed.feedItemData = true; 86 } 87 } 88 }); 89 90 Zotero.FeedItem.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (reload, failOnMissing) { 91 if (this.guid && !this.id) { 92 // fill in item ID 93 this.id = yield this.ObjectsClass.getIDFromGUID(this.guid); 94 } 95 yield Zotero.FeedItem._super.prototype.loadPrimaryData.apply(this, arguments); 96 }); 97 98 Zotero.FeedItem.prototype.setField = function(field, value) { 99 if (field == 'libraryID') { 100 // Ensure that it references a feed 101 if (!Zotero.Libraries.get(value).isFeed) { 102 throw new Error('libraryID must reference a feed'); 103 } 104 } 105 106 return Zotero.FeedItem._super.prototype.setField.apply(this, arguments); 107 } 108 109 Zotero.FeedItem.prototype.fromJSON = function(json) { 110 // Spaghetti to handle weird date formats in feedItems 111 let val = json.date; 112 if (val) { 113 let d = Zotero.Date.sqlToDate(val, true); 114 if (!d || isNaN(d.getTime())) { 115 d = Zotero.Date.isoToDate(val); 116 } 117 if ((!d || isNaN(d.getTime())) && Zotero.Date.isHTTPDate(val)) { 118 d = new Date(val); 119 } 120 if (!d || isNaN(d.getTime())) { 121 d = Zotero.Date.strToDate(val); 122 if (d) { 123 json.date = [d.year, Zotero.Utilities.lpad(d.month+1, '0', 2), Zotero.Utilities.lpad(d.day, '0', 2)].join('-'); 124 } else { 125 Zotero.logError("Discarding invalid date '" + json.date 126 + "' for item " + this.libraryKey); 127 delete json.date; 128 } 129 } else { 130 json.date = Zotero.Date.dateToSQL(d, true); 131 } 132 } 133 Zotero.FeedItem._super.prototype.fromJSON.apply(this, arguments); 134 } 135 136 Zotero.FeedItem.prototype._initSave = Zotero.Promise.coroutine(function* (env) { 137 if (!this.guid) { 138 throw new Error('GUID must be set before saving ' + this._ObjectType); 139 } 140 141 let proceed = yield Zotero.FeedItem._super.prototype._initSave.apply(this, arguments); 142 if (!proceed) return proceed; 143 144 if (env.isNew) { 145 // verify that GUID doesn't already exist for a new item 146 var item = yield this.ObjectsClass.getIDFromGUID(this.guid); 147 if (item) { 148 throw new Error('Cannot create new item with GUID ' + this.guid + '. Item already exists.'); 149 } 150 151 // Register GUID => itemID mapping in cache on commit 152 if (!env.transactionOptions) env.transactionOptions = {}; 153 var superOnCommit = env.transactionOptions.onCommit; 154 env.transactionOptions.onCommit = () => { 155 if (superOnCommit) superOnCommit(); 156 this.ObjectsClass._setGUIDMapping(this.guid, this._id); 157 }; 158 } 159 160 return proceed; 161 }); 162 163 Zotero.FeedItem.prototype._saveData = Zotero.Promise.coroutine(function* (env) { 164 yield Zotero.FeedItem._super.prototype._saveData.apply(this, arguments); 165 166 if (this._changed.feedItemData || env.isNew) { 167 var sql = "REPLACE INTO feedItems VALUES (?,?,?,?)"; 168 yield Zotero.DB.queryAsync( 169 sql, 170 [ 171 this.id, 172 this.guid, 173 this._feedItemReadTime, 174 this._feedItemTranslatedTime 175 ] 176 ); 177 178 this._clearChanged('feedItemData'); 179 } 180 }); 181 182 Zotero.FeedItem.prototype._finalizeErase = Zotero.Promise.coroutine(function* () { 183 // Set for syncing 184 let feed = Zotero.Feeds.get(this.libraryID); 185 186 return Zotero.FeedItem._super.prototype._finalizeErase.apply(this, arguments); 187 }); 188 189 Zotero.FeedItem.prototype.toggleRead = Zotero.Promise.coroutine(function* (state) { 190 state = state !== undefined ? !!state : !this.isRead; 191 let changed = this.isRead != state; 192 if (changed) { 193 this.isRead = state; 194 195 yield this.saveTx(); 196 197 let feed = Zotero.Feeds.get(this.libraryID); 198 yield feed.updateUnreadCount(); 199 } 200 }); 201 202 /** 203 * Uses the item url to translate an existing feed item. 204 * If libraryID empty, overwrites feed item, otherwise saves 205 * in the library 206 * @param libraryID {Integer} save item in library 207 * @param collectionID {Integer} add item to collection 208 * @return {Promise<FeedItem|Item>} translated feed item 209 */ 210 Zotero.FeedItem.prototype.translate = Zotero.Promise.coroutine(function* (libraryID, collectionID) { 211 Zotero.debug("Translating feed item " + this.id + " with URL " + this.getField('url'), 2); 212 if (Zotero.locked) { 213 Zotero.debug('Zotero locked, skipping feed item translation'); 214 return; 215 } 216 217 let deferred = Zotero.Promise.defer(); 218 let error = function(e) { Zotero.debug(e, 1); deferred.reject(e); }; 219 let translate = new Zotero.Translate.Web(); 220 var win = Services.wm.getMostRecentWindow("navigator:browser"); 221 let progressWindow = win.ZoteroPane.progressWindow; 222 223 if (libraryID) { 224 // Show progress notifications when scraping to a library. 225 translate.clearHandlers("done"); 226 translate.clearHandlers("itemDone"); 227 translate.setHandler("done", progressWindow.Translation.doneHandler); 228 translate.setHandler("itemDone", progressWindow.Translation.itemDoneHandler()); 229 if (collectionID) { 230 var collection = yield Zotero.Collections.getAsync(collectionID); 231 } 232 progressWindow.show(); 233 progressWindow.Translation.scrapingTo(libraryID, collection); 234 } 235 236 // Load document 237 let hiddenBrowser = Zotero.HTTP.loadDocuments( 238 this.getField('url'), 239 doc => deferred.resolve(doc), 240 () => {}, 241 error, 242 true 243 ); 244 let doc = yield deferred.promise; 245 246 // Set translate document 247 translate.setDocument(doc); 248 249 // Load translators 250 deferred = Zotero.Promise.defer(); 251 translate.setHandler('translators', (me, translators) => deferred.resolve(translators)); 252 translate.getTranslators(); 253 let translators = yield deferred.promise; 254 if (!translators || !translators.length) { 255 Zotero.debug("No translators detected for feed item " + this.id + " with URL " + this.getField('url') + 256 ' -- cloning item instead', 2); 257 let item = yield this.clone(libraryID, collectionID, doc); 258 progressWindow.Translation.itemDoneHandler()(null, null, item); 259 progressWindow.Translation.doneHandler(null, true); 260 return; 261 } 262 translate.setTranslator(translators[0]); 263 264 deferred = Zotero.Promise.defer(); 265 266 if (libraryID) { 267 let result = yield translate.translate({libraryID, collections: collectionID ? [collectionID] : false}) 268 .then(items => items ? items[0] : false); 269 Zotero.Browser.deleteHiddenBrowser(hiddenBrowser); 270 if (!result) { 271 let item = yield this.clone(libraryID, collectionID, doc); 272 progressWindow.Translation.itemDoneHandler()(null, null, item); 273 progressWindow.Translation.doneHandler(null, true); 274 return; 275 } 276 return result; 277 } 278 279 // Clear these to prevent saving 280 translate.clearHandlers('itemDone'); 281 translate.clearHandlers('itemsDone'); 282 translate.setHandler('error', error); 283 translate.setHandler('itemDone', (_, items) => deferred.resolve(items)); 284 285 translate.translate({libraryID: false, saveAttachments: false}); 286 287 let itemData = yield deferred.promise; 288 Zotero.Browser.deleteHiddenBrowser(hiddenBrowser); 289 290 // clean itemData 291 const deleteFields = ['attachments', 'notes', 'id', 'itemID', 'path', 'seeAlso', 'version', 'dateAdded', 'dateModified']; 292 for (let field of deleteFields) { 293 delete itemData[field]; 294 } 295 296 this.fromJSON(itemData); 297 this.isTranslated = true; 298 yield this.saveTx(); 299 300 return this; 301 }); 302 303 /** 304 * Clones the feed item (usually, when proper translation is unavailable) 305 * @param libraryID {Integer} save item in library 306 * @param collectionID {Integer} add item to collection 307 * @return {Promise<FeedItem|Item>} translated feed item 308 */ 309 Zotero.FeedItem.prototype.clone = Zotero.Promise.coroutine(function* (libraryID, collectionID, doc) { 310 let dbItem = Zotero.Item.prototype.clone.call(this, libraryID); 311 if (collectionID) { 312 dbItem.addToCollection(collectionID); 313 } 314 yield dbItem.saveTx(); 315 316 let item = {title: dbItem.getField('title'), itemType: dbItem.itemType, attachments: []}; 317 318 // Add snapshot 319 if (Zotero.Libraries.get(libraryID).filesEditable) { 320 item.attachments = [{title: "Snapshot"}]; 321 yield Zotero.Attachments.importFromDocument({ 322 document: doc, 323 parentItemID: dbItem.id 324 }); 325 } 326 327 return item; 328 });