Update zotero:// extensions (report, timeline, etc.) for async DB, and more

- Protocol handler extensions can now handle promises and can also make
  data available as it's ready instead of all at once (e.g., reports now
  output one entry at a time)
- zotero:// URL syntaxes are now more consistent and closer to the web
  API (old URLs should work, but some may currently be broken)

Also:

- Code to generate server API, currently available for testing via
  zotero://data URLs but eventually moving to HTTP -- zotero://data URLs match
  web API URLs, with a different prefix for the personal library (/library vs.
  /users/12345)
- Miscellaneous fixes to data objects

Under the hood:

- Extensions now return an AsyncChannel, which is an nsIChannel implementation
  that takes a promise-yielding generator that returns a string,
  nsIAsyncInputStream, or file that will be used for the channel's data
- New function Zotero.Utilities.Internal.getAsyncInputStream() takes a
  generator that yields either promises or strings and returns an async input
  stream filled with the yielded strings
- Zotero.Router parsers URLs and extract parameters
- Zotero.Item.toResponseJSON()
This commit is contained in:
Dan Stillman 2014-09-08 16:51:05 -04:00
parent c5ee3651fe
commit 755ead2119
26 changed files with 1731 additions and 900 deletions

View file

@ -44,7 +44,11 @@ var ZoteroAdvancedSearch = new function() {
_searchBox.onLibraryChange = this.onLibraryChange; _searchBox.onLibraryChange = this.onLibraryChange;
var io = window.arguments[0]; var io = window.arguments[0];
_searchBox.search = io.dataIn.search;
io.dataIn.search.loadPrimaryData()
.then(function () {
_searchBox.search = io.dataIn.search;
});
} }
@ -62,6 +66,7 @@ var ZoteroAdvancedSearch = new function() {
// Hack to create a condition for the search's library -- // Hack to create a condition for the search's library --
// this logic should really go in the search itself instead of here // this logic should really go in the search itself instead of here
// and in collectionTreeView.js // and in collectionTreeView.js
yield search.loadPrimaryData();
var conditions = search.getSearchConditions(); var conditions = search.getSearchConditions();
if (!conditions.some(function (condition) condition.condition == 'libraryID')) { if (!conditions.some(function (condition) condition.condition == 'libraryID')) {
yield search.addCondition('libraryID', 'is', _searchBox.search.libraryID); yield search.addCondition('libraryID', 'is', _searchBox.search.libraryID);

View file

@ -176,8 +176,9 @@
if (this.onLibraryChange) { if (this.onLibraryChange) {
this.onLibraryChange(libraryID); this.onLibraryChange(libraryID);
} }
if (!this.searchRef.id) {
this.searchRef.libraryID = libraryID; this.searchRef.libraryID = libraryID;
}
]]></body> ]]></body>
</method> </method>

View file

@ -25,47 +25,45 @@
var Zotero_Report_Interface = new function() { var Zotero_Report_Interface = new function() {
this.loadCollectionReport = loadCollectionReport;
this.loadItemReport = loadItemReport;
this.loadItemReportByIds = loadItemReportByIds;
/* /*
* Load a report for the currently selected collection * Load a report for the currently selected collection
*/ */
function loadCollectionReport(event) { this.loadCollectionReport = function (event) {
var queryString = '';
var col = ZoteroPane_Local.getSelectedCollection();
var sortColumn = ZoteroPane_Local.getSortField(); var sortColumn = ZoteroPane_Local.getSortField();
var sortDirection = ZoteroPane_Local.getSortDirection(); var sortDirection = ZoteroPane_Local.getSortDirection();
if (sortColumn != 'title' || sortDirection != 'ascending') { var queryString = '?sort=' + sortColumn
queryString = '?sort=' + sortColumn + (sortDirection == 'ascending' ? '' : '/d'); + '&direction=' + (sortDirection == 'ascending' ? 'asc' : 'desc');
var url = 'zotero://report/';
var source = ZoteroPane_Local.getSelectedCollection();
if (!source) {
source = ZoteroPane_Local.getSelectedSavedSearch();
}
if (!source) {
throw new Error('No collection currently selected');
} }
if (col) { url += Zotero.API.getLibraryPrefix(source.libraryID) + '/';
ZoteroPane_Local.loadURI('zotero://report/collection/'
+ Zotero.Collections.getLibraryKeyHash(col) if (source instanceof Zotero.Collection) {
+ '/html/report.html' + queryString, event); url += 'collections/' + source.key;
return; }
else {
url += 'searches/' + source.key;
} }
var s = ZoteroPane_Local.getSelectedSavedSearch(); url += '/items/report.html' + queryString;
if (s) {
ZoteroPane_Local.loadURI('zotero://report/search/'
+ Zotero.Searches.getLibraryKeyHash(s)
+ '/html/report.html' + queryString, event);
return;
}
throw ('No collection currently selected'); ZoteroPane_Local.loadURI(url, event);
} }
/* /*
* Load a report for the currently selected items * Load a report for the currently selected items
*/ */
function loadItemReport(event) { this.loadItemReport = function (event) {
var libraryID = ZoteroPane_Local.getSelectedLibraryID();
var items = ZoteroPane_Local.getSelectedItems(); var items = ZoteroPane_Local.getSelectedItems();
if (!items || !items.length) { if (!items || !items.length) {
@ -77,18 +75,8 @@ var Zotero_Report_Interface = new function() {
keyHashes.push(Zotero.Items.getLibraryKeyHash(item)); keyHashes.push(Zotero.Items.getLibraryKeyHash(item));
} }
ZoteroPane_Local.loadURI('zotero://report/items/' + keyHashes.join('-') + '/html/report.html', event); var url = 'zotero://report/' + Zotero.API.getLibraryPrefix(libraryID) + '/items/report.html'
} + '?itemKey=' + items.map(item => item.key).join(',');
ZoteroPane_Local.loadURI(url, event);
/*
* Load a report for the specified items
*/
function loadItemReportByIds(ids) {
if (!ids || !ids.length) {
throw ('No itemIDs provided to loadItemReportByIds()');
}
ZoteroPane_Local.loadURI('zotero://report/items/' + ids.join('-') + '/html/report.html');
} }
} }

View file

@ -30,25 +30,23 @@ var Zotero_Timeline_Interface = new function() {
*/ */
this.loadTimeline = function () { this.loadTimeline = function () {
var uri = 'zotero://timeline/'; var uri = 'zotero://timeline/';
var col = ZoteroPane_Local.getSelectedCollection(); var col = ZoteroPane_Local.getSelectedCollection();
if (col) { if (col) {
ZoteroPane_Local.loadURI(uri + 'collection/' + Zotero.Collections.getLibraryKeyHash(col)); uri += Zotero.API.getLibraryPrefix(col.libraryID) + '/collections/' + col.key;
return;
} }
else {
var s = ZoteroPane_Local.getSelectedSavedSearch(); var s = ZoteroPane_Local.getSelectedSavedSearch();
if (s) { if (s) {
ZoteroPane_Local.loadURI(uri + 'search/' + Zotero.Searches.getLibraryKeyHash(s)); uri += Zotero.API.getLibraryPrefix(s.libraryID) + '/searches/' + s.key;
return; }
else {
let libraryID = ZoteroPane_Local.getSelectedLibraryID();
if (libraryID) {
uri += Zotero.API.getLibraryPrefix(libraryID);
}
}
} }
var l = ZoteroPane_Local.getSelectedLibraryID();
if (l) {
ZoteroPane_Local.loadURI(uri + 'library/' + l);
return;
}
ZoteroPane_Local.loadURI(uri); ZoteroPane_Local.loadURI(uri);
} }
} }

View file

