commit 77133f465c70af80416d297f30115f867201a329
parent cd93bf39274bdbba62dbc50b5e1088db2cea0eaf
Author: Dan Stillman <dstillman@zotero.org>
Date: Mon, 2 Jun 2008 09:15:43 +0000
- Saved search syncing, with automatic latest-wins conflict resolution
- Last sync time displayed in sync button tooltip
- Various and sundry bug fixes
DB must be re-upgraded from 1.0
Diffstat:
11 files changed, 605 insertions(+), 224 deletions(-)
diff --git a/chrome/content/zotero/fileInterface.js b/chrome/content/zotero/fileInterface.js
@@ -145,9 +145,8 @@ var Zotero_File_Interface = new function() {
// find name
var searchRef = ZoteroPane.getSelectedSavedSearch();
if(searchRef) {
- var search = new Zotero.Search();
- search.load(searchRef['id']);
- exporter.name = search.getName();
+ var search = new Zotero.Search(searchRef.id);
+ exporter.name = search.name;
}
}
exporter.save();
@@ -285,9 +284,8 @@ var Zotero_File_Interface = new function() {
} else {
var searchRef = ZoteroPane.getSelectedSavedSearch();
if(searchRef) {
- var search = new Zotero.Search();
- search.load(searchRef['id']);
- name = search.getName();
+ var search = new Zotero.Search(searchRef.id);
+ name = search.name;
}
}
diff --git a/chrome/content/zotero/overlay.js b/chrome/content/zotero/overlay.js
@@ -1133,8 +1133,7 @@ var ZoteroPane = new function()
}
}
else {
- var s = new Zotero.Search();
- s.load(row.ref.id);
+ var s = new Zotero.Search(row.ref.id);
var io = {dataIn: {search: s, name: row.getName()}, dataOut: null};
window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io);
if (io.dataOut) {
diff --git a/chrome/content/zotero/overlay.xul b/chrome/content/zotero/overlay.xul
@@ -302,7 +302,18 @@
<vbox id="zotero-item-pane" persist="width">
<toolbar align="right">
- <toolbarbutton tooltiptext="Sync with Zotero Server" image="chrome://zotero/skin/arrow_refresh.png" oncommand="Zotero.Sync.Server.sync()"/>
+ <toolbarbutton
+ id="zotero-tb-sync"
+ image="chrome://zotero/skin/arrow_refresh.png"
+ tooltip="_child"
+ oncommand="Zotero.Sync.Server.sync()">
+ <tooltip
+ onpopupshowing="this.firstChild.nextSibling.value = 'Last sync: ' + (Zotero.Sync.Server.lastLocalSyncTime ? new Date(Zotero.Sync.Server.lastLocalSyncTime * 1000).toLocaleString() : 'Not yet synced')"
+ noautohide="true"><!-- localize -->
+ <label value="Sync with Zotero Server"/>
+ <label id="zotero-last-sync-time"/>
+ </tooltip>
+ </toolbarbutton>
<toolbarseparator/>
<toolbarbutton id="zotero-tb-fullscreen" tooltiptext="&zotero.toolbar.fullscreen.tooltip;" oncommand="ZoteroPane.fullScreen();"/>
<toolbarbutton class="tabs-closebutton" oncommand="ZoteroPane.toggleDisplay()"/>
diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js
@@ -244,9 +244,8 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
break;
case 'search':
- var search = Zotero.Searches.get(ids);
this.reload();
- this.selection.select(this._searchRowMap[search.id]);
+ this.selection.select(this._searchRowMap[ids]);
break;
}
}
@@ -930,7 +929,7 @@ Zotero.ItemGroup.prototype.getSearchObject = function() {
var includeScopeChildren = false;
// Create/load the inner search
- var s = new Zotero.Search();
+ var s = new Zotero.Search(this.isSearch() ? this.ref.id : null);
if (this.isLibrary()) {
s.addCondition('noChildren', 'true');
includeScopeChildren = true;
@@ -943,10 +942,7 @@ Zotero.ItemGroup.prototype.getSearchObject = function() {
}
includeScopeChildren = true;
}
- else if (this.isSearch()){
- s.load(this.ref['id']);
- }
- else {
+ else if (!this.isSearch()) {
throw ('Invalid search mode in Zotero.ItemGroup.getSearchObject()');
}
diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js
@@ -26,13 +26,17 @@ Zotero.Collection = function(collectionID) {
this._init();
}
-Zotero.Collection.prototype._init = function (collectionID) {
+Zotero.Collection.prototype._init = function () {
// Public members for access by public methods -- do not access directly
this._name = null;
this._parent = null;
this._dateModified = null;
this._key = null;
+ this._loaded = false;
+ this._changed = false;
+ this._previousData = false;
+
this._hasChildCollections = false;
this._childCollections = [];
this._childCollectionsLoaded = false;
@@ -40,8 +44,6 @@ Zotero.Collection.prototype._init = function (collectionID) {
this._hasChildItems = false;
this._childItems = [];
this._childItemsLoaded = false;
-
- this._previousData = false;
}
@@ -122,7 +124,6 @@ Zotero.Collection.prototype.load = function() {
+ "(SELECT COUNT(*) FROM collectionItems WHERE "
+ "collectionID=C.collectionID)!=0 AS hasChildItems "
+ "FROM collections C WHERE collectionID=?";
-
var data = Zotero.DB.rowQuery(sql, this.id);
this._init();
diff --git a/chrome/content/zotero/xpcom/id.js b/chrome/content/zotero/xpcom/id.js
@@ -32,6 +32,11 @@ Zotero.ID = new function () {
* Gets an unused primary key id for a DB table
*/
function get(table, notNull, skip) {
+ // Used in sync.js
+ if (table == 'searches') {
+ table = 'savedSearches';
+ }
+
switch (table) {
// Autoincrement tables
//
diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js
@@ -20,62 +20,149 @@
***** END LICENSE BLOCK *****
*/
-Zotero.Search = function(savedSearchID){
+Zotero.Search = function(searchID) {
+ this._id = searchID ? searchID : null;
+ this._init();
+}
+
+
+Zotero.Search.prototype._init = function () {
+ // Public members for access by public methods -- do not access directly
+ this._name = null;
+ this._dateModified = null;
+ this._key = null;
+
+ this._loaded = false;
+ this._changed = false;
+ this._previousData = false;
+
this._scope = null;
this._scopeIncludeChildren = null;
this._sql = null;
this._sqlParams = null;
this._maxSearchConditionID = 0;
this._conditions = [];
- this._savedSearchID = null;
- this._savedSearchName = null;
this._hasPrimaryConditions = false;
+}
+
+
+
+Zotero.Search.prototype.getID = function(){
+ Zotero.debug('Zotero.Search.getName() is deprecated -- use Search.id');
+ return this._id;
+}
+
+Zotero.Search.prototype.getName = function() {
+ Zotero.debug('Zotero.Search.getName() is deprecated -- use Search.name');
+ return this.name;
+}
+
+Zotero.Search.prototype.setName = function(val) {
+ Zotero.debug('Zotero.Search.setName() is deprecated -- use Search.name');
+ this.name = val;
+}
+
+
+Zotero.Search.prototype.__defineGetter__('id', function () { return this._id; });
+
+Zotero.Search.prototype.__defineSetter__('id', function (val) { this._set('id', val); });
+Zotero.Search.prototype.__defineSetter__('searchID', function (val) { this._set('id', val); });
+Zotero.Search.prototype.__defineGetter__('name', function () { return this._get('name'); });
+Zotero.Search.prototype.__defineSetter__('name', function (val) { this._set('name', val); });
+Zotero.Search.prototype.__defineGetter__('dateModified', function () { return this._get('dateModified'); });
+Zotero.Search.prototype.__defineSetter__('dateModified', function (val) { this._set('dateModified', val); });
+Zotero.Search.prototype.__defineGetter__('key', function () { return this._get('key'); });
+Zotero.Search.prototype.__defineSetter__('key', function (val) { this._set('key', val); });
+
+Zotero.Search.prototype.__defineGetter__('conditions', function (arr) { this.getSearchConditions(); });
+
+
+Zotero.Search.prototype._get = function (field) {
+ if (this.id && !this._loaded) {
+ this.load();
+ }
+ return this['_' + field];
+}
+
+
+Zotero.Search.prototype._set = function (field, val) {
+ switch (field) {
+ //case 'id': // set using constructor
+ case 'searchID':
+ throw ("Invalid field '" + field + "' in Zotero.Search.set()");
+ }
+
+ if (this.id) {
+ if (!this._loaded) {
+ this.load();
+ }
+ }
+ else {
+ this._loaded = true;
+ }
- if (savedSearchID) {
- this.load(savedSearchID);
+ if (this['_' + field] != val) {
+ this._prepFieldChange(field);
+
+ switch (field) {
+ default:
+ this['_' + field] = val;
+ }
}
}
-/*
- * Set the name for the saved search
+/**
+ * Check if saved search exists in the database
*
- * Must be called before save() for new searches
+ * @return bool TRUE if the search exists, FALSE if not
*/
-Zotero.Search.prototype.setName = function(name){
- if (!name){
- throw("Invalid saved search name '" + name + '"');
+Zotero.Search.prototype.exists = function() {
+ if (!this.id) {
+ throw ('searchID not set in Zotero.Search.exists()');
}
- this._savedSearchName = name;
+ var sql = "SELECT COUNT(*) FROM savedSearches WHERE savedSearchID=?";
+ return !!Zotero.DB.valueQuery(sql, this.id);
}
/*
* Load a saved search from the DB
*/
-Zotero.Search.prototype.load = function(savedSearchID){
- var sql = "SELECT savedSearchName, MAX(searchConditionID) AS maxID "
- + "FROM savedSearches LEFT JOIN savedSearchConditions "
- + "USING (savedSearchID) WHERE savedSearchID=" + savedSearchID
- + " GROUP BY savedSearchID";
- var row = Zotero.DB.rowQuery(sql);
-
- if (!row){
- throw('Saved search ' + savedSearchID + ' does not exist');
+Zotero.Search.prototype.load = function() {
+ // Changed in 1.5
+ if (arguments[0]) {
+ throw ('Parameter no longer allowed in Zotero.Search.load()');
}
- this._sql = null;
- this._sqlParams = null;
- this._maxSearchConditionID = row['maxID'];
- this._conditions = [];
- this._savedSearchID = savedSearchID;
- this._savedSearchName = row['savedSearchName'];
+ var sql = "SELECT S.*, "
+ + "MAX(searchConditionID) AS maxID "
+ + "FROM savedSearches S LEFT JOIN savedSearchConditions "
+ + "USING (savedSearchID) WHERE savedSearchID=? "
+ + "GROUP BY savedSearchID";
+ var data = Zotero.DB.rowQuery(sql, this.id);
- var conditions = Zotero.DB.query("SELECT * FROM savedSearchConditions "
- + "WHERE savedSearchID=" + savedSearchID + " ORDER BY searchConditionID");
+ this._init();
+ this._loaded = true;
+
+ if (!data) {
+ return;
+ }
- for (var i in conditions){
+ this._changed = false;
+ this._previousData = false;
+ this._id = data.savedSearchID;
+ this._name = data.savedSearchName;
+ this._dateModified = data.dateModified;
+ this._key = data.key;
+ this._maxSearchConditionID = data.maxID;
+
+ var sql = "SELECT * FROM savedSearchConditions "
+ + "WHERE savedSearchID=? ORDER BY searchConditionID";
+ var conditions = Zotero.DB.query(sql, this.id);
+
+ for (var i in conditions) {
// Parse "condition[/mode]"
var [condition, mode] =
Zotero.SearchConditions.parseCondition(conditions[i]['condition']);
@@ -98,21 +185,6 @@ Zotero.Search.prototype.load = function(savedSearchID){
}
-Zotero.Search.prototype.getID = function(){
- Zotero.debug('Zotero.Search.getName() is deprecated -- use Search.id');
- return this._savedSearchID;
-}
-
-Zotero.Search.prototype.__defineGetter__('id', function () { return this._savedSearchID; });
-
-
-Zotero.Search.prototype.getName = function() {
- Zotero.debug('Zotero.Search.getName() is deprecated -- use Search.name');
- return this._savedSearchName;
-}
-
-Zotero.Search.prototype.__defineGetter__('name', function () { return this._savedSearchName; });
-
/*
* Save the search to the DB and return a savedSearchID
*
@@ -120,75 +192,134 @@ Zotero.Search.prototype.__defineGetter__('name', function () { return this._save
* and the caller must dispose of the search or reload the condition ids,
* which may change after the save.
*
- * For new searches, setName() must be called before saving
+ * For new searches, name must be set called before saving
*/
Zotero.Search.prototype.save = function(fixGaps) {
- if (!this._savedSearchName){
+ if (!this.name) {
throw('Name not provided for saved search');
}
Zotero.DB.beginTransaction();
- if (this._savedSearchID){
- var sql = "UPDATE savedSearches SET savedSearchName=? WHERE savedSearchID=?";
- Zotero.DB.query(sql, [this._savedSearchName, this._savedSearchID]);
+ // ID change
+ if (this._changed.id) {
+ var oldID = this._previousData.primary.id;
+ var params = [this.id, oldID];
- Zotero.DB.query("DELETE FROM savedSearchConditions "
- + "WHERE savedSearchID=" + this._savedSearchID);
- }
- else {
- var isNew = true;
+ Zotero.debug("Changing search id " + oldID + " to " + this.id);
- this._savedSearchID = Zotero.ID.get('savedSearches');
+ var row = Zotero.DB.rowQuery("SELECT * FROM savedSearches WHERE savedSearchID=?", oldID);
+ // Add a new row so we can update the old rows despite FK checks
+ // Use temp key due to UNIQUE constraint on key column
+ Zotero.DB.query("INSERT INTO savedSearches VALUES (?, ?, ?, ?)",
+ [this.id, row.savedSearchName, row.dateModified, 'TEMPKEY']);
- var sql = "INSERT INTO savedSearches (savedSearchID, savedSearchName) "
- + "VALUES (?,?)";
- Zotero.DB.query(sql,
- [this._savedSearchID, {string: this._savedSearchName}]);
- }
-
- // Close gaps in savedSearchIDs
- var saveConditions = {};
- var i = 1;
- for (var id in this._conditions) {
- if (!fixGaps && id != i) {
- Zotero.DB.rollbackTransaction();
- throw ('searchConditionIDs not contiguous and |fixGaps| not set in save() of saved search ' + this._savedSearchID);
- }
- saveConditions[i] = this._conditions[id];
- i++;
+ Zotero.DB.query("UPDATE savedSearchConditions SET savedSearchID=? WHERE savedSearchID=?", params);
+
+ Zotero.DB.query("DELETE FROM savedSearches WHERE savedSearchID=?", oldID);
+ Zotero.DB.query("UPDATE savedSearches SET key=? WHERE savedSearchID=?", [row.key, this.id]);
+
+ //Zotero.Searches.unload(oldID);
+ Zotero.Notifier.trigger('id-change', 'search', oldID + '-' + this.id);
+
+ // update caches
}
- this._conditions = saveConditions;
+ var isNew = !this.id || !this.exists();
- // TODO: use proper bound parameters once DB class is updated
- for (var i in this._conditions){
- var sql = "INSERT INTO savedSearchConditions (savedSearchID, "
- + "searchConditionID, condition, operator, value, required) "
- + "VALUES (?,?,?,?,?,?)";
+ try {
+ var searchID = this.id ? this.id : Zotero.ID.get('savedSearches');
- // Convert condition and mode to "condition[/mode]"
- var condition = this._conditions[i]['mode'] ?
- this._conditions[i]['condition'] + '/' + this._conditions[i]['mode'] :
- this._conditions[i]['condition']
+ Zotero.debug("Saving " + (isNew ? 'new ' : '') + "search " + this.id);
- var sqlParams = [
- this._savedSearchID, i, condition,
- this._conditions[i]['operator']
- ? this._conditions[i]['operator'] : null,
- this._conditions[i]['value']
- ? this._conditions[i]['value'] : null,
- this._conditions[i]['required']
- ? 1 : null
+ var key = this.key ? this.key : this._generateKey();
+
+ var columns = [
+ 'savedSearchID', 'savedSearchName', 'dateModified', 'key'
+ ];
+ var placeholders = ['?', '?', '?', '?'];
+ var sqlValues = [
+ searchID ? { int: searchID } : null,
+ { string: this.name },
+ // If date modified hasn't changed, use current timestamp
+ this._changed.dateModified ?
+ this.dateModified : Zotero.DB.transactionDateTime,
+ key
];
- Zotero.DB.query(sql, sqlParams);
+
+ var sql = "REPLACE INTO savedSearches (" + columns.join(', ') + ") VALUES ("
+ + placeholders.join(', ') + ")";
+ var insertID = Zotero.DB.query(sql, sqlValues);
+ if (!searchID) {
+ searchID = insertID;
+ }
+
+ if (!isNew) {
+ var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?";
+ Zotero.DB.query(sql, this.id);
+ }
+
+ // Close gaps in savedSearchIDs
+ var saveConditions = {};
+ var i = 1;
+ for (var id in this._conditions) {
+ if (!fixGaps && id != i) {
+ Zotero.DB.rollbackTransaction();
+ throw ('searchConditionIDs not contiguous and |fixGaps| not set in save() of saved search ' + this._id);
+ }
+ saveConditions[i] = this._conditions[id];
+ i++;
+ }
+
+ this._conditions = saveConditions;
+
+ // TODO: use proper bound parameters once DB class is updated
+ for (var i in this._conditions){
+ var sql = "INSERT INTO savedSearchConditions (savedSearchID, "
+ + "searchConditionID, condition, operator, value, required) "
+ + "VALUES (?,?,?,?,?,?)";
+
+ // Convert condition and mode to "condition[/mode]"
+ var condition = this._conditions[i].mode ?
+ this._conditions[i].condition + '/' + this._conditions[i].mode :
+ this._conditions[i].condition
+
+ var sqlParams = [
+ searchID, i, condition,
+ this._conditions[i].operator
+ ? this._conditions[i].operator : null,
+ this._conditions[i].value
+ ? this._conditions[i].value : null,
+ this._conditions[i].required
+ ? 1 : null
+ ];
+ Zotero.DB.query(sql, sqlParams);
+ }
+
+ Zotero.DB.commitTransaction();
+ }
+ catch (e) {
+ Zotero.DB.rollbackTransaction();
+ throw (e);
}
- Zotero.DB.commitTransaction();
- Zotero.Notifier.trigger(
- (isNew ? 'add' : 'modify'), 'search', this._savedSearchID
- );
- return this._savedSearchID;
+ // If successful, set values in object
+ if (!this.id) {
+ this._id = searchID;
+ }
+
+ if (!this.key) {
+ this._key = key;
+ }
+
+ if (isNew) {
+ Zotero.Notifier.trigger('add', 'search', this.id);
+ }
+ else {
+ Zotero.Notifier.trigger('modify', 'search', this.id, this._previousData);
+ }
+
+ return this._id;
}
@@ -198,9 +329,9 @@ Zotero.Search.prototype.clone = function() {
var conditions = this.getSearchConditions();
for each(var condition in conditions) {
- var name = condition['mode'] ?
- condition['condition'] + '/' + condition['mode'] :
- condition['condition']
+ var name = condition.mode ?
+ condition.condition + '/' + condition.mode :
+ condition.condition
s.addCondition(name, condition.operator, condition.value,
condition.required);
@@ -210,7 +341,11 @@ Zotero.Search.prototype.clone = function() {
}
-Zotero.Search.prototype.addCondition = function(condition, operator, value, required){
+Zotero.Search.prototype.addCondition = function(condition, operator, value, required) {
+ if (this.id && !this._loaded) {
+ this.load();
+ }
+
if (!Zotero.SearchConditions.hasOperator(condition, operator)){
throw ("Invalid operator '" + operator + "' for condition " + condition);
}
@@ -271,6 +406,10 @@ Zotero.Search.prototype.setScope = function (searchObj, includeChildren) {
Zotero.Search.prototype.updateCondition = function(searchConditionID, condition, operator, value, required){
+ if (this.id && !this._loaded) {
+ this.load();
+ }
+
if (typeof this._conditions[searchConditionID] == 'undefined'){
throw ('Invalid searchConditionID ' + searchConditionID + ' in updateCondition()');
}
@@ -282,7 +421,7 @@ Zotero.Search.prototype.updateCondition = function(searchConditionID, condition,
var [condition, mode] = Zotero.SearchConditions.parseCondition(condition);
this._conditions[searchConditionID] = {
- id: searchConditionID,
+ id: parseInt(searchConditionID),
condition: condition,
mode: mode,
operator: operator,
@@ -296,6 +435,10 @@ Zotero.Search.prototype.updateCondition = function(searchConditionID, condition,
Zotero.Search.prototype.removeCondition = function(searchConditionID){
+ if (this.id && !this._loaded) {
+ this.load();
+ }
+
if (typeof this._conditions[searchConditionID] == 'undefined'){
throw ('Invalid searchConditionID ' + searchConditionID + ' in removeCondition()');
}
@@ -309,6 +452,9 @@ Zotero.Search.prototype.removeCondition = function(searchConditionID){
* for the given searchConditionID
*/
Zotero.Search.prototype.getSearchCondition = function(searchConditionID){
+ if (this.id && !this._loaded) {
+ this.load();
+ }
return this._conditions[searchConditionID];
}
@@ -318,6 +464,9 @@ Zotero.Search.prototype.getSearchCondition = function(searchConditionID){
* used in the search, indexed by searchConditionID
*/
Zotero.Search.prototype.getSearchConditions = function(){
+ if (this.id && !this._loaded) {
+ this.load();
+ }
var conditions = [];
var i = 1;
for each(var condition in this._conditions) {
@@ -336,6 +485,9 @@ Zotero.Search.prototype.getSearchConditions = function(){
Zotero.Search.prototype.hasPostSearchFilter = function() {
+ if (this.id && !this._loaded) {
+ this.load();
+ }
for each(var i in this._conditions){
if (i.condition == 'fulltextContent'){
return true;
@@ -349,6 +501,10 @@ Zotero.Search.prototype.hasPostSearchFilter = function() {
* Run the search and return an array of item ids for results
*/
Zotero.Search.prototype.search = function(asTempTable){
+ if (this.id && !this._loaded) {
+ this.load();
+ }
+
if (!this._sql){
this._buildQuery();
}
@@ -651,6 +807,20 @@ Zotero.Search.prototype.search = function(asTempTable){
}
+Zotero.Search.prototype.serialize = function() {
+ var obj = {
+ primary: {
+ id: this.id,
+ dateModified: this.dateModified,
+ key: this.key
+ },
+ name: this.name,
+ conditions: this.getSearchConditions()
+ };
+ return obj;
+}
+
+
/*
* Get the SQL string for the search
*/
@@ -670,6 +840,20 @@ Zotero.Search.prototype.getSQLParams = function(){
}
+Zotero.Search.prototype._prepFieldChange = function (field) {
+ if (!this._changed) {
+ this._changed = {};
+ }
+ this._changed[field] = true;
+
+ // Save a copy of the data before changing
+ // TODO: only save previous data if search exists
+ if (this.id && this.exists() && !this._previousData) {
+ this._previousData = this.serialize();
+ }
+}
+
+
/*
* Batch insert
*/
@@ -895,8 +1079,7 @@ Zotero.Search.prototype._buildQuery = function(){
condSQL += "NOT ";
}
condSQL += "IN (";
- var search = new Zotero.Search();
- search.load(condition['value']);
+ var search = new Zotero.Search(condition.value);
// Check if there are any post-search filters
var hasFilter = search.hasPostSearchFilter();
@@ -1292,16 +1475,32 @@ Zotero.Search.prototype._buildQuery = function(){
}
+Zotero.Search.prototype._generateKey = function () {
+ return Zotero.ID.getKey();
+}
+
+
+
Zotero.Searches = new function(){
this.get = get;
this.getAll = getAll;
+ this.getUpdated = getUpdated;
this.erase = erase;
- function get(id){
- var sql = "SELECT savedSearchID AS id, savedSearchName AS name "
- + "FROM savedSearches WHERE savedSearchID=?";
- return Zotero.DB.rowQuery(sql, [id]);
+ /**
+ * Retrieve a saved search
+ *
+ * @param int id savedSearchID
+ * @return object|bool Zotero.Search object,
+ * or false if it doesn't exist
+ */
+ function get(id) {
+ var sql = "SELECT COUNT(*) FROM savedSearches WHERE savedSearchID=?";
+ if (Zotero.DB.valueQuery(sql, id)) {
+ return new Zotero.Search(id);
+ }
+ return false;
}
@@ -1315,21 +1514,37 @@ Zotero.Searches = new function(){
}
+ function getUpdated(date) {
+ var sql = "SELECT savedSearchID FROM savedSearches";
+ if (date) {
+ sql += " WHERE dateModified>?";
+ return Zotero.DB.columnQuery(sql, Zotero.Date.dateToSQL(date, true));
+ }
+ return Zotero.DB.columnQuery(sql);
+ }
+
+
/*
* Delete a given saved search from the DB
*/
- function erase(savedSearchID){
- Zotero.DB.beginTransaction();
- var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID="
- + savedSearchID;
- Zotero.DB.query(sql);
+ function erase(ids) {
+ ids = Zotero.flattenArguments(ids);
+ var notifierData = {};
- var sql = "DELETE FROM savedSearches WHERE savedSearchID="
- + savedSearchID;
- Zotero.DB.query(sql);
+ Zotero.DB.beginTransaction();
+ for each(var id in ids) {
+ var search = new Zotero.Search(id);
+ notifierData[id] = { old: search.serialize() };
+
+ var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?";
+ Zotero.DB.query(sql, id);
+
+ var sql = "DELETE FROM savedSearches WHERE savedSearchID=?";
+ Zotero.DB.query(sql, id);
+ }
Zotero.DB.commitTransaction();
- Zotero.Notifier.trigger('delete', 'search', savedSearchID);
+ Zotero.Notifier.trigger('delete', 'search', ids, notifierData);
}
}
diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js
@@ -9,8 +9,26 @@ Zotero.Sync = new function() {
this.purgeDeletedObjects = purgeDeletedObjects;
this.removeFromDeleted = removeFromDeleted;
+ // Keep in sync with syncObjectTypes table
this.__defineGetter__('syncObjects', function () {
- return ['Creator', 'Item', 'Collection'];
+ return {
+ creator: {
+ singular: 'Creator',
+ plural: 'Creators'
+ },
+ item: {
+ singular: 'Item',
+ plural: 'Items'
+ },
+ collection: {
+ singular: 'Collection',
+ plural: 'Collections'
+ },
+ search: {
+ singular: 'Search',
+ plural: 'Searches'
+ }
+ };
});
default xml namespace = '';
@@ -47,7 +65,7 @@ Zotero.Sync = new function() {
}
- function getObjectTypeName(typeID) {
+ function getObjectTypeName(typeID, plural) {
if (!_typesLoaded) {
_loadObjectTypes();
}
@@ -64,10 +82,8 @@ Zotero.Sync = new function() {
uploadIDs.changed = {};
uploadIDs.deleted = {};
- for each(var Type in Zotero.Sync.syncObjects) {
- var Types = Type + 's'; // 'Items'
- var type = Type.toLowerCase(); // 'item'
- var types = type + 's'; // 'items'
+ for each(var syncObject in Zotero.Sync.syncObjects) {
+ var types = syncObject.plural.toLowerCase(); // 'items'
uploadIDs.updated[types] = [];
uploadIDs.changed[types] = {};
@@ -89,10 +105,9 @@ Zotero.Sync = new function() {
}
var updatedIDs = {};
- for each(var Type in this.syncObjects) {
- var Types = Type + 's'; // 'Items'
- var type = Type.toLowerCase(); // 'item'
- var types = type + 's'; // 'items'
+ for each(var syncObject in this.syncObjects) {
+ var Types = syncObject.plural; // 'Items'
+ var types = syncObject.plural.toLowerCase(); // 'items'
Zotero.debug("Getting updated local " + types);
@@ -156,12 +171,14 @@ Zotero.Sync = new function() {
}
var deletedIDs = {};
- for each(var Type in this.syncObjects) {
- deletedIDs[Type.toLowerCase() + 's'] = [];
+ for each(var syncObject in this.syncObjects) {
+ deletedIDs[syncObject.plural.toLowerCase()] = [];
}
for each(var row in rows) {
- deletedIDs[this.getObjectTypeName(row.syncObjectTypeID) + 's'].push({
+ var type = this.getObjectTypeName(row.syncObjectTypeID);
+ type = this.syncObjects[type].plural.toLowerCase()
+ deletedIDs[type].push({
id: row.objectID,
key: row.key
});
@@ -239,8 +256,7 @@ Zotero.Sync.EventListener = new function () {
* Blacklist objects from going into the sync delete log
*/
function ignoreDeletions(type, ids) {
- var cap = type[0].toUpperCase() + type.substr(1);
- if (Zotero.Sync.syncObjects.indexOf(cap) == -1) {
+ if (!Zotero.Sync.syncObjects[type]) {
throw ("Invalid type '" + type +
"' in Zotero.Sync.EventListener.ignoreDeletions()");
}
@@ -260,8 +276,7 @@ Zotero.Sync.EventListener = new function () {
* Remove objects blacklisted from the sync delete log
*/
function unignoreDeletions(type, ids) {
- var cap = type[0].toUpperCase() + type.substr(1);
- if (Zotero.Sync.syncObjects.indexOf(cap) == -1) {
+ if (!Zotero.Sync.syncObjects[type]) {
throw ("Invalid type '" + type +
"' in Zotero.Sync.EventListener.ignoreDeletions()");
}
@@ -521,9 +536,7 @@ Zotero.Sync.Server = new function () {
}
if (_syncInProgress) {
- Zotero.log("Sync operation already in progress", 'error');
- return;
-
+ _error("Sync operation already in progress");
}
_syncInProgress = true;
@@ -990,8 +1003,14 @@ Zotero.Sync.Server = new function () {
function _error(e) {
+ _syncInProgress = false;
_resetAttempts();
Zotero.DB.rollbackAllTransactions();
+
+ if (_sessionID && _sessionLock) {
+ Zotero.Sync.Server.unlock()
+ }
+
throw(e);
}
}
@@ -1047,6 +1066,10 @@ Zotero.Sync.Server.Data = new function() {
this.xmlToCollection = xmlToCollection;
this.creatorToXML = creatorToXML;
this.xmlToCreator = xmlToCreator;
+ this.searchToXML = searchToXML;
+ this.xmlToSearch = xmlToSearch;
+
+ var _noMergeTypes = ['search'];
default xml namespace = '';
@@ -1061,10 +1084,11 @@ Zotero.Sync.Server.Data = new function() {
Zotero.DB.beginTransaction();
- for each(var Type in Zotero.Sync.syncObjects) {
- var Types = Type + 's'; // 'Items'
+ for each(var syncObject in Zotero.Sync.syncObjects) {
+ var Type = syncObject.singular; // 'Item'
+ var Types = syncObject.plural; // 'Items'
var type = Type.toLowerCase(); // 'item'
- var types = type + 's'; // 'items'
+ var types = Types.toLowerCase(); // 'items'
if (!xml[types]) {
continue;
@@ -1092,48 +1116,61 @@ Zotero.Sync.Server.Data = new function() {
// Local object has been modified since last sync
if ((objDate > lastLocalSyncDate &&
- objDate < Zotero.Sync.Server.nextLocalSyncDate)
- // Check for object in updated array, since it might
- // have been modified during sync process, making its
- // date equal to Zotero.Sync.Server.nextLocalSyncDate
- // and therefore excluded above (example: an item
- // linked to a creator whose id changed)
- || uploadIDs.updated[types].indexOf(obj.id) != -1) {
+ objDate < Zotero.Sync.Server.nextLocalSyncDate)
+ // Check for object in updated array, since it might
+ // have been modified during sync process, making its
+ // date equal to Zotero.Sync.Server.nextLocalSyncDate
+ // and therefore excluded above (example: an item
+ // linked to a creator whose id changed)
+ || uploadIDs.updated[types].indexOf(obj.id) != -1) {
var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode);
- /*
- // For now, show item conflicts even if only
- // dateModified changed, since we need to handle
- // creator conflicts there
- if (type != 'item') {
- // Skip if only dateModified changed
- var diff = obj.diff(remoteObj, false, true);
- if (!diff) {
+ // Some types we don't bother to reconcile
+ if (_noMergeTypes.indexOf(type) != -1) {
+ if (obj.dateModified > remoteObj.dateModified) {
+ Zotero.Sync.addToUpdated(uploadIDs.updated.items, obj.id);
continue;
}
+ else {
+ obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode, obj);
+ }
}
- */
-
- // Will be handled by item CR for now
- if (type == 'creator') {
- remoteCreatorStore[remoteObj.id] = remoteObj;
+ // Mark other types for conflict resolution
+ else {
+ /*
+ // For now, show item conflicts even if only
+ // dateModified changed, since we need to handle
+ // creator conflicts there
+ if (type != 'item') {
+ // Skip if only dateModified changed
+ var diff = obj.diff(remoteObj, false, true);
+ if (!diff) {
+ continue;
+ }
+ }
+ */
+
+ // Will be handled by item CR for now
+ if (type == 'creator') {
+ remoteCreatorStore[remoteObj.id] = remoteObj;
+ continue;
+ }
+
+ if (type != 'item') {
+ alert('Reconciliation unimplemented for ' + types);
+ throw ('Reconciliation unimplemented for ' + types);
+ }
+
+ // TODO: order reconcile by parent/child?
+
+ toReconcile.push([
+ obj,
+ remoteObj
+ ]);
+
continue;
}
-
- if (type != 'item') {
- alert('Reconciliation unimplemented for ' + types);
- _error('Reconciliation unimplemented for ' + types);
- }
-
- // TODO: order reconcile by parent/child?
-
- toReconcile.push([
- obj,
- remoteObj
- ]);
-
- continue;
}
// Local object hasn't been modified -- overwrite
else {
@@ -1216,12 +1253,15 @@ Zotero.Sync.Server.Data = new function() {
continue;
}
- var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode);
+ // TODO: non-merged items
+
if (type != 'item') {
- alert('Reconciliation unimplemented for ' + types);
- _error('Reconciliation unimplemented for ' + types);
+ alert('Delete reconciliation unimplemented for ' + types);
+ _error('Delete reconciliation unimplemented for ' + types);
}
+ var remoteObj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode);
+
// TODO: order reconcile by parent/child?
toReconcile.push([
@@ -1229,7 +1269,7 @@ Zotero.Sync.Server.Data = new function() {
remoteObj
]);
- break typeloop;
+ continue typeloop;
}
// Create locally
@@ -1245,7 +1285,10 @@ Zotero.Sync.Server.Data = new function() {
}
}
+
+ //
// Handle deleted objects
+ //
if (xml.deleted && xml.deleted[types]) {
Zotero.debug("Processing remotely deleted " + types);
@@ -1275,7 +1318,9 @@ Zotero.Sync.Server.Data = new function() {
}
}
+ //
// Reconcile objects that have changed locally and remotely
+ //
if (toReconcile.length) {
var io = {
dataIn: {
@@ -1345,11 +1390,11 @@ Zotero.Sync.Server.Data = new function() {
}
}
+ // Sort collections in order of parent collections,
+ // so referenced parent collections always exist when saving
if (type == 'collection') {
var collections = [];
- // Sort collections in order of parent collections,
- // so referenced parent collections always exist when saving
var cmp = function (a, b) {
var pA = a.parent;
var pB = b.parent;
@@ -1421,8 +1466,10 @@ Zotero.Sync.Server.Data = new function() {
/**
* ids = {
- * items: [123, 234, 345, 456],
- * creators: [321, 432, 543, 654],
+ * updated: {
+ * items: [123, 234, 345, 456],
+ * creators: [321, 432, 543, 654]
+ * },
* changed: {
* items: {
* oldID: { oldID: 1234, newID: 5678 }, ...
@@ -1449,10 +1496,11 @@ Zotero.Sync.Server.Data = new function() {
// Updates
- for each(var Type in Zotero.Sync.syncObjects) {
- var Types = Type + 's'; // 'Items'
+ for each(var syncObject in Zotero.Sync.syncObjects) {
+ var Type = syncObject.singular; // 'Item'
+ var Types = syncObject.plural; // 'Items'
var type = Type.toLowerCase(); // 'item'
- var types = type + 's'; // 'items'
+ var types = Types.toLowerCase(); // 'items'
if (!ids.updated[types]) {
continue;
@@ -1462,7 +1510,7 @@ Zotero.Sync.Server.Data = new function() {
switch (type) {
// Items.get() can take multiple ids,
- // so we handle it differently
+ // so we handle them differently
case 'item':
var objs = Zotero[Types].get(ids.updated[types]);
for each(var obj in objs) {
@@ -1481,10 +1529,11 @@ Zotero.Sync.Server.Data = new function() {
// TODO: handle changed ids
// Deletions
- for each(var Type in Zotero.Sync.syncObjects) {
- var Types = Type + 's'; // 'Items'
+ for each(var syncObject in Zotero.Sync.syncObjects) {
+ var Type = syncObject.singular; // 'Item'
+ var Types = syncObject.plural; // 'Items'
var type = Type.toLowerCase(); // 'item'
- var types = type + 's'; // 'items'
+ var types = Types.toLowerCase(); // 'items'
if (!ids.deleted[types]) {
continue;
@@ -1849,4 +1898,106 @@ Zotero.Sync.Server.Data = new function() {
return creator;
}
+
+
+ function searchToXML(search) {
+ var xml = <search/>;
+
+ xml.@id = search.id;
+ xml.@name = search.name;
+ xml.@dateModified = search.dateModified;
+ xml.@key = search.key;
+
+ var conditions = search.getSearchConditions();
+ if (conditions) {
+ for each(var condition in conditions) {
+ var conditionXML = <condition/>
+ conditionXML.@id = condition.id;
+ conditionXML.@condition = condition.condition;
+ if (condition.mode) {
+ conditionXML.@mode = condition.mode;
+ }
+ conditionXML.@operator = condition.operator;
+ conditionXML.@value = condition.value;
+ if (condition.required) {
+ conditionXML.@required = 1;
+ }
+ xml.condition += conditionXML;
+ }
+ }
+
+ return xml;
+ }
+
+
+ /**
+ * Convert E4X <search> object into an unsaved Zotero.Search
+ *
+ * @param object xmlSearch E4X XML node with search data
+ * @param object item (Optional) Existing Zotero.Search to update
+ * @param bool newID (Optional) Ignore passed searchID and choose new one
+ */
+ function xmlToSearch(xmlSearch, search, newID) {
+ if (!search) {
+ if (newID) {
+ search = new Zotero.Search(null);
+ }
+ else {
+ search = new Zotero.Search(parseInt(xmlSearch.@id));
+ /*
+ if (search.exists()) {
+ throw ("Search specified in XML node already exists "
+ + "in Zotero.Sync.Server.Data.xmlToSearch()");
+ }
+ */
+ }
+ }
+ else if (newID) {
+ _error("Cannot use new id with existing search in "
+ + "Zotero.Sync.Server.Data.xmlToSearch()");
+ }
+
+ search.name = xmlSearch.@name.toString();
+ search.dateModified = xmlSearch.@dateModified.toString();
+ search.key = xmlSearch.@key.toString();
+
+ var conditionID = -1;
+
+ // Search conditions
+ for each(var condition in xmlSearch.condition) {
+ conditionID = parseInt(condition.@id);
+ var name = condition.@condition.toString();
+ var mode = condition.@mode.toString();
+ if (mode) {
+ name = name + '/' + mode;
+ }
+ if (search.getSearchCondition(conditionID)) {
+ search.updateCondition(
+ conditionID,
+ name,
+ condition.@operator.toString(),
+ condition.@value.toString(),
+ !!condition.@required.toString()
+ );
+ }
+ else {
+ var newID = search.addCondition(
+ name,
+ condition.@operator.toString(),
+ condition.@value.toString(),
+ !!condition.@required.toString()
+ );
+ if (newID != conditionID) {
+ throw ("Search condition ids not contiguous in Zotero.Sync.Server.xmlToSearch()");
+ }
+ }
+ }
+
+ conditionID++;
+ while (search.getSearchCondition(conditionID)) {
+ search.removeCondition(conditionID);
+ }
+
+ return search;
+ }
}
diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css
@@ -191,6 +191,11 @@
list-style-image: url('chrome://zotero/skin/toolbar-advanced-search.png');
}
+#zotero-tb-sync #zotero-last-sync-time
+{
+ color: gray;
+}
+
#zotero-tb-fullscreen
{
list-style-image: url('chrome://zotero/skin/toolbar-fullscreen-bottom.png');
diff --git a/system.sql b/system.sql
@@ -30,7 +30,7 @@ CREATE TABLE fields (
fieldID INTEGER PRIMARY KEY,
fieldName TEXT,
fieldFormatID INT,
- FOREIGN KEY (fieldFormatID) REFERENCES fieldFormat(fieldFormatID)
+ FOREIGN KEY (fieldFormatID) REFERENCES fieldFormats(fieldFormatID)
);
-- Defines valid fields for each itemType, their display order, and their default visibility
@@ -1248,4 +1248,4 @@ INSERT INTO "charsets" VALUES(168, 'x0212');
INSERT INTO "syncObjectTypes" VALUES(1, 'collection');
INSERT INTO "syncObjectTypes" VALUES(2, 'creator');
INSERT INTO "syncObjectTypes" VALUES(3, 'item');
-INSERT INTO "syncObjectTypes" VALUES(4, 'savedSearch');
+INSERT INTO "syncObjectTypes" VALUES(4, 'search');
diff --git a/triggers.sql b/triggers.sql
@@ -608,29 +608,29 @@ CREATE TRIGGER fkd_itemTags_tagID_tags_tagID
WHERE (SELECT COUNT(*) FROM itemTags WHERE tagID = OLD.tagID) > 0;
END;
--- savedSearchConditions/searchConditionID
-DROP TRIGGER IF EXISTS fki_savedSearchConditions_searchConditionID_savedSearches_savedSearchID;
-CREATE TRIGGER fki_savedSearchConditions_searchConditionID_savedSearches_savedSearchID
+-- savedSearchConditions/savedSearchID
+DROP TRIGGER IF EXISTS fki_savedSearchConditions_savedSearchID_savedSearches_savedSearchID;
+CREATE TRIGGER fki_savedSearchConditions_savedSearchID_savedSearches_savedSearchID
BEFORE INSERT ON savedSearchConditions
FOR EACH ROW BEGIN
- SELECT RAISE(ABORT, 'insert on table "savedSearchConditions" violates foreign key constraint "fki_savedSearchConditions_searchConditionID_savedSearches_savedSearchID"')
- WHERE NEW.searchConditionID IS NOT NULL AND (SELECT COUNT(*) FROM savedSearches WHERE savedSearchID = NEW.searchConditionID) = 0;
+ SELECT RAISE(ABORT, 'insert on table "savedSearchConditions" violates foreign key constraint "fki_savedSearchConditions_savedSearchID_savedSearches_savedSearchID"')
+ WHERE (SELECT COUNT(*) FROM savedSearches WHERE savedSearchID = NEW.savedSearchID) = 0;
END;
-DROP TRIGGER IF EXISTS fku_savedSearchConditions_searchConditionID_savedSearches_savedSearchID;
-CREATE TRIGGER fku_savedSearchConditions_searchConditionID_savedSearches_savedSearchID
- BEFORE UPDATE OF searchConditionID ON savedSearchConditions
+DROP TRIGGER IF EXISTS fku_savedSearchConditions_savedSearchID_savedSearches_savedSearchID;
+CREATE TRIGGER fku_savedSearchConditions_savedSearchID_savedSearches_savedSearchID
+ BEFORE UPDATE OF savedSearchID ON savedSearchConditions
FOR EACH ROW BEGIN
- SELECT RAISE(ABORT, 'update on table "savedSearchConditions" violates foreign key constraint "fku_savedSearchConditions_searchConditionID_savedSearches_savedSearchID"')
- WHERE NEW.searchConditionID IS NOT NULL AND (SELECT COUNT(*) FROM savedSearches WHERE savedSearchID = NEW.searchConditionID) = 0;
+ SELECT RAISE(ABORT, 'update on table "savedSearchConditions" violates foreign key constraint "fku_savedSearchConditions_savedSearchID_savedSearches_savedSearchID"')
+ WHERE (SELECT COUNT(*) FROM savedSearches WHERE savedSearchID = NEW.savedSearchID) = 0;
END;
-DROP TRIGGER IF EXISTS fkd_savedSearchConditions_searchConditionID_savedSearches_savedSearchID;
-CREATE TRIGGER fkd_savedSearchConditions_searchConditionID_savedSearches_savedSearchID
+DROP TRIGGER IF EXISTS fkd_savedSearchConditions_savedSearchID_savedSearches_savedSearchID;
+CREATE TRIGGER fkd_savedSearchConditions_savedSearchID_savedSearches_savedSearchID
BEFORE DELETE ON savedSearches
FOR EACH ROW BEGIN
- SELECT RAISE(ABORT, 'delete on table "savedSearches" violates foreign key constraint "fkd_savedSearchConditions_searchConditionID_savedSearches_savedSearchID"')
- WHERE (SELECT COUNT(*) FROM savedSearchConditions WHERE searchConditionID = OLD.savedSearchID) > 0;
+ SELECT RAISE(ABORT, 'delete on table "savedSearches" violates foreign key constraint "fkd_savedSearchConditions_savedSearchID_savedSearches_savedSearchID"')
+ WHERE (SELECT COUNT(*) FROM savedSearchConditions WHERE savedSearchID = OLD.savedSearchID) > 0;
END;
-- syncDeleteLog/syncObjectTypeID