- 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
This commit is contained in:
Dan Stillman 2008-06-02 09:15:43 +00:00
parent cd93bf3927
commit 77133f465c
11 changed files with 611 additions and 230 deletions

View file

@ -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;
}
}

View file

@ -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) {

View file

@ -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()"/>

View file

@ -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()');
}

View file

@ -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();

View file

@ -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
//

View file

@ -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 (savedSearchID) {
this.load(savedSearchID);
if (this.id) {
if (!this._loaded) {
this.load();
}
}
else {
this._loaded = true;
}
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;
for (var i in conditions){
if (!data) {
return;
}
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);
Zotero.debug("Changing search id " + oldID + " to " + this.id);
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']);
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
}
var isNew = !this.id || !this.exists();
try {
var searchID = this.id ? this.id : Zotero.ID.get('savedSearches');
Zotero.debug("Saving " + (isNew ? 'new ' : '') + "search " + this.id);
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
];
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);
}
// 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 {
var isNew = true;
this._savedSearchID = Zotero.ID.get('savedSearches');
var sql = "INSERT INTO savedSearches (savedSearchID, savedSearchName) "
+ "VALUES (?,?)";
Zotero.DB.query(sql,
[this._savedSearchID, {string: this._savedSearchName}]);
Zotero.Notifier.trigger('modify', 'search', this.id, this._previousData);
}
// 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++;
}
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 = [
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
];
Zotero.DB.query(sql, sqlParams);
}
Zotero.DB.commitTransaction();
Zotero.Notifier.trigger(
(isNew ? 'add' : 'modify'), 'search', this._savedSearchID
);
return this._savedSearchID;
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);
}
}

View file

@ -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;
}
}

View file

@ -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');

View file

@ -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');

View file

@ -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