Various feeds changes

And move Z.Attachments.cleanAttachmentURI() to Z.Utilities.cleanURL()
This commit is contained in:
Adomas Venčkauskas 2016-01-13 13:13:29 +00:00 committed by Dan Stillman
parent 8a2dc6e7f2
commit e6ede4b36f
24 changed files with 698 additions and 211 deletions

View file

@ -36,33 +36,15 @@ var Zotero_Feed_Settings = new function() {
urlTainted = false;
let cleanURL = function(url) {
url = url.trim();
if (!url) return;
let ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
let cleanUrl;
try {
let uri = ios.newURI(url, null, null);
if (uri.scheme != 'http' && uri.scheme != 'https') {
let cleanUrl = Zotero.Utilities.cleanURL(url, true);
if (cleanUrl) {
if (/^https?:\/\/[^\/\s]+\/\S/.test(cleanUrl)) {
return cleanUrl;
} else {
Zotero.debug(uri.scheme + " is not a supported protocol for feeds.");
}
cleanUrl = uri.spec;
} catch (e) {
if (e.result == Components.results.NS_ERROR_MALFORMED_URI) {
// Assume it's a URL missing "http://" part
try {
cleanUrl = ios.newURI('http://' + url, null, null).spec;
} catch (e) {}
}
throw e;
}
if (!cleanUrl) return;
if (/^https?:\/\/[^\/\s]+\/\S/.test(cleanUrl)) return cleanUrl;
};
this.init = function() {
@ -93,9 +75,9 @@ var Zotero_Feed_Settings = new function() {
}
document.getElementById('feed-ttl').value = ttl;
let cleanAfter = data.cleanAfter;
if (cleanAfter === undefined) cleanAfter = 2;
document.getElementById('feed-cleanAfter').value = cleanAfter;
let cleanupAfter = data.cleanupAfter;
if (cleanupAfter === undefined) cleanupAfter = 2;
document.getElementById('feed-cleanupAfter').value = cleanupAfter;
if (data.url && !data.urlIsValid) {
this.validateUrl();
@ -114,7 +96,7 @@ var Zotero_Feed_Settings = new function() {
urlIsValid = false;
document.getElementById('feed-title').disabled = true;
document.getElementById('feed-ttl').disabled = true;
document.getElementById('feed-cleanAfter').disabled = true;
document.getElementById('feed-cleanupAfter').disabled = true;
document.documentElement.getButton('accept').disabled = true;
};
@ -132,6 +114,8 @@ var Zotero_Feed_Settings = new function() {
let fr = feedReader = new Zotero.FeedReader(url);
yield fr.process();
let feed = fr.feedProperties;
// Prevent progress if textbox changes triggered another call to
// validateUrl / invalidateUrl (old session)
if (feedReader !== fr || urlTainted) return;
let title = document.getElementById('feed-title');
@ -149,7 +133,7 @@ var Zotero_Feed_Settings = new function() {
urlIsValid = true;
title.disabled = false;
ttl.disabled = false;
document.getElementById('feed-cleanAfter').disabled = false;
document.getElementById('feed-cleanupAfter').disabled = false;
document.documentElement.getButton('accept').disabled = false;
}
catch (e) {
@ -164,7 +148,7 @@ var Zotero_Feed_Settings = new function() {
data.url = document.getElementById('feed-url').value;
data.title = document.getElementById('feed-title').value;
data.ttl = document.getElementById('feed-ttl').value * 60;
data.cleanAfter = document.getElementById('feed-cleanAfter').value * 1;
data.cleanupAfter = document.getElementById('feed-cleanupAfter').value * 1;
return true;
};

View file

@ -40,9 +40,9 @@
<label value="&zotero.feedSettings.refresh.label2;" control="feed-ttl"/>
</hbox>
<hbox align="center">
<label value="&zotero.feedSettings.cleanAfter.label1;" control="feed-cleanAfter"/>
<textbox id="feed-cleanAfter" type="number" min="0" increment="1" size="2"/>
<label value="&zotero.feedSettings.cleanAfter.label2;" control="feed-cleanAfter"/>
<label value="&zotero.feedSettings.cleanupAfter.label1;" control="feed-cleanupAfter"/>
<textbox id="feed-cleanupAfter" type="number" min="0" increment="1" size="2"/>
<label value="&zotero.feedSettings.cleanupAfter.label2;" control="feed-cleanupAfter"/>
</hbox>
</vbox>
</vbox>

View file

@ -683,32 +683,13 @@ Zotero.Attachments = new function(){
return attachmentItem;
});
/**
* @deprecated Use Zotero.Utilities.cleanURL instead
*/
this.cleanAttachmentURI = function (uri, tryHttp) {
uri = uri.trim();
if (!uri) return false;
var ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
try {
return ios.newURI(uri, null, null).spec // Valid URI if succeeds
} catch (e) {
if (e instanceof Components.Exception
&& e.result == Components.results.NS_ERROR_MALFORMED_URI
) {
if (tryHttp && /\w\.\w/.test(uri)) {
// Assume it's a URL missing "http://" part
try {
return ios.newURI('http://' + uri, null, null).spec;
} catch (e) {}
}
Zotero.debug('cleanAttachmentURI: Invalid URI: ' + uri, 2);
return false;
}
throw e;
}
return Zotero.Utilities.cleanURL(uri, tryHttp);
}

View file

@ -490,8 +490,12 @@ Zotero.CollectionTreeView.prototype.notify = Zotero.Promise.coroutine(function*
break;
case 'feed':
case 'group':
yield this.reload();
yield this.selectByID(currentTreeRow.id);
break;
case 'feed':
yield this.reload();
yield this.selectByID("L" + id);
break;
@ -790,9 +794,6 @@ Zotero.CollectionTreeView.prototype.isContainerEmpty = function(row)
&& this._unfiledLibraries.indexOf(libraryID) == -1
&& this.hideSources.indexOf('trash') != -1;
}
if (treeRow.isFeed()) {
return false; // If it's shown, it has something
}
if (treeRow.isCollection()) {
return !treeRow.ref.hasChildCollections();
}
@ -1107,6 +1108,7 @@ Zotero.CollectionTreeView.prototype.deleteSelection = Zotero.Promise.coroutine(f
yield treeRow.ref.eraseTx({
deleteItems: true
});
}
if (treeRow.isCollection() || treeRow.isFeed()) {
yield treeRow.ref.erase(deleteItems);
}
@ -1139,7 +1141,7 @@ Zotero.CollectionTreeView.prototype._expandRow = Zotero.Promise.coroutine(functi
var isCollection = treeRow.isCollection();
var libraryID = treeRow.ref.libraryID;
if (treeRow.isPublications()) {
if (treeRow.isPublications() || treeRow.isFeed()) {
return false;
}
@ -1335,7 +1337,7 @@ Zotero.CollectionTreeView.prototype._rememberOpenStates = Zotero.Promise.corouti
var open = this.isContainerOpen(i);
// Collections and feeds default to closed
if (!open && treeRow.isCollection() && treeRow.isFeed()) {
if (!open && treeRow.isCollection() || treeRow.isFeed()) {
delete state[treeRow.id];
continue;
}
@ -1434,6 +1436,11 @@ Zotero.CollectionTreeView.prototype.canDropCheck = function (row, orient, dataTr
return false;
}
if (treeRow.isFeed()) {
Zotero.debug("Cannot drop into feeds");
return false;
}
if (dataType == 'zotero/item') {
var ids = data;
var items = Zotero.Items.get(ids);

View file

@ -23,6 +23,19 @@
***** END LICENSE BLOCK *****
*/
/**
* Zotero.Feed, extends Zotero.Library
*
* Custom parameters:
* - name - name of the feed displayed in the collection tree
* - url
* - cleanupAfter - number of days after which read items should be removed
* - refreshInterval - in terms of hours
*
* @param params
* @returns Zotero.Feed
* @constructor
*/
Zotero.Feed = function(params = {}) {
params.libraryType = 'feed';
Zotero.Feed._super.call(this, params);
@ -30,8 +43,8 @@ Zotero.Feed = function(params = {}) {
this._feedCleanupAfter = null;
this._feedRefreshInterval = null;
// Feeds are editable by the user. Remove the setter
this.editable = true;
// Feeds are not editable by the user. Remove the setter
this.editable = false;
Zotero.defineProperty(this, 'editable', {
get: function() this._get('_libraryEditable')
});
@ -42,8 +55,8 @@ Zotero.Feed = function(params = {}) {
get: function() this._get('_libraryFilesEditable')
});
Zotero.Utilities.assignProps(this, params, ['name', 'url', 'refreshInterval',
'cleanupAfter']);
Zotero.Utilities.assignProps(this, params,
['name', 'url', 'refreshInterval', 'cleanupAfter']);
// Return a proxy so that we can disable the object once it's deleted
return new Proxy(this, {
@ -63,6 +76,8 @@ Zotero.Feed._colToProp = function(c) {
return "_feed" + Zotero.Utilities.capitalize(c);
}
Zotero.extendClass(Zotero.Library, Zotero.Feed);
Zotero.defineProperty(Zotero.Feed, '_unreadCountSQL', {
value: "(SELECT COUNT(*) FROM items I JOIN feedItems FeI USING (itemID)"
+ " WHERE I.libraryID=F.libraryID AND FeI.readTime IS NULL) AS _feedUnreadCount"
@ -86,8 +101,6 @@ Zotero.defineProperty(Zotero.Feed, '_rowSQL', {
+ " FROM feeds F JOIN libraries L USING (libraryID)"
});
Zotero.extendClass(Zotero.Library, Zotero.Feed);
Zotero.defineProperty(Zotero.Feed.prototype, '_objectType', {
value: 'feed'
});
@ -103,12 +116,7 @@ Zotero.defineProperty(Zotero.Feed.prototype, 'unreadCount', {
get: function() this._feedUnreadCount
});
Zotero.defineProperty(Zotero.Feed.prototype, 'updating', {
get: function() this._updating,
set: function(v) {
if (!v == !this._updating) return; // Unchanged
this._updating = !!v;
Zotero.Notifier.trigger('statusChanged', 'feed', this.id);
}
get: function() !!this._updating,
});
(function() {
@ -291,10 +299,10 @@ Zotero.Feed.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) {
Zotero.Feed.prototype.getExpiredFeedItemIDs = Zotero.Promise.coroutine(function* () {
let sql = "SELECT itemID AS id FROM feedItems "
+ "LEFT JOIN libraryItems LI USING (itemID)"
+ "WHERE LI.libraryID=?"
+ "WHERE readTime IS NOT NULL "
+ "AND (julianday(readTime, 'utc') + (?) - julianday('now', 'utc')) > 0";
+ "LEFT JOIN items I USING (itemID) "
+ "WHERE I.libraryID=? "
+ "AND readTime IS NOT NULL "
+ "AND julianday('now', 'utc') - (julianday(readTime, 'utc') + ?) > 0";
let expiredIDs = yield Zotero.DB.queryAsync(sql, [this.id, {int: this.cleanupAfter}]);
return expiredIDs.map(row => row.id);
});
@ -307,7 +315,7 @@ Zotero.Feed.prototype.clearExpiredItems = Zotero.Promise.coroutine(function* ()
Zotero.debug("Cleaning up read feed items...");
if (expiredItems.length) {
Zotero.debug(expiredItems.join(', '));
yield Zotero.FeedItems.eraseTx(expiredItems);
yield Zotero.FeedItems.forceErase(expiredItems);
} else {
Zotero.debug("No expired feed items");
}
@ -319,26 +327,36 @@ Zotero.Feed.prototype.clearExpiredItems = Zotero.Promise.coroutine(function* ()
});
Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () {
this.updating = true;
this.lastCheckError = null;
var toAdd = [];
if (this._updating) {
return this._updating;
}
let deferred = Zotero.Promise.defer();
this._updating = deferred.promise;
Zotero.Notifier.trigger('statusChanged', 'feed', this.id);
this._set('_feedLastCheckError', null);
yield this.clearExpiredItems();
try {
let fr = new Zotero.FeedReader(this.url);
yield fr.process();
let itemIterator = new fr.ItemIterator();
let item, toAdd = [], processedGUIDs = [];
let item, processedGUIDs = [];
while (item = yield itemIterator.next().value) {
// NOTE: Might cause issues with feeds that set pubDate for publication date of the item
// rather than the date the item was added to the feed.
if (item.dateModified && this.lastUpdate
&& item.dateModified < this.lastUpdate
) {
Zotero.debug("Item modification date before last update date (" + this._feedLastCheck + ")");
Zotero.debug("Item modification date before last update date (" + this.lastCheck + ")");
Zotero.debug(item);
// We can stop now
fr.terminate();
break;
}
// Append id at the end to prevent same item collisions from different feeds
item.guid += ":" + this.id;
if (processedGUIDs.indexOf(item.guid) != -1) {
Zotero.debug("Feed item " + item.guid + " already processed from feed.");
continue;
@ -356,8 +374,8 @@ Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () {
} else {
Zotero.debug("Feed item " + item.guid + " already in library.");
if (item.dateModified && feedItem.dateModified
&& feedItem.dateModified == item.dateModified
if (!item.dateModified ||
(feedItem.dateModified && feedItem.dateModified == item.dateModified)
) {
Zotero.debug("Modification date has not changed. Skipping update.");
continue;
@ -370,39 +388,46 @@ Zotero.Feed.prototype._updateFeed = Zotero.Promise.coroutine(function* () {
// Delete invalid data
delete item.guid;
delete item.dateAdded;
feedItem.fromJSON(item);
toAdd.push(feedItem);
}
// Save in reverse order
let savePromises = new Array(toAdd.length);
for (let i=toAdd.length-1; i>=0; i--) {
// Saving currently has to happen sequentially so as not to violate the
// unique constraints in dataValues (FIXME)
yield toAdd[i].save({skipEditCheck: true});
}
this.lastUpdate = Zotero.Date.dateToSQL(new Date(), true);
}
catch (e) {
if (e.message) {
Zotero.debug("Error processing feed from " + this.url);
Zotero.debug(e);
}
this.lastCheckError = e.message || 'Error processing feed';
this._set('_feedLastCheckError', e.message || 'Error processing feed');
}
this.lastCheck = Zotero.Date.dateToSQL(new Date(), true);
yield this.saveTx({skipEditCheck: true});
this.updating = false;
if (toAdd.length) {
yield Zotero.DB.executeTransaction(function* () {
// Save in reverse order
for (let i=toAdd.length-1; i>=0; i--) {
// Saving currently has to happen sequentially so as not to violate the
// unique constraints in itemDataValues (FIXME)
yield toAdd[i].save({skipEditCheck: true});
}
});
this._set('_feedLastUpdate', Zotero.Date.dateToSQL(new Date(), true));
}
this._set('_feedLastCheck', Zotero.Date.dateToSQL(new Date(), true));
yield this.saveTx();
yield this.updateUnreadCount();
deferred.resolve();
this._updating = false;
Zotero.Notifier.trigger('statusChanged', 'feed', this.id);
});
Zotero.Feed.prototype.updateFeed = function() {
return this._updateFeed()
.finally(function() {
Zotero.Feed.prototype.updateFeed = Zotero.Promise.coroutine(function* () {
try {
let result = yield this._updateFeed();
return result;
} finally {
Zotero.Feeds.scheduleNextFeedCheck();
});
}
}
});
Zotero.Feed.prototype._finalizeErase = Zotero.Promise.coroutine(function* (){
let notifierData = {};
@ -411,14 +436,14 @@ Zotero.Feed.prototype._finalizeErase = Zotero.Promise.coroutine(function* (){
};
Zotero.Notifier.trigger('delete', 'feed', this.id, notifierData);
Zotero.Feeds.unregister(this.libraryID);
return Zotero.Feed._super.prototype._finalizeErase.call(this);
return Zotero.Feed._super.prototype._finalizeErase.apply(this, arguments);
});
Zotero.Feed.prototype.erase = Zotero.Promise.coroutine(function* (deleteItems) {
Zotero.Feed.prototype.erase = Zotero.Promise.coroutine(function* (options = {}) {
let childItemIDs = yield Zotero.FeedItems.getAll(this.id, false, false, true);
yield Zotero.FeedItems.erase(childItemIDs);
yield Zotero.FeedItems.forceErase(childItemIDs);
yield Zotero.Feed._super.prototype.erase.call(this);
yield Zotero.Feed._super.prototype.erase.call(this, options);
});
Zotero.Feed.prototype.updateUnreadCount = Zotero.Promise.coroutine(function* () {

View file

@ -89,6 +89,32 @@ Zotero.FeedItem.prototype.setField = function(field, value) {
return Zotero.FeedItem._super.prototype.setField.apply(this, arguments);
}
Zotero.FeedItem.prototype.fromJSON = function(json) {
// Handle weird formats in feedItems
let dateFields = ['accessDate', 'dateAdded', 'dateModified'];
for (let dateField of dateFields) {
let val = json[dateField];
if (val) {
let d = new Date(val);
if (isNaN(d.getTime())) {
d = Zotero.Date.sqlToDate(val, true);
}
if (!d || isNaN(d.getTime())) {
d = Zotero.Date.strToDate(val);
d = new Date(d.year, d.month, d.day);
Zotero.debug(dateField + " " + JSON.stringify(d), 1);
}
if (!d) {
Zotero.logError("Discarding invalid " + field + " '" + val
+ "' for item " + this.libraryKey);
delete json[dateField];
continue;
}
json[dateField] = d.toISOString();
}
}
Zotero.FeedItem._super.prototype.fromJSON.apply(this, arguments);
}
Zotero.FeedItem.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
if (!this.guid) {
@ -124,6 +150,11 @@ Zotero.FeedItem.prototype.forceSaveTx = function(options) {
return this.saveTx(newOptions);
}
Zotero.FeedItem.prototype.save = function(options = {}) {
options.skipDateModifiedUpdate = true;
return Zotero.FeedItem._super.prototype.save.apply(this, arguments)
}
Zotero.FeedItem.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
yield Zotero.FeedItem._super.prototype._saveData.apply(this, arguments);
@ -138,12 +169,12 @@ Zotero.FeedItem.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
Zotero.FeedItem.prototype.toggleRead = Zotero.Promise.coroutine(function* (state) {
state = state !== undefined ? !!state : !this.isRead;
let changed = this.isRead != state;
this.isRead = state;
if (changed) {
yield this.save({skipEditCheck: true, skipDateModifiedUpdate: true});
this.isRead = state;
yield this.saveTx({skipEditCheck: true, skipDateModifiedUpdate: true});
let feed = Zotero.Feeds.get(this.libraryID);
feed.updateUnreadCount();
yield feed.updateUnreadCount();
}
});

View file

@ -95,6 +95,7 @@ Zotero.FeedItems = new Proxy(function() {
});
this.toggleReadByID = Zotero.Promise.coroutine(function* (ids, state) {
var feedsToUpdate = new Set();
if (!Array.isArray(ids)) {
if (typeof ids != 'string') throw new Error('ids must be a string or array in Zotero.FeedItems.toggleReadByID');
@ -104,20 +105,33 @@ Zotero.FeedItems = new Proxy(function() {
if (state == undefined) {
// If state undefined, toggle read if at least one unread
state = true;
state = false;
for (let item of items) {
if (item.isRead) {
state = false;
if (!item.isRead) {
state = true;
break;
}
}
}
for (let i=0; i<items.length; i++) {
items[i].toggleRead(state);
yield Zotero.DB.executeTransaction(function() {
for (let i=0; i<items.length; i++) {
items[i].isRead = state;
yield items[i].save({skipEditCheck: true});
feedsToUpdate.add(items[i].libraryID);
}
});
for (let feedID of feedsToUpdate) {
let feed = Zotero.Feeds.get(feedID);
yield feed.updateUnreadCount();
}
});
this.forceErase = function(ids, options = {}) {
options.skipEditCheck = true;
return this.erase(ids, options);
};
return this;
}.call({}),

View file

@ -118,8 +118,8 @@ Zotero.Feeds = new function() {
Zotero.debug("Scheduling next feed update.");
let sql = "SELECT ( CASE "
+ "WHEN lastCheck IS NULL THEN 0 "
+ "ELSE julianday(lastCheck, 'utc') + (refreshInterval/1440.0) - julianday('now', 'utc') "
+ "END ) * 1440 AS nextCheck "
+ "ELSE strftime('%s', lastCheck) + refreshInterval*3600 - strftime('%s', 'now') "
+ "END ) AS nextCheck "
+ "FROM feeds WHERE refreshInterval IS NOT NULL "
+ "ORDER BY nextCheck ASC LIMIT 1";
var nextCheck = yield Zotero.DB.valueQueryAsync(sql);
@ -130,9 +130,9 @@ Zotero.Feeds = new function() {
}
if (nextCheck !== false) {
nextCheck = nextCheck > 0 ? Math.ceil(nextCheck * 60000) : 0;
Zotero.debug("Next feed check in " + nextCheck/60000 + " minutes");
this._nextFeedCheck = Zotero.Promise.delay(nextCheck).cancellable();
nextCheck = nextCheck > 0 ? nextCheck * 1000 : 0;
Zotero.debug("Next feed check in " + nextCheck / 60000 + " minutes");
this._nextFeedCheck = Zotero.Promise.delay(nextCheck);
Zotero.Promise.all([this._nextFeedCheck, globalFeedCheckDelay])
.then(() => {
globalFeedCheckDelay = Zotero.Promise.delay(60000); // Don't perform auto-updates more than once per minute
@ -157,16 +157,12 @@ Zotero.Feeds = new function() {
+ "OR (julianday(lastCheck, 'utc') + (refreshInterval/1440.0) - julianday('now', 'utc')) <= 0 )";
let needUpdate = (yield Zotero.DB.queryAsync(sql)).map(row => row.id);
Zotero.debug("Running update for feeds: " + needUpdate.join(', '));
let feeds = Zotero.Libraries.get(needUpdate);
let updatePromises = [];
for (let i=0; i<feeds.length; i++) {
updatePromises.push(feeds[i]._updateFeed());
for (let i=0; i<needUpdate.length; i++) {
let feed = Zotero.Feeds.get(needUpdate[i]);
yield feed._updateFeed();
}
return Zotero.Promise.settle(updatePromises)
.then(() => {
Zotero.debug("All feed updates done.");
this.scheduleNextFeedCheck()
});
Zotero.debug("All feed updates done.");
this.scheduleNextFeedCheck();
});
}

View file

@ -298,7 +298,7 @@ Zotero.Libraries = new function () {
this._ensureExists(libraryID);
return Zotero.Libraries.get(libraryID).filesEditable;
};
/**
* @deprecated
*

View file

@ -35,6 +35,7 @@
* http://rss.sciencedirect.com/publication/science/20925212
* http://www.ncbi.nlm.nih.gov/entrez/eutils/erss.cgi?rss_guid=1fmfIeN4X5Q8HemTZD5Rj6iu6-FQVCn7xc7_IPIIQtS1XiD9bf
* http://export.arxiv.org/rss/astro-ph
* http://fhs.dukejournals.org/rss_feeds/recent.xml TODO: refreshing unreads all items
*/
/**
@ -104,7 +105,6 @@ Zotero.FeedReader = function(url) {
this._feedProperties = info;
this._feed = feed;
return info;
}.bind(this)).then(function(){
let items = this._feed.items;
if (items && items.length) {
@ -119,9 +119,11 @@ Zotero.FeedReader = function(url) {
this._feedItems.push(Zotero.Promise.defer()); // Push a new deferred promise so an iterator has something to return
lastItem.resolve(feedItem);
}
this._feedProcessed.resolve();
}
this._feedProcessed.resolve();
}.bind(this)).catch(function(e) {
Zotero.debug("Feed processing failed " + e.message);
this._feedProcessed.reject(e);
}.bind(this)).finally(function() {
// Make sure the last promise gets resolved to null
let lastItem = this._feedItems[this._feedItems.length - 1];
@ -229,6 +231,10 @@ Zotero.defineProperty(Zotero.FeedReader.prototype, 'ItemIterator', {
};
};
iterator.prototype.last = function() {
return items[items.length-1];
}
return iterator;
}
}, {lazy: true});
@ -304,7 +310,8 @@ Zotero.FeedReader._processCreators = function(feedEntry, field, role) {
names.push(name);
}
}
} catch(e) {
}
catch(e) {
if (e.result != Components.results.NS_ERROR_FAILURE) throw e;
if (field != 'authors') return [];
@ -372,20 +379,21 @@ Zotero.FeedReader._getFeedItem = function(feedEntry, feedInfo) {
if (feedEntry.updated) item.dateModified = new Date(feedEntry.updated);
if (feedEntry.published) {
let date = new Date(feedEntry.published);
var date = new Date(feedEntry.published);
if (!date.getUTCSeconds() && !(date.getUTCHours() && date.getUTCMinutes())) {
// There was probably no time, but there may have been a a date range,
// so something could have ended up in the hour _or_ minute field
item.date = getFeedField(feedEntry, null, 'pubDate')
date = getFeedField(feedEntry, 'pubDate')
/* In case it was magically pulled from some other field */
|| ( date.getUTCFullYear() + '-'
+ (date.getUTCMonth() + 1) + '-'
+ date.getUTCDate() );
} else {
item.date = Zotero.FeedReader._formatDate(date);
// Add time zone
}
else {
date = Zotero.Date.dateToSQL(date, true);
}
item.dateAdded = date;
if (!item.dateModified) {
items.dateModified = date;
@ -395,16 +403,16 @@ Zotero.FeedReader._getFeedItem = function(feedEntry, feedInfo) {
if (!item.dateModified) {
// When there's no reliable modification date, we can assume that item doesn't get updated
Zotero.debug("FeedReader: Feed item missing a modification date (" + item.guid + ")");
} else {
// Convert date modified to string, since those are directly comparable
item.dateModified = Zotero.Date.dateToSQL(item.dateModified, true);
}
if (!item.date && item.dateModified) {
if (!item.dateAdded && item.dateModified) {
// Use lastModified date
item.date = Zotero.FeedReader._formatDate(item.dateModified);
item.dateAdded = item.dateModified;
}
// Convert date modified to string, since those are directly comparable
if (item.dateModified) item.dateModified = Zotero.Date.dateToSQL(item.dateModified, true);
if (feedEntry.rights) item.rights = Zotero.FeedReader._getRichText(feedEntry.rights, 'rights');
item.creators = Zotero.FeedReader._processCreators(feedEntry, 'authors', 'author');
@ -421,7 +429,7 @@ Zotero.FeedReader._getFeedItem = function(feedEntry, feedInfo) {
/** Done with basic metadata, now look for better data **/
let date = Zotero.FeedReader._getFeedField(feedEntry, 'publicationDate', 'prism')
date = Zotero.FeedReader._getFeedField(feedEntry, 'publicationDate', 'prism')
|| Zotero.FeedReader._getFeedField(feedEntry, 'date', 'dc');
if (date) item.date = date;
@ -499,13 +507,6 @@ Zotero.FeedReader._getRichText = function(feedText, field) {
return Zotero.Utilities.dom2text(domFragment, field);
};
/*
* Format JS date as SQL date
*/
Zotero.FeedReader._formatDate = function(date) {
return Zotero.Date.dateToSQL(date, true);
}
/*
* Get field value from feed entry by namespace:fieldName
*/

View file

@ -98,6 +98,7 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree
}
this._treebox = treebox;
this.setSortColumn();
if (this._ownerDocument.defaultView.ZoteroPane_Local) {
this._ownerDocument.defaultView.ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading'));
@ -259,6 +260,48 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree
});
Zotero.ItemTreeView.prototype.setSortColumn = function() {
var dir, col, currentCol, currentDir;
for (let i=0, len=this._treebox.columns.count; i<len; i++) {
let column = this._treebox.columns.getColumnAt(i);
if (column.element.getAttribute('sortActive')) {
currentCol = column;
currentDir = column.element.getAttribute('sortDirection');
column.element.removeAttribute('sortActive');
column.element.removeAttribute('sortDirection');
break;
}
}
let colId = Zotero.Prefs.get('itemTree.sortColumnId');
// Restore previous sort setting (feed -> non-feed)
if (! this.collectionTreeRow.isFeed() && colId) {
col = this._treebox.columns.getNamedColumn(colId);
dir = Zotero.Prefs.get('itemTree.sortDirection');
Zotero.Prefs.clear('itemTree.sortColumnId');
Zotero.Prefs.clear('itemTree.sortDirection');
// Sort Feeds by dateAdded (anything -> feed)
} else if (this.collectionTreeRow.isFeed()) {
col = this._treebox.columns.getNamedColumn("zotero-items-column-dateAdded");
dir = 'descending';
// No previous sort setting stored, so store it (non-feed -> feed)
if (!colId && currentCol) {
Zotero.Prefs.set('itemTree.sortColumnId', currentCol.id);
Zotero.Prefs.set('itemTree.sortDirection', currentDir);
}
// Retain current sort setting (non-feed -> non-feed)
} else {
col = currentCol;
dir = currentDir;
}
if (col) {
col.element.setAttribute('sortActive', true);
col.element.setAttribute('sortDirection', dir);
}
}
/**
* Reload the rows from the data access methods
* (doesn't call the tree.invalidate methods, etc.)

View file

@ -188,7 +188,7 @@ Zotero.URI = new function () {
return path;
}
if (obj instanceof Zotero.Item || obj instanceof Zotero.Feed) {
if (obj instanceof Zotero.Item) {
return path + '/items/' + obj.key;
}

View file

@ -252,6 +252,38 @@ Zotero.Utilities = {
var x = x.replace(/^[\x00-\x27\x29-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F\s]+/, "");
return x.replace(/[\x00-\x28\x2A-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F\s]+$/, "");
},
/**
* Cleans a http url string
* @param url {String}
* @params tryHttp {Boolean} Attempt prepending 'http://' to the url
* @returns {String}
*/
cleanURL: function(url, tryHttp=false) {
url = url.trim();
if (!url) return false;
var ios = Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService);
try {
return ios.newURI(url, null, null).spec; // Valid URI if succeeds
} catch (e) {
if (e instanceof Components.Exception
&& e.result == Components.results.NS_ERROR_MALFORMED_URI
) {
if (tryHttp && /\w\.\w/.test(url)) {
// Assume it's a URL missing "http://" part
try {
return ios.newURI('http://' + url, null, null).spec;
} catch (e) {}
}
Zotero.debug('cleanURL: Invalid URI: ' + url, 2);
return false;
}
throw e;
}
},
/**
* Eliminates HTML tags, replacing &lt;br&gt;s with newlines

View file

@ -860,8 +860,8 @@ var ZoteroPane = new function()
feed.url = data.url;
feed.name = data.title;
feed.refreshInterval = data.ttl;
feed.cleanupAfter = data.cleanAfter;
yield feed.save({skipEditCheck: true});
feed.cleanupAfter = data.cleanupAfter;
yield feed.saveTx();
yield feed.updateFeed();
}
});
@ -1927,7 +1927,7 @@ var ZoteroPane = new function()
feed.name = data.title;
feed.refreshInterval = data.ttl;
feed.cleanupAfter = data.cleanAfter;
yield feed.save({skipEditCheck: true});
yield feed.saveTx();
});
this.refreshFeed = function() {
@ -4285,8 +4285,7 @@ var ZoteroPane = new function()
}
let feedItem;
itemReadTimeout = Zotero.FeedItems.getAsync(feedItemID)
.cancellable()
itemReadTimeout = Zotero.FeedItems.getAsync(feedItemID)
.then(function(newFeedItem) {
if (!newFeedItem) {
throw new Zotero.Promise.CancellationError('Not a FeedItem');

View file

@ -265,8 +265,8 @@
<!ENTITY zotero.feedSettings.refresh.label1 "Refresh Interval:">
<!ENTITY zotero.feedSettings.refresh.label2 "hour(s)">
<!ENTITY zotero.feedSettings.title.label "Title">
<!ENTITY zotero.feedSettings.cleanAfter.label1 "Remove read articles after ">
<!ENTITY zotero.feedSettings.cleanAfter.label2 "day(s)">
<!ENTITY zotero.feedSettings.cleanupAfter.label1 "Remove read articles after ">
<!ENTITY zotero.feedSettings.cleanupAfter.label2 "day(s)">
<!ENTITY zotero.recognizePDF.recognizing.label "Retrieving Metadata…">

View file

@ -184,7 +184,7 @@ pane.collections.duplicate = Duplicate Items
pane.collections.menu.rename.collection = Rename Collection…
pane.collections.menu.edit.savedSearch = Edit Saved Search…
pane.collections.menu.edit.savedSearch = Edit Feed…
pane.collections.menu.edit.feed = Edit Feed…
pane.collections.menu.delete.collection = Delete Collection…
pane.collections.menu.delete.collectionAndItems = Delete Collection and Items…
pane.collections.menu.delete.savedSearch = Delete Saved Search…

View file

@ -305,6 +305,24 @@ var createGroup = Zotero.Promise.coroutine(function* (props = {}) {
return group;
});
var createFeed = Zotero.Promise.coroutine(function* (props = {}) {
var feed = new Zotero.Feed;
feed.name = props.name || "Test " + Zotero.Utilities.randomString();
feed.description = props.description || "";
feed.url = props.url || 'http://www.' + Zotero.Utilities.randomString() + '.com/feed.rss';
feed.refreshInterval = props.refreshInterval || 12;
feed.cleanupAfter = props.cleanupAfter || 2;
yield feed.saveTx();
return feed;
});
var clearFeeds = Zotero.Promise.coroutine(function* () {
let feeds = Zotero.Feeds.getAll();
for (let i=0; i<feeds.length; i++) {
yield feeds[i].eraseTx();
}
});
//
// Data objects
//

View file

@ -292,6 +292,13 @@ describe("Zotero.CollectionTreeView", function() {
spy.restore();
}
})
it("should select a new feed", function* () {
var feed = yield createFeed();
// Library should still be selected
assert.equal(cv.getSelectedLibraryID(), feed.id);
})
})
describe("#drop()", function () {

View file

@ -0,0 +1,42 @@
<?xml version="1.0"?>
<!-- Lifted from http://cyber.law.harvard.edu/rss/examples/rss2sample.xml -->
<rss version="2.0">
<channel>
<title>Liftoff News</title>
<link>http://liftoff.msfc.nasa.gov/</link>
<description>Liftoff to Space Exploration.</description>
<language>en-us</language>
<pubDate>Tue, 03 Jun 2037 09:39:21 GMT</pubDate>
<lastBuildDate>Tue, 03 Jun 2037 09:39:21 GMT</lastBuildDate>
<docs>http://blogs.law.harvard.edu/tech/rss</docs>
<generator>Weblog Editor 2.0</generator>
<managingEditor>editor@example.com</managingEditor>
<webMaster>webmaster@example.com</webMaster>
<item>
<title>Star City</title>
<link>http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp</link>
<description>How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's &lt;a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm"&gt;Star City&lt;/a&gt;.</description>
<pubDate>Tue, 03 Jun 2037 09:39:21 GMT</pubDate>
<guid>http://liftoff.msfc.nasa.gov/2003/06/03.html#item573</guid>
</item>
<item>
<description>Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a &lt;a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm"&gt;partial eclipse of the Sun&lt;/a&gt; on Saturday, May 31st.</description>
<pubDate>Fri, 30 May 2003 11:06:42 GMT</pubDate>
<guid>http://liftoff.msfc.nasa.gov/2003/05/30.html#item572</guid>
</item>
<item>
<title>The Engine That Does More</title>
<link>http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp</link>
<description>Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that.</description>
<pubDate>Tue, 27 May 2003 08:37:32 GMT</pubDate>
<guid>http://liftoff.msfc.nasa.gov/2003/05/27.html#item571</guid>
</item>
<item>
<title>Astronauts' Dirty Laundry</title>
<link>http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp</link>
<description>Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options.</description>
<pubDate>Tue, 20 May 2003 08:56:02 GMT</pubDate>
<guid>http://liftoff.msfc.nasa.gov/2003/05/20.html#item570</guid>
</item>
</channel>
</rss>

View file

@ -1,12 +1,12 @@
describe("Zotero.FeedItem", function () {
let feed, libraryID;
before(function* () {
feed = new Zotero.Feed({ name: 'Test ' + Zotero.randomString(), url: 'http://' + Zotero.randomString() + '.com/' });
feed = yield createFeed({ name: 'Test ' + Zotero.randomString(), url: 'http://' + Zotero.randomString() + '.com/' });
yield feed.saveTx();
libraryID = feed.libraryID;
});
after(function() {
return feed.eraseTx();
return clearFeeds();
});
it("should be an instance of Zotero.Item", function() {
@ -90,6 +90,22 @@ describe("Zotero.FeedItem", function () {
assert.closeTo(readTime, expectedTimestamp, 2000, 'read timestamp is correct in the DB');
});
});
describe("#fromJSON()", function() {
it("should attempt to parse non ISO-8601 dates", function* () {
var json = {
itemType: "journalArticle",
accessDate: "2015-06-07 20:56:00",
dateAdded: "18-20 June 2015", // magically parsed by `new Date()`
dateModified: "07/06/2015", // US
};
var item = new Zotero.FeedItem;
item.fromJSON(json);
assert.strictEqual(item.getField('accessDate'), '2015-06-07 20:56:00');
assert.strictEqual(item.getField('dateAdded'), '2015-06-18 20:00:00');
// sets a timezone specific hour when new Date parses from strings without hour specified.
assert.strictEqual(item.getField('dateModified'), Zotero.Date.dateToSQL(new Date(2015, 6, 6), true));
})
});
describe("#save()", function() {
it("should require edit check override", function* () {
let feedItem = new Zotero.FeedItem('book', { guid: Zotero.randomString() });
@ -175,4 +191,33 @@ describe("Zotero.FeedItem", function () {
yield assert.isRejected(feedItem.eraseTx(), /^Error: Cannot edit feedItem in read-only library/);
});
});
describe("#toggleRead()", function() {
it('should toggle state', function* () {
feed = yield createFeed();
let item = yield createDataObject('feedItem', { guid: Zotero.randomString(), libraryID: feed.id });
item.isRead = false;
yield item.forceSaveTx();
yield item.toggleRead();
assert.isTrue(item.isRead, "item is toggled to read state");
});
it('should save if specified state is different from current', function* (){
feed = yield createFeed();
let item = yield createDataObject('feedItem', { guid: Zotero.randomString(), libraryID: feed.id });
item.isRead = false;
yield item.forceSaveTx();
sinon.spy(item, 'save');
yield item.toggleRead(true);
assert.isTrue(item.save.called, "item was saved on toggle read");
item.save.reset();
yield item.toggleRead(true);
assert.isFalse(item.save.called, "item was not saved on toggle read to same state");
});
});
});

View file

@ -1,11 +1,10 @@
describe("Zotero.FeedItems", function () {
let feed;
before(function() {
feed = new Zotero.Feed({ name: 'foo', url: 'http://' + Zotero.randomString() + '.com' });
return feed.saveTx();
before(function* () {
feed = yield createFeed({ name: 'foo', url: 'http://' + Zotero.randomString() + '.com' });
});
after(function() {
return feed.eraseTx();
return clearFeeds();
});
describe("#getIDFromGUID()", function() {
@ -35,4 +34,64 @@ describe("Zotero.FeedItems", function () {
assert.isFalse(feedItem);
});
});
describe("#toggleReadByID()", function() {
var save, feed, items, ids;
before(function() {
save = sinon.spy(Zotero.FeedItem.prototype, 'save');
});
beforeEach(function* (){
feed = yield createFeed();
items = [];
for (let i = 0; i < 10; i++) {
let item = yield createDataObject('feedItem', { guid: Zotero.randomString(), libraryID: feed.id });
item.isRead = true;
yield item.forceSaveTx();
items.push(item);
}
ids = Array.map(items, (i) => i.id);
});
after(function() {
save.restore();
});
afterEach(function* () {
save.reset();
yield clearFeeds();
});
it('should toggle all items read if at least one unread', function* () {
items[0].isRead = false;
yield items[0].forceSaveTx();
yield Zotero.FeedItems.toggleReadByID(ids);
for(let i = 0; i < 10; i++) {
assert.isTrue(save.thisValues[i].isRead, "#toggleRead called with true");
}
});
it('should toggle all items unread if all read', function* () {
yield Zotero.FeedItems.toggleReadByID(ids);
for(let i = 0; i < 10; i++) {
assert.isFalse(save.thisValues[i].isRead, "#toggleRead called with false");
}
});
it('should toggle all items unread if unread state specified', function* () {
items[0].isRead = false;
yield items[0].forceSaveTx();
yield Zotero.FeedItems.toggleReadByID(ids, false);
for(let i = 0; i < 10; i++) {
assert.isFalse(save.thisValues[i].isRead, "#toggleRead called with true");
}
});
});
});

View file

@ -30,6 +30,10 @@ describe("Zotero.FeedReader", function () {
language: 'en'
};
after(function* () {
yield clearFeeds();
});
describe('FeedReader()', function () {
it('should throw if url not provided', function() {
assert.throw(() => new Zotero.FeedReader())
@ -108,7 +112,7 @@ describe("Zotero.FeedReader", function () {
abstractNote: 'How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia\'s Star City.',
url: 'http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp',
dateModified: '2003-06-03 09:39:21',
date: '2003-06-03 09:39:21',
dateAdded: '2003-06-03 09:39:21',
creators: [{
firstName: '',
lastName: 'editor@example.com',
@ -127,12 +131,13 @@ describe("Zotero.FeedReader", function () {
});
it('should parse items correctly for a detailed feed', function* () {
let expected = { guid: 'http://www.example.com/item1',
let expected = {
guid: 'http://www.example.com/item1',
title: 'Title 1',
abstractNote: 'Description 1',
url: 'http://www.example.com/item1',
dateModified: '2016-01-07 00:00:00',
date: '2016-01-07',
dateAdded: '2016-01-07 00:00:00',
creators: [
{ firstName: 'Author1 A. T.', lastName: 'Rohtua', creatorType: 'author' },
{ firstName: 'Author2 A.', lastName: 'Auth', creatorType: 'author' },
@ -141,6 +146,7 @@ describe("Zotero.FeedReader", function () {
{ firstName: 'Contributor2 C.', lastName: 'Contrib', creatorType: 'contributor' },
{ firstName: 'Contributor3', lastName: 'Contr', creatorType: 'contributor' }
],
date: '2016-01-07',
publicationTitle: 'Publication',
ISSN: '0000-0000',
publisher: 'Publisher',

View file

@ -1,24 +1,7 @@
describe("Zotero.Feed", function() {
let createFeed = Zotero.Promise.coroutine(function* (props = {}) {
let feed = new Zotero.Feed({
name: props.name || 'Test ' + Zotero.randomString(),
url: props.url || 'http://www.' + Zotero.randomString() + '.com',
refreshInterval: props.refreshInterval,
cleanupAfter: props.cleanupAfter
});
yield feed.saveTx();
return feed;
});
// Clean up after after tests
after(function* () {
let feeds = Zotero.Feeds.getAll();
yield Zotero.DB.executeTransaction(function* () {
for (let i=0; i<feeds.length; i++) {
yield feeds[i].erase();
}
});
yield clearFeeds();
});
it("should be an instance of Zotero.Library", function() {
@ -93,7 +76,7 @@ describe("Zotero.Feed", function() {
name: 'Test ' + Zotero.randomString(),
url: 'http://' + Zotero.randomString() + '.com/',
refreshInterval: 30,
cleanupAfter: 2
cleanupAfter: 1
};
let feed = yield createFeed(props);
@ -179,6 +162,157 @@ describe("Zotero.Feed", function() {
});
});
describe("#clearExpiredItems()", function() {
var feed, expiredFeedItem, readFeedItem, feedItem, feedItemIDs;
before(function* (){
feed = yield createFeed({cleanupAfter: 1});
expiredFeedItem = yield createDataObject('feedItem', { libraryID: feed.libraryID });
// Read 2 days ago
expiredFeedItem.isRead = true;
expiredFeedItem._feedItemReadTime = Zotero.Date.dateToSQL(
new Date(Date.now() - 2 * 24*60*60*1000), true);
yield expiredFeedItem.forceSaveTx();
readFeedItem = yield createDataObject('feedItem', { libraryID: feed.libraryID });
readFeedItem.isRead = true;
yield readFeedItem.forceSaveTx();
feedItem = yield createDataObject('feedItem', { libraryID: feed.libraryID });
feedItemIDs = yield Zotero.FeedItems.getAll(feed.libraryID).map((row) => row.id);
assert.include(feedItemIDs, feedItem.id, "feed contains unread feed item");
assert.include(feedItemIDs, readFeedItem.id, "feed contains read feed item");
assert.include(feedItemIDs, expiredFeedItem.id, "feed contains expired feed item");
yield feed.clearExpiredItems();
feedItemIDs = yield Zotero.FeedItems.getAll(feed.libraryID).map((row) => row.id);
});
it('should clear expired items', function() {
assert.notInclude(feedItemIDs, expiredFeedItem.id, "feed no longer contain expired feed item");
});
it('should not clear read items that have not expired yet', function() {
assert.include(feedItemIDs, readFeedItem.id, "feed still contains new feed item");
})
it('should not clear unread items', function() {
assert.include(feedItemIDs, feedItem.id, "feed still contains new feed item");
});
});
describe('#updateFeed()', function() {
var feedUrl = getTestDataItemUrl("feed.rss");
var modifiedFeedUrl = getTestDataItemUrl("feedModified.rss");
afterEach(function* () {
yield clearFeeds();
});
it('should schedule next feed check', function* () {
let scheduleNextFeedCheck = sinon.stub(Zotero.Feeds, 'scheduleNextFeedCheck');
let feed = yield createFeed();
feed._feedUrl = feedUrl;
yield feed.updateFeed();
assert.equal(scheduleNextFeedCheck.called, true);
scheduleNextFeedCheck.restore();
});
it('should add new feed items', function* () {
let feed = yield createFeed();
feed._feedUrl = feedUrl;
yield feed.updateFeed();
let feedItems = yield Zotero.FeedItems.getAll(feed.id);
assert.equal(feedItems.length, 4);
});
it('should set lastCheck and lastUpdated values', function* () {
let feed = yield createFeed();
feed._feedUrl = feedUrl;
assert.notOk(feed.lastCheck);
assert.notOk(feed.lastUpdate);
yield feed.updateFeed();
assert.ok(feed.lastCheck >= Zotero.Date.dateToSQL(new Date(Date.now() - 1000*60), true));
assert.ok(feed.lastUpdate >= Zotero.Date.dateToSQL(new Date(Date.now() - 1000*60), true));
});
it('should update modified items and set unread', function* () {
let feed = yield createFeed();
feed._feedUrl = feedUrl;
yield feed.updateFeed();
let feedItem = yield Zotero.FeedItems.getAsyncByGUID("http://liftoff.msfc.nasa.gov/2003/06/03.html#item573");
feedItem.isRead = true;
yield feedItem.forceSaveTx();
feedItem = yield Zotero.FeedItems.getAsyncByGUID("http://liftoff.msfc.nasa.gov/2003/06/03.html#item573");
assert.isTrue(feedItem.isRead);
let oldDateModified = feedItem.dateModified;
feed._feedUrl = modifiedFeedUrl;
yield feed.updateFeed();
feedItem = yield Zotero.FeedItems.getAsyncByGUID("http://liftoff.msfc.nasa.gov/2003/06/03.html#item573");
assert.notEqual(oldDateModified, feedItem.dateModified);
assert.isFalse(feedItem.isRead)
});
it('should skip items that are not modified', function* () {
let feed = yield createFeed();
feed._feedUrl = feedUrl;
yield feed.updateFeed();
let feedItems = yield Zotero.FeedItems.getAll(feed.id);
let datesAdded = [], datesModified = [];
for(let feedItem of feedItems) {
datesAdded.push(feedItem.dateAdded);
datesModified.push(feedItem.dateModified);
}
feed._feedUrl = modifiedFeedUrl;
yield feed.updateFeed();
feedItems = yield Zotero.FeedItems.getAll(feed.id);
let changedCount = 0;
for (let i = 0; i < feedItems.length; i++) {
assert.equal(feedItems[i].dateAdded, datesAdded[i]);
if (feedItems[i].dateModified != datesModified[i]) {
changedCount++;
}
}
assert.equal(changedCount, 1);
});
it('should update unread count', function* () {
let feed = yield createFeed();
feed._feedUrl = feedUrl;
yield feed.updateFeed();
assert.equal(feed.unreadCount, 4);
let feedItems = yield Zotero.FeedItems.getAll(feed.id);
for (let feedItem of feedItems) {
feedItem.isRead = true;
yield feedItem.forceSaveTx();
}
feed._feedUrl = modifiedFeedUrl;
yield feed.updateFeed();
assert.equal(feed.unreadCount, 1);
});
});
describe("Adding items", function() {
let feed;
before(function* () {

View file

@ -1,25 +1,9 @@
describe("Zotero.Feeds", function () {
let createFeed = Zotero.Promise.coroutine(function* (props = {}) {
let feed = new Zotero.Feed({
name: props.name || 'Test ' + Zotero.randomString(),
url: props.url || 'http://www.' + Zotero.randomString() + '.com',
refreshInterval: props.refreshInterval,
cleanupAfter: props.cleanupAfter
});
yield feed.saveTx();
return feed;
});
let clearFeeds = Zotero.Promise.coroutine(function* () {
let feeds = Zotero.Feeds.getAll();
yield Zotero.DB.executeTransaction(function* () {
for (let i=0; i<feeds.length; i++) {
yield feeds[i].erase();
}
});
after(function* () {
yield clearFeeds();
});
describe("#haveFeeds()", function() {
it("should return false for a DB without feeds", function* () {
yield clearFeeds();
@ -56,4 +40,83 @@ describe("Zotero.Feeds", function () {
assert.sameMembers(feeds, [feed1, feed2]);
});
});
describe('#updateFeeds', function() {
var freshFeed, recentFeed, oldFeed;
var _updateFeed;
before(function* () {
yield clearFeeds();
sinon.stub(Zotero.Feeds, 'scheduleNextFeedCheck');
_updateFeed = sinon.stub(Zotero.Feed.prototype, '_updateFeed').resolves();
let url = getTestDataItemUrl("feed.rss");
freshFeed = yield createFeed({refreshInterval: 2});
freshFeed._feedUrl = url;
freshFeed.lastCheck = null;
yield freshFeed.saveTx();
recentFeed = yield createFeed({refreshInterval: 2});
recentFeed._feedUrl = url;
recentFeed.lastCheck = Zotero.Date.dateToSQL(new Date(), true);
yield recentFeed.saveTx();
oldFeed = yield createFeed({refreshInterval: 2});
oldFeed._feedUrl = url;
oldFeed.lastCheck = Zotero.Date.dateToSQL(new Date(Date.now() - 1000*60*60*6), true);
yield oldFeed.saveTx();
yield Zotero.Feeds.updateFeeds();
assert.isTrue(_updateFeed.called);
});
after(function() {
Zotero.Feeds.scheduleNextFeedCheck.restore();
_updateFeed.restore();
});
it('should update feeds that have never been updated', function() {
for (var feed of _updateFeed.thisValues) {
if (feed.id == freshFeed.id) {
break;
}
}
assert.isTrue(feed._updateFeed.called);
});
it('should update feeds that need updating since last check', function() {
for (var feed of _updateFeed.thisValues) {
if (feed.id == oldFeed.id) {
break;
}
}
assert.isTrue(feed._updateFeed.called);
});
it("should not update feeds that don't need updating", function() {
for (var feed of _updateFeed.thisValues) {
if (feed.id != recentFeed.id) {
break;
}
// should never reach
assert.isOk(null, "does not update feed that did not need updating")
}
});
});
describe('#scheduleNextFeedCheck()', function() {
it('schedules next feed check', function* () {
sinon.spy(Zotero.Feeds, 'scheduleNextFeedCheck');
sinon.spy(Zotero.Promise, 'delay');
yield clearFeeds();
let feed = yield createFeed({refreshInterval: 1});
feed._set('_feedLastCheck', Zotero.Date.dateToSQL(new Date(), true));
yield feed.saveTx();
yield Zotero.Feeds.scheduleNextFeedCheck();
assert.equal(Zotero.Promise.delay.args[0][0], 1000*60*60);
Zotero.Feeds.scheduleNextFeedCheck.restore();
Zotero.Promise.delay.restore();
});
})
})