@ -0,0 +1,191 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2014 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
Zotero.API = {
parseParams: function (params) {
if (params.groupID) {
params.libraryID = Zotero.Groups.getLibraryIDFromGroupID(params.groupID);
}
if (typeof params.itemKey == 'string') {
params.itemKey = params.itemKey.split(',');
}
},
getResultsFromParams: Zotero.Promise.coroutine(function* (params) {
var results;
switch (params.scopeObject) {
case 'collections':
if (params.scopeObjectKey) {
var col = yield Zotero.Collections.getByLibraryAndKeyAsync(
params.libraryID, params.scopeObjectKey
);
}
else {
var col = yield Zotero.Collections.getAsync(params.scopeObjectID);
}
if (!col) {
throw new Error('Invalid collection ID or key');
}
yield col.loadChildItems();
results = col.getChildItems();
break;
case 'searches':
if (params.scopeObjectKey) {
var s = yield Zotero.Searches.getByLibraryAndKeyAsync(
params.libraryID, params.scopeObjectKey
);
}
else {
var s = yield Zotero.Searches.getAsync(params.scopeObjectID);
}
if (!s) {
throw new Error('Invalid search ID or key');
}
// FIXME: Hack to exclude group libraries for now
var s2 = new Zotero.Search();
s2.setScope(s);
var groups = Zotero.Groups.getAll();
for each(var group in groups) {
yield s2.addCondition('libraryID', 'isNot', group.libraryID);
}
var ids = yield s2.search();
break;
default:
if (params.scopeObject) {
throw new Error("Invalid scope object '" + params.scopeObject + "'");
}
if (params.itemKey) {
var s = new Zotero.Search;
yield s.addCondition('libraryID', 'is', params.libraryID);
yield s.addCondition('blockStart');
for (let i=0; i<params.itemKey.length; i++) {
let itemKey = params.itemKey[i];
yield s.addCondition('key', 'is', itemKey);
}
yield s.addCondition('blockEnd');
var ids = yield s.search();
}
else {
// Display all items
var s = new Zotero.Search();
yield s.addCondition('libraryID', 'is', params.libraryID);
yield s.addCondition('noChildren', 'true');
var ids = yield s.search();
}
}
if (results) {
// Filter results by item key
if (params.itemKey) {
results = results.filter(function (result) {
return params.itemKey.indexOf(result.key) !== -1;
});
}
}
else if (ids) {
// Filter results by item key
if (params.itemKey) {
ids = ids.filter(function (id) {
var [libraryID, key] = Zotero.Items.getLibraryAndKeyFromID(id);
return params.itemKey.indexOf(key) !== -1;
});
}
results = yield Zotero.Items.getAsync(ids);
}
return results;
}),
getLibraryPrefix: function (libraryID) {
return libraryID
? 'groups/' + Zotero.Groups.getGroupIDFromLibraryID(libraryID)
: 'library';
}
};
Zotero.API.Data = {
/**
* Parse a relative URI path and return parameters for the request
*/
parsePath: function (path) {
var params = {};
var router = new Zotero.Router(params);
// Top-level objects
router.add('library/:controller/top', function () {
params.libraryID = 0;
params.subset = 'top';
});
router.add('groups/:groupID/:controller/top', function () {
params.subset = 'top';
});
router.add('library/:scopeObject/:scopeObjectKey/items/:objectKey/:subset', function () {
params.libraryID = 0;
params.controller = 'items';
});
router.add('groups/:groupID/:scopeObject/:scopeObjectKey/items/:objectKey/:subset', function () {
params.controller = 'items';
});
// All objects
router.add('library/:controller', function () {
params.libraryID = 0;
});
router.add('groups/:groupID/:controller', function () {});
var parsed = router.run(path);
if (!parsed || !params.controller) {
throw new Zotero.Router.InvalidPathException(path);
}
if (params.groupID) {
params.libraryID = Zotero.Groups.getLibraryIDFromGroupID(params.groupID);
}
Zotero.Router.Utilities.convertControllerToObjectType(params);
return params;
},
getGenerator: function (path) {
var params = this.parsePath(path);
//Zotero.debug(params);
return Zotero.DataObjectUtilities.getClassForObjectType(params.objectType)
.apiDataGenerator(params);
}
};

View file

@ -121,8 +121,7 @@ Zotero.Collection.prototype.loadPrimaryData = Zotero.Promise.coroutine(function*
var key = this._key; var key = this._key;
var libraryID = this._libraryID; var libraryID = this._libraryID;
// Should be same as query in Zotero.Collections, just with collectionID var sql = Zotero.Collections.getPrimaryDataSQL();
var sql = Zotero.Collections._getPrimaryDataSQL();
if (id) { if (id) {
sql += " AND O.collectionID=?"; sql += " AND O.collectionID=?";
var params = id; var params = id;

View file

@ -204,7 +204,7 @@ Zotero.Collections = new function() {
} }
this._getPrimaryDataSQL = function () { this.getPrimaryDataSQL = function () {
// This should be the same as the query in Zotero.Collection.load(), // This should be the same as the query in Zotero.Collection.load(),
// just without a specific collectionID // just without a specific collectionID
return "SELECT " return "SELECT "

View file

@ -53,6 +53,12 @@ Zotero.DataObjectUtilities = {
return key; return key;
}, },
getObjectTypeSingular: function (objectTypePlural) {
return objectTypePlural.replace(/(s|es)$/, '');
},
"getObjectTypePlural": function getObjectTypePlural(objectType) { "getObjectTypePlural": function getObjectTypePlural(objectType) {
return objectType == 'search' ? 'searches' : objectType + 's'; return objectType == 'search' ? 'searches' : objectType + 's';
}, },

View file

@ -186,17 +186,38 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
}); });
/**
* @deprecated - use .libraryKey
*/
this.makeLibraryKeyHash = function (libraryID, key) { this.makeLibraryKeyHash = function (libraryID, key) {
Zotero.debug("WARNING: Zotero.DataObjects.makeLibraryKeyHash() is deprecated -- use obj.libraryKey instead");
return libraryID + '_' + key; return libraryID + '_' + key;
} }
/**
* @deprecated - use .libraryKey
*/
this.getLibraryKeyHash = function (obj) { this.getLibraryKeyHash = function (obj) {
Zotero.debug("WARNING: Zotero.DataObjects.getLibraryKeyHash() is deprecated -- use obj.libraryKey instead");
return this.makeLibraryKeyHash(obj.libraryID, obj.key); return this.makeLibraryKeyHash(obj.libraryID, obj.key);
} }
this.parseLibraryKey = function (libraryKey) {
var [libraryID, key] = libraryKey.split('/');
return {
libraryID: parseInt(libraryID),
key: key
};
}
/**
* @deprecated - Use Zotero.DataObjects.parseLibraryKey()
*/
this.parseLibraryKeyHash = function (libraryKey) { this.parseLibraryKeyHash = function (libraryKey) {
Zotero.debug("WARNING: Zotero.DataObjects.parseLibraryKeyHash() is deprecated -- use .parseLibraryKey() instead");
var [libraryID, key] = libraryKey.split('_'); var [libraryID, key] = libraryKey.split('_');
if (!key) { if (!key) {
return false; return false;
@ -254,7 +275,8 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
this.getIDFromLibraryAndKey = function (libraryID, key) { this.getIDFromLibraryAndKey = function (libraryID, key) {
return this._objectIDs[libraryID][key] ? this._objectIDs[libraryID][key] : false; return (this._objectIDs[libraryID] && this._objectIDs[libraryID][key])
? this._objectIDs[libraryID][key] : false;
} }
@ -531,8 +553,8 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
return loaded; return loaded;
} }
// _getPrimaryDataSQL() should use "O" for the primary table alias // getPrimaryDataSQL() should use "O" for the primary table alias
var sql = this._getPrimaryDataSQL(); var sql = this.getPrimaryDataSQL();
var params = []; var params = [];
if (libraryID !== false) { if (libraryID !== false) {
sql += ' AND O.libraryID=?'; sql += ' AND O.libraryID=?';

View file

@ -293,7 +293,7 @@ Zotero.Item.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (relo
if (!columns.length) { if (!columns.length) {
return; return;
} }
// This should match Zotero.Items._getPrimaryDataSQL(), but without // This should match Zotero.Items.getPrimaryDataSQL(), but without
// necessarily including all columns // necessarily including all columns
var sql = "SELECT " + columns.join(", ") + Zotero.Items.primaryDataSQLFrom; var sql = "SELECT " + columns.join(", ") + Zotero.Items.primaryDataSQLFrom;
if (id) { if (id) {
@ -923,9 +923,9 @@ Zotero.Item.prototype.getCreator = function (pos) {
* @param {Integer} pos * @param {Integer} pos
* @return {Object|Boolean} The API JSON creator data at the given position, or FALSE if none * @return {Object|Boolean} The API JSON creator data at the given position, or FALSE if none
*/ */
Zotero.Item.prototype.getCreatorsJSON = function (pos) { Zotero.Item.prototype.getCreatorJSON = function (pos) {
this._requireData('creators'); this._requireData('creators');
return this._creators[pos] ? Zotero.Creators.internalToAPIJSON(this._creators[pos]) : false; return this._creators[pos] ? Zotero.Creators.internalToJSON(this._creators[pos]) : false;
} }
@ -954,7 +954,7 @@ Zotero.Item.prototype.getCreators = function () {
*/ */
Zotero.Item.prototype.getCreatorsAPIData = function () { Zotero.Item.prototype.getCreatorsAPIData = function () {
this._requireData('creators'); this._requireData('creators');
return this._creators.map(function (data) Zotero.Creators.internalToAPIJSON(data)); return this._creators.map(function (data) Zotero.Creators.internalToJSON(data));
} }
@ -2075,7 +2075,7 @@ Zotero.Item.prototype.numAttachments = function(includeTrashed) {
/** /**
* Get an nsILocalFile for the attachment, or false for invalid paths * Get an nsILocalFile for the attachment, or false for invalid paths
* *
* This no longer checks whether a file exists * Note: This no longer checks whether a file exists
* *
* @return {nsILocalFile|false} An nsIFile, or false for invalid paths * @return {nsILocalFile|false} An nsIFile, or false for invalid paths
*/ */
@ -4150,12 +4150,8 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc
} }
var obj = {}; var obj = {};
if (options && options.includeKey) { obj.itemKey = this.key;
obj.itemKey = this.key; obj.itemVersion = this.version;
}
if (options && options.includeVersion) {
obj.itemVersion = this.version;
}
obj.itemType = Zotero.ItemTypes.getName(this.itemTypeID); obj.itemType = Zotero.ItemTypes.getName(this.itemTypeID);
// Fields // Fields
@ -4170,7 +4166,7 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc
// Creators // Creators
if (this.isRegularItem()) { if (this.isRegularItem()) {
yield this.loadCreators() yield this.loadCreators()
obj.creators = this.getCreators(); obj.creators = this.getCreatorsAPIData();
} }
else { else {
var parent = this.parentKey; var parent = this.parentKey;
@ -4200,14 +4196,8 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc
obj.tags = []; obj.tags = [];
yield this.loadTags() yield this.loadTags()
var tags = yield this.getTags(); var tags = yield this.getTags();
for each (let tag in tags) { for (let i=0; i<tags.length; i++) {
let tagObj = {}; obj.tags.push(tags[i]);
tagObj.tag = tag.name;
let type = tag.type;
if (type != 0 || mode == 'full') {
tagObj.type = tag.type;
}
obj.tags.push(tagObj);
} }
// Collections // Collections
@ -4248,10 +4238,8 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc
obj.deleted = deleted; obj.deleted = deleted;
} }
if (options && options.includeDate) { obj.dateAdded = Zotero.Date.sqlToISO8601(this.dateAdded);
obj.dateAdded = this.dateAdded; obj.dateModified = Zotero.Date.sqlToISO8601(this.dateModified);
obj.dateModified = this.dateModified;
}
if (mode == 'patch') { if (mode == 'patch') {
for (let i in patchBase) { for (let i in patchBase) {
@ -4277,6 +4265,34 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc
}); });
Zotero.Item.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options, patchBase) {
var json = {
key: this.key,
version: this.version,
meta: {},
data: yield this.toJSON(options, patchBase)
};
// TODO: library block?
// creatorSummary
var firstCreator = this.getField('firstCreator');
if (firstCreator) {
json.meta.creatorSummary = firstCreator;
}
// parsedDate
var parsedDate = Zotero.Date.multipartToSQL(this.getField('date', true, true));
if (parsedDate) {
// 0000?
json.meta.parsedDate = parsedDate;
}
// numChildren
if (this.isRegularItem()) {
json.meta.numChildren = this.numChildren();
}
return json;
})
////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////
// //

View file

@ -154,6 +154,67 @@ Zotero.Items = new function() {
}); });
/**
* Return item data in web API format
*
* var data = Zotero.Items.getAPIData(0, 'collections/NF3GJ38A/items');
*
* @param {Number} libraryID
* @param {String} [apiPath='items'] - Web API style
* @return {Promise<String>}.
*/
this.getAPIData = Zotero.Promise.coroutine(function* (libraryID, apiPath) {
var gen = this.getAPIDataGenerator(...arguments);
var data = "";
while (true) {
var result = gen.next();
if (result.done) {
break;
}
var val = yield result.value;
if (typeof val == 'string') {
data += val;
}
else if (val === undefined) {
continue;
}
else {
throw new Error("Invalid return value from generator");
}
}
return data;
});
/**
* Zotero.Utilities.Internal.getAsyncInputStream-compatible generator that yields item data
* in web API format as strings
*
* @param {Object} params - Request parameters from Zotero.API.parsePath()
*/
this.apiDataGenerator = function* (params) {
Zotero.debug(params);
var s = new Zotero.Search;
yield s.addCondition('libraryID', 'is', params.libraryID);
if (params.scopeObject == 'collections') {
yield s.addCondition('collection', 'is', params.libraryID + '/' + params.scopeObjectKey);
}
yield s.addCondition('title', 'contains', 'test');
var ids = yield s.search();
yield '[\n';
for (let i=0; i<ids.length; i++) {
let prefix = i > 0 ? ',\n' : '';
let item = yield this.getAsync(ids[i], { noCache: true });
var json = yield item.toResponseJSON();
yield prefix + JSON.stringify(json, null, 4);
}
yield '\n]';
};
/* /*
* Create a new item with optional metadata and pass back the primary reference * Create a new item with optional metadata and pass back the primary reference
* *
@ -621,8 +682,7 @@ Zotero.Items = new function() {
}); });
this._getPrimaryDataSQL = function () { this.getPrimaryDataSQL = function () {
// This should match Zotero.Item.loadPrimaryData, but with all possible columns
return "SELECT " return "SELECT "
+ Object.keys(this._primaryDataSQLParts).map((val) => this._primaryDataSQLParts[val]).join(', ') + Object.keys(this._primaryDataSQLParts).map((val) => this._primaryDataSQLParts[val]).join(', ')
+ this.primaryDataSQLFrom; + this.primaryDataSQLFrom;

View file

@ -845,7 +845,7 @@ Zotero.Tags = new function() {
} }
this._getPrimaryDataSQL = function () { this.getPrimaryDataSQL = function () {
// This should be the same as the query in Zotero.Tag.load(), // This should be the same as the query in Zotero.Tag.load(),
// just without a specific tagID // just without a specific tagID
return "SELECT * FROM tags O WHERE 1"; return "SELECT * FROM tags O WHERE 1";

View file

@ -544,6 +544,29 @@ Zotero.Date = new function(){
return false; return false;
} }
this.sqlToISO8601 = function (sqlDate) {
var date = sqlDate.substr(0, 10);
var matches = date.match(/^([0-9]{4})\-([0-9]{2})\-([0-9]{2})/);
if (!matches) {
return false;
}
date = matches[1];
// Drop parts for reduced precision
if (matches[2] !== "00") {
date += "-" + matches[2];
if (matches[3] !== "00") {
date += "-" + matches[3];
}
}
var time = sqlDate.substr(11);
// TODO: validate times
if (time) {
date += "T" + time + "Z";
}
return date;
}
function strToMultipart(str){ function strToMultipart(str){
if (!str){ if (!str){
return ''; return '';

View file

@ -189,7 +189,7 @@ Zotero.File = new function(){
* @param {nsIURI|nsIFile|string spec|string path|nsIChannel|nsIInputStream} source The source to read * @param {nsIURI|nsIFile|string spec|string path|nsIChannel|nsIInputStream} source The source to read
* @param {String} [charset] The character set; defaults to UTF-8 * @param {String} [charset] The character set; defaults to UTF-8
* @param {Integer} [maxLength] Maximum length to fetch, in bytes * @param {Integer} [maxLength] Maximum length to fetch, in bytes
* @return {Promise} A Q promise that is resolved with the contents of the file * @return {Promise} A promise that is resolved with the contents of the file
*/ */
this.getContentsAsync = function (source, charset, maxLength) { this.getContentsAsync = function (source, charset, maxLength) {
Zotero.debug("Getting contents of " + source); Zotero.debug("Getting contents of " + source);
@ -243,7 +243,7 @@ Zotero.File = new function(){
* *
* @param {nsIURI|nsIFile|string spec|nsIChannel|nsIInputStream} source The source to read * @param {nsIURI|nsIFile|string spec|nsIChannel|nsIInputStream} source The source to read
* @param {Integer} [maxLength] Maximum length to fetch, in bytes (unimplemented) * @param {Integer} [maxLength] Maximum length to fetch, in bytes (unimplemented)
* @return {Promise} A Q promise that is resolved with the contents of the source * @return {Promise} A promise that is resolved with the contents of the source
*/ */
this.getBinaryContentsAsync = function (source, maxLength) { this.getBinaryContentsAsync = function (source, maxLength) {
var deferred = Zotero.Promise.defer(); var deferred = Zotero.Promise.defer();

View file

@ -24,57 +24,54 @@
*/ */
Zotero.Report = new function() { Zotero.Report = {};
this.generateHTMLDetails = generateHTMLDetails;
this.generateHTMLList = generateHTMLList; Zotero.Report.HTML = new function () {
this.listGenerator = function* (items, combineChildItems) {
var escapeXML = function (str) { yield '<!DOCTYPE html>\n'
str = str.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/g, '\u2B1A'); + '<html>\n'
return Zotero.Utilities.htmlSpecialChars(str); + ' <head>\n'
} + ' <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n'
+ ' <title>' + Zotero.getString('report.title.default') + '</title>\n'
+ ' <link rel="stylesheet" type="text/css" href="zotero://report/detail.css"/>\n'
function generateHTMLDetails(items, combineChildItems) { + ' <link rel="stylesheet" type="text/css" media="screen,projection" href="zotero://report/detail_screen.css"/>\n'
var content = '<!DOCTYPE html>\n'; + ' <link rel="stylesheet" type="text/css" media="print" href="zotero://report/detail_print.css"/>\n'
content += '<html>\n'; + ' </head>\n'
content += '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n'; + ' <body>\n'
content += '<title>' + Zotero.getString('report.title.default') + '</title>\n'; + ' <ul class="report' + (combineChildItems ? ' combineChildItems' : '') + '">';
content += '<link rel="stylesheet" type="text/css" href="zotero://report/detail.css"/>\n';
content += '<link rel="stylesheet" type="text/css" media="screen,projection" href="zotero://report/detail_screen.css"/>\n';
content += '<link rel="stylesheet" type="text/css" media="print" href="zotero://report/detail_print.css"/>\n';
content += '</head>\n\n<body>\n';
content += '<ul class="report' + (combineChildItems ? ' combineChildItems' : '') + '">\n'; for (let i=0; i<items.length; i++) {
for each(var arr in items) { let obj = items[i];
content += '\n<li id="i' + arr.itemID + '" class="item ' + arr.itemType + '">\n';
if (arr.title) { let content = '\n\t\t\t<li id="item_' + obj.itemKey + '" class="item ' + obj.itemType + '">\n';
if (obj.title) {
// Top-level item matched search, so display title // Top-level item matched search, so display title
if (arr.reportSearchMatch) { if (obj.reportSearchMatch) {
content += '<h2>' + escapeXML(arr.title) + '</h2>\n'; content += '\t\t\t<h2>' + escapeXML(obj.title) + '</h2>\n';
} }
// Non-matching parent, so display "Parent Item: [Title]" // Non-matching parent, so display "Parent Item: [Title]"
else { else {
content += '<h2 class="parentItem">' + escapeXML(Zotero.getString('report.parentItem')) content += '\t\t\t<h2 class="parentItem">' + escapeXML(Zotero.getString('report.parentItem'))
+ ' <span class="title">' + escapeXML(arr.title) + '</span></h2>'; + ' <span class="title">' + escapeXML(obj.title) + '</span></h2>\n';
} }
} }
// If parent matches search, display parent item metadata table and tags // If parent matches search, display parent item metadata table and tags
if (arr.reportSearchMatch) { if (obj.reportSearchMatch) {
content += _generateMetadataTable(arr); content += _generateMetadataTable(obj);
content += _generateTagsList(arr); content += _generateTagsList(obj);
// Independent note // Independent note
if (arr['note']) { if (obj['note']) {
content += '\n'; content += '\n\t\t\t';
// If not valid XML, display notes with entities encoded // If not valid XML, display notes with entities encoded
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
.createInstance(Components.interfaces.nsIDOMParser); .createInstance(Components.interfaces.nsIDOMParser);
var doc = parser.parseFromString('<div>' var doc = parser.parseFromString('<div>'
+ arr.note + obj.note
// &nbsp; isn't valid in HTML // &nbsp; isn't valid in HTML
.replace(/&nbsp;/g, "&#160;") .replace(/&nbsp;/g, "&#160;")
// Strip control characters (for notes that were // Strip control characters (for notes that were
@ -83,26 +80,26 @@ Zotero.Report = new function() {
+ '</div>', "application/xml"); + '</div>', "application/xml");
if (doc.documentElement.tagName == 'parsererror') { if (doc.documentElement.tagName == 'parsererror') {
Zotero.debug(doc.documentElement.textContent, 2); Zotero.debug(doc.documentElement.textContent, 2);
content += '<p class="plaintext">' + escapeXML(arr.note) + '</p>\n'; content += '<p class="plaintext">' + escapeXML(obj.note) + '</p>\n';
} }
// Otherwise render markup normally // Otherwise render markup normally
else { else {
content += arr.note + '\n'; content += obj.note + '\n';
} }
} }
} }
// Children // Children
if (arr.reportChildren) { if (obj.reportChildren) {
// Child notes // Child notes
if (arr.reportChildren.notes.length) { if (obj.reportChildren.notes.length) {
// Only display "Notes:" header if parent matches search // Only display "Notes:" header if parent matches search
if (arr.reportSearchMatch) { if (obj.reportSearchMatch) {
content += '<h3 class="notes">' + escapeXML(Zotero.getString('report.notes')) + '</h3>\n'; content += '\t\t\t\t<h3 class="notes">' + escapeXML(Zotero.getString('report.notes')) + '</h3>\n';
} }
content += '<ul class="notes">\n'; content += '\t\t\t\t<ul class="notes">\n';
for each(var note in arr.reportChildren.notes) { for each(var note in obj.reportChildren.notes) {
content += '<li id="i' + note.itemID + '">\n'; content += '\t\t\t\t\t<li id="item_' + note.itemKey + '">\n';
// If not valid XML, display notes with entities encoded // If not valid XML, display notes with entities encoded
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
@ -126,114 +123,117 @@ Zotero.Report = new function() {
// Child note tags // Child note tags
content += _generateTagsList(note); content += _generateTagsList(note);
content += '</li>\n'; content += '\t\t\t\t\t</li>\n';
} }
content += '</ul>\n'; content += '\t\t\t\t</ul>\n';
} }
// Chid attachments // Chid attachments
content += _generateAttachmentsList(arr.reportChildren); content += _generateAttachmentsList(obj.reportChildren);
} }
// Related // Related items
if (arr.reportSearchMatch && arr.related && arr.related.length) { if (obj.reportSearchMatch && Zotero.Relations.relatedItemPredicate in obj.relations) {
content += '<h3 class="related">' + escapeXML(Zotero.getString('itemFields.related')) + '</h3>\n'; content += '\t\t\t\t<h3 class="related">' + escapeXML(Zotero.getString('itemFields.related')) + '</h3>\n';
content += '<ul class="related">\n'; content += '\t\t\t\t<ul class="related">\n';
var relateds = Zotero.Items.get(arr.related); var rels = obj.relations[Zotero.Relations.relatedItemPredicate];
for each(var related in relateds) { // TEMP
content += '<li id="i' + related.getID() + '">'; if (!Array.isArray(rels)) {
content += escapeXML(related.getDisplayTitle()); rels = [rels];
content += '</li>\n';
} }
content += '</ul>\n'; for (let i=0; i<rels.length; i++) {
let rel = rels[i];
let relItem = Zotero.URI.getURIItem(rel);
if (relItem) {
content += '\t\t\t\t\t<li id="item_' + relItem.key + '">';
content += escapeXML(relItem.getDisplayTitle());
content += '</li>\n';
}
}
content += '\t\t\t\t</ul>\n';
} }
content += '</li>\n\n'; content += '\t\t\t</li>\n\n';
yield content;
} }
content += '</ul>\n';
content += '</body>\n</html>';
return content; yield '\t\t</ul>\n\t</body>\n</html>';
} };
function generateHTMLList(items) { function _generateMetadataTable(obj) {
}
function _generateMetadataTable(arr) {
var table = false; var table = false;
var content = '<table>\n'; var content = '\t\t\t\t<table>\n';
// Item type // Item type
content += '<tr>\n'; content += '\t\t\t\t\t<tr>\n';
content += '<th>' content += '\t\t\t\t\t\t<th>'
+ escapeXML(Zotero.getString('itemFields.itemType')) + escapeXML(Zotero.getString('itemFields.itemType'))
+ '</th>\n'; + '</th>\n';
content += '<td>' + escapeXML(Zotero.ItemTypes.getLocalizedString(arr.itemType)) + '</td>\n'; content += '\t\t\t\t\t\t<td>' + escapeXML(Zotero.ItemTypes.getLocalizedString(obj.itemType)) + '</td>\n';
content += '</tr>\n'; content += '\t\t\t\t\t</tr>\n';
// Creators // Creators
if (arr['creators']) { if (obj['creators']) {
table = true; table = true;
var displayText; var displayText;
for each(var creator in arr['creators']) { for each(var creator in obj['creators']) {
// Two fields // One field
if (creator['fieldMode']==0) { if (creator.name !== undefined) {
displayText = creator['firstName'] + ' ' + creator['lastName']; displayText = creator.name;
}
// Single field
else if (creator['fieldMode']==1) {
displayText = creator['lastName'];
} }
// Two field
else { else {
// TODO displayText = (creator.firstName + ' ' + creator.lastName).trim();
} }
content += '<tr>\n'; content += '\t\t\t\t\t<tr>\n';
content += '<th class="' + creator.creatorType + '">' content += '\t\t\t\t\t\t<th class="' + creator.creatorType + '">'
+ escapeXML(Zotero.getString('creatorTypes.' + creator.creatorType)) + escapeXML(Zotero.getString('creatorTypes.' + creator.creatorType))
+ '</th>\n'; + '</th>\n';
content += '<td>' + escapeXML(displayText) + '</td>\n'; content += '\t\t\t\t\t\t<td>' + escapeXML(displayText) + '</td>\n';
content += '</tr>\n'; content += '\t\t\t\t\t</tr>\n';
} }
} }
// Move dateAdded and dateModified to the end of the array // Move dateAdded and dateModified to the end of the objay
var da = arr['dateAdded']; var da = obj['dateAdded'];
var dm = arr['dateModified']; var dm = obj['dateModified'];
delete arr['dateAdded']; delete obj['dateAdded'];
delete arr['dateModified']; delete obj['dateModified'];
arr['dateAdded'] = da; obj['dateAdded'] = da;
arr['dateModified'] = dm; obj['dateModified'] = dm;
for (var i in arr) { for (var i in obj) {
// Skip certain fields // Skip certain fields
switch (i) { switch (i) {
case 'reportSearchMatch': case 'reportSearchMatch':
case 'reportChildren': case 'reportChildren':
case 'libraryID': case 'itemKey':
case 'key': case 'itemVersion':
case 'itemType': case 'itemType':
case 'itemID':
case 'parentItemID':
case 'title': case 'title':
case 'firstCreator':
case 'creators': case 'creators':
case 'tags':
case 'related':
case 'notes':
case 'note': case 'note':
case 'attachments': case 'collections':
case 'relations':
case 'tags':
case 'deleted':
case 'parentItem':
case 'charset':
case 'contentType':
case 'linkMode':
case 'path':
continue; continue;
} }
try { try {
var localizedFieldName = Zotero.ItemFields.getLocalizedString(arr.itemType, i); var localizedFieldName = Zotero.ItemFields.getLocalizedString(obj.itemType, i);
} }
// Skip fields we don't have a localized string for // Skip fields we don't have a localized string for
catch (e) { catch (e) {
@ -241,79 +241,82 @@ Zotero.Report = new function() {
continue; continue;
} }
arr[i] = Zotero.Utilities.trim(arr[i] + ''); obj[i] = (obj[i] + '').trim();
// Skip empty fields // Skip empty fields
if (!arr[i]) { if (!obj[i]) {
continue; continue;
} }
table = true; table = true;
var fieldText; var fieldText;
if (i == 'url' && arr[i].match(/^https?:\/\//)) { if (i == 'url' && obj[i].match(/^https?:\/\//)) {
fieldText = '<a href="' + escapeXML(arr[i]) + '">' fieldText = '<a href="' + escapeXML(obj[i]) + '">' + escapeXML(obj[i]) + '</a>';
+ escapeXML(arr[i]) + '</a>';
} }
// Hyperlink DOI // Hyperlink DOI
else if (i == 'DOI') { else if (i == 'DOI') {
fieldText = '<a href="' + escapeXML('http://doi.org/' + arr[i]) + '">' fieldText = '<a href="' + escapeXML('http://doi.org/' + obj[i]) + '">'
+ escapeXML(arr[i]) + '</a>'; + escapeXML(obj[i]) + '</a>';
} }
// Remove SQL date from multipart dates // Remove SQL date from multipart dates
// (e.g. '2006-00-00 Summer 2006' becomes 'Summer 2006') // (e.g. '2006-00-00 Summer 2006' becomes 'Summer 2006')
else if (i=='date') { else if (i=='date') {
fieldText = escapeXML(Zotero.Date.multipartToStr(arr[i])); fieldText = escapeXML(Zotero.Date.multipartToStr(obj[i]));
} }
// Convert dates to local format // Convert dates to local format
else if (i=='accessDate' || i=='dateAdded' || i=='dateModified') { else if (i=='accessDate' || i=='dateAdded' || i=='dateModified') {
var date = Zotero.Date.sqlToDate(arr[i], true) var date = Zotero.Date.isoToDate(obj[i], true)
fieldText = escapeXML(date.toLocaleString()); fieldText = escapeXML(date.toLocaleString());
} }
else { else {
fieldText = escapeXML(arr[i]); fieldText = escapeXML(obj[i]);
} }
content += '<tr>\n<th>' + escapeXML(localizedFieldName) content += '\t\t\t\t\t<tr>\n\t\t\t\t\t<th>' + escapeXML(localizedFieldName)
+ '</th>\n<td>' + fieldText + '</td>\n</tr>\n'; + '</th>\n\t\t\t\t\t\t<td>' + fieldText + '</td>\n\t\t\t\t\t</tr>\n';
} }
content += '</table>'; content += '\t\t\t\t</table>\n';
return table ? content : ''; return table ? content : '';
} }
function _generateTagsList(arr) { function _generateTagsList(obj) {
var content = ''; var content = '';
if (arr['tags'] && arr['tags'].length) { if (obj.tags && obj.tags.length) {
var str = Zotero.getString('report.tags'); var str = Zotero.getString('report.tags');
content += '<h3 class="tags">' + escapeXML(str) + '</h3>\n'; content += '\t\t\t\t<h3 class="tags">' + escapeXML(str) + '</h3>\n';
content += '<ul class="tags">\n'; content += '\t\t\t\t<ul class="tags">\n';
for each(var tag in arr.tags) { for (let i=0; i<obj.tags.length; i++) {
content += '<li>' + escapeXML(tag.fields.name) + '</li>\n'; content += '\t\t\t\t\t<li>' + escapeXML(obj.tags[i].tag) + '</li>\n';
} }
content += '</ul>\n'; content += '\t\t\t\t</ul>\n';
} }
return content; return content;
} }
function _generateAttachmentsList(arr) { function _generateAttachmentsList(obj) {
var content = ''; var content = '';
if (arr.attachments && arr.attachments.length) { if (obj.attachments && obj.attachments.length) {
content += '<h3 class="attachments">' + escapeXML(Zotero.getString('itemFields.attachments')) + '</h3>\n'; content += '\t\t\t\t<h3 class="attachments">' + escapeXML(Zotero.getString('itemFields.attachments')) + '</h3>\n';
content += '<ul class="attachments">\n'; content += '\t\t\t\t<ul class="attachments">\n';
for each(var attachment in arr.attachments) { for (let i=0; i<obj.attachments.length; i++) {
content += '<li id="i' + attachment.itemID + '">'; let attachment = obj.attachments[i];
content += escapeXML(attachment.title);
content += '\t\t\t\t\t<li id="item_' + attachment.itemKey + '">';
if (attachment.title !== undefined) {
content += escapeXML(attachment.title);
}
// Attachment tags // Attachment tags
content += _generateTagsList(attachment); content += _generateTagsList(attachment);
// Attachment note // Attachment note
if (attachment.note) { if (attachment.note) {
content += '<div class="note">'; content += '\t\t\t\t\t\t<div class="note">';
if (attachment.note.substr(0, 1024).match(/<p[^>]*>/)) { if (attachment.note.substr(0, 1024).match(/<p[^>]*>/)) {
content += attachment.note + '\n'; content += attachment.note + '\n';
} }
@ -321,13 +324,19 @@ Zotero.Report = new function() {
else { else {
content += '<p class="plaintext">' + escapeXML(attachment.note) + '</p>\n'; content += '<p class="plaintext">' + escapeXML(attachment.note) + '</p>\n';
} }
content += '</div>'; content += '\t\t\t\t\t</div>';
} }
content += '</li>\n'; content += '\t\t\t\t\t</li>\n';
} }
content += '</ul>\n'; content += '\t\t\t\t</ul>\n';
} }
return content; return content;
} }
var escapeXML = function (str) {
str = str.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/g, '\u2B1A');
return Zotero.Utilities.htmlSpecialChars(str);
}
} }

View file

@ -0,0 +1,25 @@
Components.utils.import("resource://zotero/pathparser.js", Zotero);
Zotero.Router = Zotero.PathParser;
delete Zotero.PathParser;
Zotero.Router.Utilities = {
convertControllerToObjectType: function (params) {
if (params.controller !== undefined) {
params.objectType = Zotero.DataObjectUtilities.getObjectTypeSingular(params.controller);
delete params.controller;
}
}
};
Zotero.Router.InvalidPathException = function (path) {
this.path = path;
}
Zotero.Router.InvalidPathException.prototype = {
name: "InvalidPathException",
toString: function () {
return "Path '" + this.path + "' could not be parsed";
}
};

View file

@ -119,13 +119,17 @@ Zotero.Search.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (re
var libraryID = this._libraryID; var libraryID = this._libraryID;
var desc = id ? id : libraryID + "/" + key; var desc = id ? id : libraryID + "/" + key;
var sql = "SELECT * FROM savedSearches WHERE "; if (!id && !key) {
throw new Error('ID or key not set');
}
var sql = Zotero.Searches.getPrimaryDataSQL()
if (id) { if (id) {
sql += "savedSearchID=?"; sql += " AND savedSearchID=?";
var params = id; var params = id;
} }
else { else {
sql += "key=? AND libraryID=?"; sql += " AND key=? AND libraryID=?";
var params = [key, libraryID]; var params = [key, libraryID];
} }
var data = yield Zotero.DB.rowQueryAsync(sql, params); var data = yield Zotero.DB.rowQueryAsync(sql, params);
@ -323,7 +327,7 @@ Zotero.Search.prototype.clone = Zotero.Promise.coroutine(function* (libraryID) {
Zotero.Search.prototype.addCondition = Zotero.Promise.coroutine(function* (condition, operator, value, required) { Zotero.Search.prototype.addCondition = Zotero.Promise.coroutine(function* (condition, operator, value, required) {
yield this.loadPrimaryData(); this._requireData('conditions');
if (!Zotero.SearchConditions.hasOperator(condition, operator)){ if (!Zotero.SearchConditions.hasOperator(condition, operator)){
throw ("Invalid operator '" + operator + "' for condition " + condition); throw ("Invalid operator '" + operator + "' for condition " + condition);
@ -483,7 +487,7 @@ Zotero.Search.prototype.removeCondition = Zotero.Promise.coroutine(function* (se
* for the given searchConditionID * for the given searchConditionID
*/ */
Zotero.Search.prototype.getSearchCondition = function(searchConditionID){ Zotero.Search.prototype.getSearchCondition = function(searchConditionID){
this._requireData('primaryData'); this._requireData('conditions');
return this._conditions[searchConditionID]; return this._conditions[searchConditionID];
} }
@ -493,8 +497,7 @@ Zotero.Search.prototype.getSearchCondition = function(searchConditionID){
* used in the search, indexed by searchConditionID * used in the search, indexed by searchConditionID
*/ */
Zotero.Search.prototype.getSearchConditions = function(){ Zotero.Search.prototype.getSearchConditions = function(){
this._requireData('primaryData'); this._requireData('conditions');
var conditions = []; var conditions = [];
for (var id in this._conditions) { for (var id in this._conditions) {
var condition = this._conditions[id]; var condition = this._conditions[id];
@ -512,7 +515,7 @@ Zotero.Search.prototype.getSearchConditions = function(){
Zotero.Search.prototype.hasPostSearchFilter = function() { Zotero.Search.prototype.hasPostSearchFilter = function() {
this._requireData('primaryData'); this._requireData('conditions');
for each(var i in this._conditions){ for each(var i in this._conditions){
if (i.condition == 'fulltextContent'){ if (i.condition == 'fulltextContent'){
return true; return true;
@ -531,9 +534,15 @@ Zotero.Search.prototype.hasPostSearchFilter = function() {
Zotero.Search.prototype.search = Zotero.Promise.coroutine(function* (asTempTable) { Zotero.Search.prototype.search = Zotero.Promise.coroutine(function* (asTempTable) {
var tmpTable; var tmpTable;
if (this._identified) {
yield this.loadConditions();
}
// Mark conditions as loaded
else {
this._requireData('conditions');
}
try { try {
yield this.loadPrimaryData();
if (!this._sql){ if (!this._sql){
yield this._buildQuery(); yield this._buildQuery();
} }
@ -580,6 +589,11 @@ Zotero.Search.prototype.search = Zotero.Promise.coroutine(function* (asTempTable
// Run a subsearch to define the superset of possible results // Run a subsearch to define the superset of possible results
if (this._scope) { if (this._scope) {
if (this._scope._identified) {
yield this._scope.loadPrimaryData();
yield this._scope.loadConditions();
}
// If subsearch has post-search filter, run and insert ids into temp table // If subsearch has post-search filter, run and insert ids into temp table
if (this._scope.hasPostSearchFilter()) { if (this._scope.hasPostSearchFilter()) {
var ids = yield this._scope.search(); var ids = yield this._scope.search();
@ -863,6 +877,7 @@ Zotero.Search.prototype.serialize = function() {
*/ */
Zotero.Search.prototype.getSQL = Zotero.Promise.coroutine(function* () { Zotero.Search.prototype.getSQL = Zotero.Promise.coroutine(function* () {
if (!this._sql) { if (!this._sql) {
yield this.loadConditions();
yield this._buildQuery(); yield this._buildQuery();
} }
return this._sql; return this._sql;
@ -871,6 +886,7 @@ Zotero.Search.prototype.getSQL = Zotero.Promise.coroutine(function* () {
Zotero.Search.prototype.getSQLParams = Zotero.Promise.coroutine(function* () { Zotero.Search.prototype.getSQLParams = Zotero.Promise.coroutine(function* () {
if (!this._sql) { if (!this._sql) {
yield this.loadConditions();
yield this._buildQuery(); yield this._buildQuery();
} }
return this._sqlParams; return this._sqlParams;
@ -966,6 +982,8 @@ Zotero.Search.idsToTempTable = function (ids) {
* Build the SQL query for the search * Build the SQL query for the search
*/ */
Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () { Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () {
this._requireData('conditions');
var sql = 'SELECT itemID FROM items'; var sql = 'SELECT itemID FROM items';
var sqlParams = []; var sqlParams = [];
// Separate ANY conditions for 'required' condition support // Separate ANY conditions for 'required' condition support
@ -1171,14 +1189,16 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () {
if (condition.value) { if (condition.value) {
var lkh = Zotero.Collections.parseLibraryKeyHash(condition.value); var lkh = Zotero.Collections.parseLibraryKeyHash(condition.value);
if (lkh) { if (lkh) {
col = Zotero.Collections.getByLibraryAndKey(lkh.libraryID, lkh.key); col = yield Zotero.Collections.getByLibraryAndKeyAsync(lkh.libraryID, lkh.key);
} }
} }
if (!col) { if (!col) {
var msg = "Collection " + condition.value + " specified in saved search doesn't exist"; var msg = "Collection " + condition.value + " specified in saved search doesn't exist";
Zotero.debug(msg, 2); Zotero.debug(msg, 2);
Zotero.log(msg, 'warning', 'chrome://zotero/content/xpcom/search.js'); Zotero.log(msg, 'warning', 'chrome://zotero/content/xpcom/search.js');
continue; col = {
id: 0
};
} }
var q = ['?']; var q = ['?'];
@ -1633,6 +1653,22 @@ Zotero.Searches = new function(){
Zotero.DataObjects.apply(this, ['search', 'searches', 'savedSearch', 'savedSearches']); Zotero.DataObjects.apply(this, ['search', 'searches', 'savedSearch', 'savedSearches']);
this.constructor.prototype = new Zotero.DataObjects(); this.constructor.prototype = new Zotero.DataObjects();
Object.defineProperty(this, "_primaryDataSQLParts", {
get: function () {
return _primaryDataSQLParts ? _primaryDataSQLParts : (_primaryDataSQLParts = {
savedSearchID: "O.savedSearchID",
name: "O.savedSearchName",
libraryID: "O.libraryID",
key: "O.key",
version: "O.version",
synced: "O.synced"
});
}
});
var _primaryDataSQLParts;
this.init = Zotero.Promise.coroutine(function* () { this.init = Zotero.Promise.coroutine(function* () {
yield this.constructor.prototype.init.apply(this); yield this.constructor.prototype.init.apply(this);
@ -1661,7 +1697,7 @@ Zotero.Searches = new function(){
}); });
var searches = []; var searches = [];
for (i=0; i<rows.length; i++) { for (var i=0; i<rows.length; i++) {
let search = new Zotero.Search; let search = new Zotero.Search;
search.id = rows[i].id; search.id = rows[i].id;
yield search.loadPrimaryData(); yield search.loadPrimaryData();
@ -1697,10 +1733,12 @@ Zotero.Searches = new function(){
}); });
this._getPrimaryDataSQL = function () { this.getPrimaryDataSQL = function () {
// This should be the same as the query in Zotero.Search.loadPrimaryData(), // This should be the same as the query in Zotero.Search.loadPrimaryData(),
// just without a specific savedSearchID // just without a specific savedSearchID
return "SELECT O.* FROM savedSearches O WHERE 1"; return "SELECT "
+ Object.keys(this._primaryDataSQLParts).map(key => this._primaryDataSQLParts[key]).join(", ") + " "
+ "FROM savedSearches O WHERE 1";
} }
} }
@ -1715,8 +1753,8 @@ Zotero.SearchConditions = new function(){
this.parseCondition = parseCondition; this.parseCondition = parseCondition;
var _initialized = false; var _initialized = false;
var _conditions = {}; var _conditions;
var _standardConditions = []; var _standardConditions;
var self = this; var self = this;
@ -2180,6 +2218,7 @@ Zotero.SearchConditions = new function(){
]; ];
// Index conditions by name and aliases // Index conditions by name and aliases
_conditions = {};
for (var i in conditions) { for (var i in conditions) {
_conditions[conditions[i]['name']] = conditions[i]; _conditions[conditions[i]['name']] = conditions[i];
if (conditions[i]['aliases']) { if (conditions[i]['aliases']) {
@ -2271,6 +2310,10 @@ Zotero.SearchConditions = new function(){
function hasOperator(condition, operator){ function hasOperator(condition, operator){
var [condition, mode] = this.parseCondition(condition); var [condition, mode] = this.parseCondition(condition);
if (!_conditions) {
throw new Zotero.Exception.UnloadedDataException("Search conditions not yet loaded");
}
if (!_conditions[condition]){ if (!_conditions[condition]){
throw ("Invalid condition '" + condition + "' in hasOperator()"); throw ("Invalid condition '" + condition + "' in hasOperator()");
} }

View file

@ -24,32 +24,28 @@
*/ */
Zotero.Timeline = new function () { Zotero.Timeline = {
this.generateXMLDetails = generateXMLDetails; generateXMLDetails: function* (items, dateType) {
this.generateXMLList = generateXMLList;
function generateXMLDetails(items, dateType) {
var escapeXML = Zotero.Utilities.htmlSpecialChars; var escapeXML = Zotero.Utilities.htmlSpecialChars;
var content = '<data>\n'; yield '<data>\n';
for each(var item in items) { for (let i=0; i<items.length; i++) {
let item = items[i];
yield item.loadItemData();
var date = item.getField(dateType, true, true); var date = item.getField(dateType, true, true);
if (date) { if (date) {
var sqlDate = (dateType == 'date') ? Zotero.Date.multipartToSQL(date) : date; let sqlDate = (dateType == 'date') ? Zotero.Date.multipartToSQL(date) : date;
sqlDate = sqlDate.replace("00-00", "01-01"); sqlDate = sqlDate.replace("00-00", "01-01");
content += '<event start="' + Zotero.Date.sqlToDate(sqlDate) + '" '; let content = '<event start="' + Zotero.Date.sqlToDate(sqlDate) + '" ';
var title = item.getField('title'); let title = item.getField('title');
content += 'title=" ' + (title ? escapeXML(title) : '') + '" '; content += 'title=" ' + (title ? escapeXML(title) : '') + '" ';
content += 'icon="' + item.getImageSrc() + '" '; content += 'icon="' + item.getImageSrc() + '" ';
content += 'color="black">'; content += 'color="black">';
content += item.id; content += item.id;
content += '</event>\n'; content += '</event>\n';
yield content;
} }
} }
content += '</data>'; yield '</data>';
return content;
} }
};
function generateXMLList(items) {
}
}

View file

@ -0,0 +1,101 @@
/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2014 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
Zotero.Users = new function () {
var _userID;
var _libraryID;
var _username;
var _localUserKey;
this.init = Zotero.Promise.coroutine(function* () {
var sql = "SELECT value FROM settings WHERE setting='account' AND key='userID'";
_userID = yield Zotero.DB.valueQueryAsync(sql);
sql = "SELECT value FROM settings WHERE setting='account' AND key='libraryID'";
_libraryID = yield Zotero.DB.valueQueryAsync(sql);
sql = "SELECT value FROM settings WHERE setting='account' AND key='username'";
_username = yield Zotero.DB.valueQueryAsync(sql);
// If we don't have a global user id, generate a local user key
if (!_userID) {
sql = "SELECT value FROM settings WHERE setting='account' AND key='localUserKey'";
let key = yield Zotero.DB.valueQueryAsync(sql);
// Generate a local user key if we don't have one
if (!key) {
key = Zotero.randomString(8);
sql = "INSERT INTO settings VALUES ('account', 'localUserKey', ?)";
yield Zotero.DB.queryAsync(sql, key);
}
_localUserKey = key;
}
});
this.getCurrentUserID = function () {
return _userID;
};
this.setCurrentUserID = Zotero.Promise.coroutine(function* (val) {
val = parseInt(val);
var sql = "REPLACE INTO settings VALUES ('account', 'userID', ?)";
Zotero.DB.queryAsync(sql, val);
_userID = val;
});
this.getCurrentLibraryID = function () {
return _libraryID;
};
this.setCurrentLibraryID = Zotero.Promise.coroutine(function* (val) {
val = parseInt(val);
var sql = "REPLACE INTO settings VALUES ('account', 'libraryID', ?)";
Zotero.DB.queryAsync(sql, val);
_userID = val;
});
this.getCurrentUsername = function () {
return _username;
};
this.setCurrentUsername = Zotero.Promise.coroutine(function* (val) {
var sql = "REPLACE INTO settings VALUES ('account', 'username', ?)";
Zotero.DB.queryAsync(sql, val);
_userID = val;
});
this.getLocalUserKey = function () {
if (!_localUserKey) {
throw new Error("Local user key not available");
}
return _localUserKey;
};
};

View file

@ -368,6 +368,123 @@ Zotero.Utilities.Internal = {
} }
}, },
/**
* Return an input stream that will be filled asynchronously with strings yielded from a
* generator. If the generator yields a promise, the promise is waited for, but its value
* is not added to the input stream.
*
* @param {GeneratorFunction|Generator} gen - Promise-returning generator function or
* generator
* @return {nsIAsyncInputStream}
*/
getAsyncInputStream: function (gen, onError) {
const funcName = 'getAsyncInputStream';
const maxOutOfSequenceSeconds = 10;
const outOfSequenceDelay = 50;
// Initialize generator if necessary
var g = gen.next ? gen : gen();
var seq = 0;
var pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
pipe.init(true, true, 0, 0, null);
var os = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
.createInstance(Components.interfaces.nsIConverterOutputStream);
os.init(pipe.outputStream, 'utf-8', 0, 0x0000);
pipe.outputStream.asyncWait({
onOutputStreamReady: function (aos) {
Zotero.debug("Output stream is ready");
let currentSeq = seq++;
Zotero.spawn(function* () {
var lastVal;
var error = false;
while (true) {
var data;
try {
let result = g.next(lastVal);
if (result.done) {
Zotero.debug("No more data to write");
aos.close();
return;
}
// If a promise is yielded, wait for it and pass on its value
if (result.value.then) {
lastVal = yield result.value;
continue;
}
// Otherwise use the return value
data = result.value;
break;
}
catch (e) {
Zotero.debug(e, 1);
if (onError) {
error = e;
data = onError();
break;
}
Zotero.debug("Closing input stream");
aos.close();
throw e;
}
}
if (typeof data != 'string') {
throw new Error("Yielded value is not a string or promise in " + funcName
+ " ('" + data + "')");
}
// Make sure that we're writing to the stream in order, in case
// onOutputStreamReady is called again before the last promise completes.
// If not in order, wait a bit and try again.
var maxTries = Math.floor(maxOutOfSequenceSeconds * 1000 / outOfSequenceDelay);
while (currentSeq != seq - 1) {
if (maxTries <= 0) {
throw new Error("Next promise took too long to finish in " + funcName);
}
Zotero.debug("Promise finished out of sequence in " + funcName
+ "-- waiting " + outOfSequenceDelay + " ms");
yield Zotero.Promise.delay(outOfSequenceDelay);
maxTries--;
}
// Write to stream
Zotero.debug("Writing " + data.length + " characters");
os.writeString(data);
if (error) {
Zotero.debug("Closing input stream");
aos.close();
throw error;
}
Zotero.debug("Waiting to write more");
// Wait until stream is ready for more
aos.asyncWait(this, 0, 0, null);
}, this)
.catch(function (e) {
Zotero.debug("Error getting data for async stream", 1);
Components.utils.reportError(e);
Zotero.debug(e, 1);
os.close();
});
}
}, 0, 0, null);
return pipe.inputStream;
},
/** /**
* Defines property on the object's prototype. * Defines property on the object's prototype.
* More compact way to do Object.defineProperty * More compact way to do Object.defineProperty

View file

@ -43,7 +43,6 @@ var ZoteroPane = new function()
this.handleKeyUp = handleKeyUp; this.handleKeyUp = handleKeyUp;
this.setHighlightedRowsCallback = setHighlightedRowsCallback; this.setHighlightedRowsCallback = setHighlightedRowsCallback;
this.handleKeyPress = handleKeyPress; this.handleKeyPress = handleKeyPress;
this.editSelectedCollection = editSelectedCollection;
this.handleSearchKeypress = handleSearchKeypress; this.handleSearchKeypress = handleSearchKeypress;
this.handleSearchInput = handleSearchInput; this.handleSearchInput = handleSearchInput;
this.getSelectedCollection = getSelectedCollection; this.getSelectedCollection = getSelectedCollection;
@ -1781,8 +1780,7 @@ var ZoteroPane = new function()
}); });
function editSelectedCollection() this.editSelectedCollection = function () {
{
if (!this.canEdit()) { if (!this.canEdit()) {
this.displayCannotEditLibraryMessage(); this.displayCannotEditLibraryMessage();
return; return;
@ -1807,11 +1805,17 @@ var ZoteroPane = new function()
else { else {
var s = new Zotero.Search(); var s = new Zotero.Search();
s.id = row.ref.id; s.id = row.ref.id;
var io = {dataIn: {search: s, name: row.getName()}, dataOut: null}; s.loadPrimaryData()
window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io); .then(function () {
if (io.dataOut) { return s.loadConditions();
this.onCollectionSelected(); //reload itemsView })
} .then(function () {
var io = {dataIn: {search: s, name: row.getName()}, dataOut: null};
window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io);
if (io.dataOut) {
this.onCollectionSelected(); //reload itemsView
}
}.bind(this));
} }
} }
} }

View file

@ -69,7 +69,7 @@
Timeline.loadXML("zotero://timeline/data/", function(xml, url) { eventSource.loadXML(xml, url); }); Timeline.loadXML("zotero://timeline/data/", function(xml, url) { eventSource.loadXML(xml, url); });
setupFilterHighlightControls(document.getElementById("my-timeline-controls"), tl, [0,1,2], theme); setupFilterHighlightControls(document.getElementById("my-timeline-controls"), tl, [0,1,2], theme);
setupOtherControls(document.getElementById("my-other-controls"), tl, document.URL); setupOtherControls(document.getElementById("my-other-controls"), tl, document.location.search);
} }
function onResize() { function onResize() {

View file

@ -145,7 +145,7 @@ function checkDate(date) {
} }
} }
} }
function changeBand(path, queryString, band, intervals, selectedIndex) { function changeBand(queryString, band, intervals, selectedIndex) {
var values = new Array('d', 'm', 'y', 'e', 'c', 'i'); var values = new Array('d', 'm', 'y', 'e', 'c', 'i');
var newIntervals = ''; var newIntervals = '';
@ -158,7 +158,7 @@ function changeBand(path, queryString, band, intervals, selectedIndex) {
} }
} }
window.location = path + queryString + 'i=' + newIntervals; window.location.search = queryString + 'i=' + newIntervals;
} }
function createOption(t, selected) { function createOption(t, selected) {
@ -192,7 +192,7 @@ function getFull(a) {
} }
function createQueryString(theQueryValue, except, timeline) { function createQueryString(theQueryValue, except, timeline) {
var temp = '?'; var temp = '';
for(var i in theQueryValue) { for(var i in theQueryValue) {
if(except != i) { if(except != i) {
temp += i + '=' + theQueryValue[i] + '&'; temp += i + '=' + theQueryValue[i] + '&';
@ -208,16 +208,9 @@ function createQueryString(theQueryValue, except, timeline) {
return temp; return temp;
} }
function setupOtherControls(div, timeline, url) { function setupOtherControls(div, timeline, queryString) {
var table = document.createElement("table"); var table = document.createElement("table");
var [path, queryString] = url.split('?');
if(path == 'zotero://timeline') {
path += '/';
}
if(path =='zotero://timeline/') {
path += 'library';
}
var defaultQueryValue = new Object(); var defaultQueryValue = new Object();
defaultQueryValue['i'] = 'mye'; defaultQueryValue['i'] = 'mye';
defaultQueryValue['t'] = 'd'; defaultQueryValue['t'] = 'd';
@ -289,7 +282,7 @@ function setupOtherControls(div, timeline, url) {
select1.appendChild(createOption(options[i],(options[i] == selected))); select1.appendChild(createOption(options[i],(options[i] == selected)));
} }
select1.onchange = function () { select1.onchange = function () {
changeBand(path, createQueryString(theQueryValue, 'i', timeline), 0, intervals, table.rows[1].cells[1].firstChild.selectedIndex); changeBand(createQueryString(theQueryValue, 'i', timeline), 0, intervals, table.rows[1].cells[1].firstChild.selectedIndex);
}; };
td.appendChild(select1); td.appendChild(select1);
@ -301,7 +294,7 @@ function setupOtherControls(div, timeline, url) {
select2.appendChild(createOption(options[i],(options[i] == selected))); select2.appendChild(createOption(options[i],(options[i] == selected)));
} }
select2.onchange = function () { select2.onchange = function () {
changeBand(path, createQueryString(theQueryValue, 'i', timeline), 1, intervals, table.rows[1].cells[2].firstChild.selectedIndex); changeBand(createQueryString(theQueryValue, 'i', timeline), 1, intervals, table.rows[1].cells[2].firstChild.selectedIndex);
}; };
td.appendChild(select2); td.appendChild(select2);
@ -313,7 +306,7 @@ function setupOtherControls(div, timeline, url) {
select3.appendChild(createOption(options[i],(options[i] == selected))); select3.appendChild(createOption(options[i],(options[i] == selected)));
} }
select3.onchange = function () { select3.onchange = function () {
changeBand(path, createQueryString(theQueryValue, 'i', timeline), 2, intervals, table.rows[1].cells[3].firstChild.selectedIndex); changeBand(createQueryString(theQueryValue, 'i', timeline), 2, intervals, table.rows[1].cells[3].firstChild.selectedIndex);
}; };
td.appendChild(select3); td.appendChild(select3);
@ -327,7 +320,7 @@ function setupOtherControls(div, timeline, url) {
select4.appendChild(createOption(options[i],(values[i] == dateType))); select4.appendChild(createOption(options[i],(values[i] == dateType)));
} }
select4.onchange = function () { select4.onchange = function () {
window.location = path + createQueryString(theQueryValue, 't', timeline) + 't=' + values[table.rows[1].cells[4].firstChild.selectedIndex]; window.location.search = createQueryString(theQueryValue, 't', timeline) + 't=' + values[table.rows[1].cells[4].firstChild.selectedIndex];
}; };
td.appendChild(select4); td.appendChild(select4);
@ -335,7 +328,7 @@ function setupOtherControls(div, timeline, url) {
var fitToScreen = document.createElement("button"); var fitToScreen = document.createElement("button");
fitToScreen.innerHTML = getString("general.fitToScreen"); fitToScreen.innerHTML = getString("general.fitToScreen");
Timeline.DOM.registerEvent(fitToScreen, "click", function () { Timeline.DOM.registerEvent(fitToScreen, "click", function () {
window.location = path + createQueryString(theQueryValue, false, timeline); window.location.search = createQueryString(theQueryValue, false, timeline);
}); });
td.appendChild(fitToScreen); td.appendChild(fitToScreen);

File diff suppressed because it is too large Load diff

View file

@ -56,6 +56,7 @@ const xpcomFilesLocal = [
'libraryTreeView', 'libraryTreeView',
'collectionTreeView', 'collectionTreeView',
'annotate', 'annotate',
'api',
'attachments', 'attachments',
'cite', 'cite',
'cookieSandbox', 'cookieSandbox',
@ -89,6 +90,7 @@ const xpcomFilesLocal = [
'proxy', 'proxy',
'quickCopy', 'quickCopy',
'report', 'report',
'router',
'schema', 'schema',
'search', 'search',
'server', 'server',

119
resource/pathparser.js Normal file
View file

@ -0,0 +1,119 @@
/**
* pathparser.js - tiny URL parser/router
*
* Copyright (c) 2014 Dan Stillman
* License: MIT
* https://github.com/dstillman/pathparser.js
*/
(function (factory) {
// AMD/RequireJS
if (typeof define === 'function' && define.amd) {
define(factory);
// CommonJS/Node
} else if (typeof exports === 'object') {
module.exports = factory();
// Mozilla JSM
} else if (~String(this).indexOf('BackstagePass')) {
EXPORTED_SYMBOLS = ["PathParser"];
PathParser = factory();
// Browser global
} else {
PathParser = factory();
}
}(function () {
"use strict";
var PathParser = function (params) {
this.rules = [];
this.params = params;
}
PathParser.prototype = (function () {
function getParamsFromRule(rule, pathParts, queryParts) {
var params = {};
var missingParams = {};
// Parse path components
for (var i = 0; i < rule.parts.length; i++) {
var rulePart = rule.parts[i];
var part = pathParts[i];
if (part !== undefined) {
if (rulePart.charAt(0) == ':') {
params[rulePart.substr(1)] = part;
continue;
}
else if (rulePart !== part) {
return false;
}
}
else if (rulePart.charAt(0) != ':') {
return false;
}
else {
missingParams[rulePart.substr(1)] = true;
}
}
// Parse query strings
for (var i = 0; i < queryParts.length; ++i) {
var nameValue = queryParts[i].split('=', 2);
var key = nameValue[0];
// But ignore empty parameters and don't override named parameters
if (nameValue.length == 2 && !params[key] && !missingParams[key]) {
params[key] = nameValue[1];
}
}
return params;
}
return {
add: function (route, handler, autoPopulateOnMatch) {
this.rules.push({
parts: route.replace(/^\//, '').split('/'),
handler: handler,
autoPopulateOnMatch: autoPopulateOnMatch === undefined || autoPopulateOnMatch
});
},
run: function (url) {
if (url && url.length) {
url = url
// Remove redundant slashes
.replace(/\/+/g, '/')
// Strip leading and trailing '/' (at end or before query string)
.replace(/^\/|\/($|\?)/, '')
// Strip fragment identifiers
.replace(/#.*$/, '');
}
var urlSplit = url.split('?', 2);
var pathParts = urlSplit[0].split('/', 50);
var queryParts = urlSplit[1] ? urlSplit[1].split('&', 50) : [];
for (var i=0; i < this.rules.length; i++) {
var rule = this.rules[i];
var params = getParamsFromRule(rule, pathParts, queryParts);
if (params) {
params.url = url;
// Automatic parameter assignment
if (rule.autoPopulateOnMatch && this.params) {
for (var param in params) {
this.params[param] = params[param];
}
}
// Call handler with 'this' bound to parameter object
if (rule.handler) {
rule.handler.call(params);
}
return true;
}
}
return false;
}
};
})();
return PathParser;
}));