Fix various saved search bugs, and add tests

Search condition ids are now indexed from 0, and always saved
contiguously (no more 'fixGaps' option), since they're just in an array
in the API. (They're still returned as an object from
Zotero.Search.prototype.getConditions() because it's easier for the
advanced search window to not have to deal with shifting ids between
saves.)
This commit is contained in:
Dan Stillman 2015-04-17 19:27:37 -04:00
parent d9c32a8e90
commit 4b040c78a7
6 changed files with 149 additions and 89 deletions

View file

@ -48,6 +48,12 @@ var ZoteroAdvancedSearch = new function() {
io.dataIn.search.loadPrimaryData()
.then(function () {
return Zotero.Groups.getAll();
})
.then(function (groups) {
// Since the search box can be used as a modal dialog, which can't use promises,
// it expects groups to be passed in.
_searchBox.groups = groups;
_searchBox.search = io.dataIn.search;
});
}

View file

@ -36,6 +36,8 @@
</resources>
<implementation>
<property name="groups"/>
<field name="searchRef"/>
<property name="search" onget="return this.searchRef;">
<setter>
@ -49,28 +51,25 @@
conditionsBox.removeChild(conditionsBox.firstChild);
var conditions = this.search.getConditions();
for(var id in conditions)
{
for (let id in conditions) {
let condition = conditions[id];
// Checkboxes
switch (conditions[id]['condition']) {
switch (condition.condition) {
case 'recursive':
case 'noChildren':
case 'includeParentsAndChildren':
var checkbox = conditions[id]['condition'] + 'Checkbox';
this.id(checkbox).setAttribute('condition',id);
this.id(checkbox).checked = (conditions[id]['operator']=='true');
let checkbox = condition.condition + 'Checkbox';
this.id(checkbox).setAttribute('condition', id);
this.id(checkbox).checked = condition.operator == 'true';
continue;
}
if(conditions[id]['condition'] == 'joinMode')
{
this.id('joinModeMenu').setAttribute('condition',id);
this.id('joinModeMenu').value = conditions[id]['operator'];
if(condition.condition == 'joinMode') {
this.id('joinModeMenu').setAttribute('condition', id);
this.id('joinModeMenu').value = condition.operator;
}
else
{
this.addCondition(conditions[id]);
else {
this.addCondition(condition);
}
}
]]>
@ -96,9 +95,8 @@
menupopup.appendChild(menuitem);
// Add groups
var groups = Zotero.Groups.getAll();
for (let i=0; i<groups.length; i++) {
let group = groups[i];
for (let i = 0; i < this.groups.length; i++) {
let group = this.groups[i];
let menuitem = document.createElement('menuitem');
menuitem.setAttribute('label', group.name);
menuitem.setAttribute('libraryID', group.libraryID);
@ -232,7 +230,7 @@
<body>
<![CDATA[
this.updateSearch();
return this.search.save({fixGaps: true});
return this.search.save();
]]>
</body>
</method>

View file

@ -35,7 +35,9 @@ function doLoad()
io = window.arguments[0];
document.getElementById('search-box').search = io.dataIn.search;
var searchBox = document.getElementById('search-box');
searchBox.groups = io.dataIn.groups;
searchBox.search = io.dataIn.search;
document.getElementById('search-name').value = io.dataIn.name;
}

View file

@ -32,8 +32,8 @@ Zotero.Search = function() {
this._scopeIncludeChildren = null;
this._sql = null;
this._sqlParams = false;
this._maxSearchConditionID = 0;
this._conditions = [];
this._maxSearchConditionID = -1;
this._conditions = {};
this._hasPrimaryConditions = false;
}
@ -179,7 +179,6 @@ Zotero.Search.prototype._initSave = Zotero.Promise.coroutine(function* (env) {
});
Zotero.Search.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
var fixGaps = env.options.fixGaps;
var isNew = env.isNew;
var searchID = env.id = this._id = this.id ? this.id : yield Zotero.ID.get('savedSearches');
@ -217,39 +216,28 @@ Zotero.Search.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
yield Zotero.DB.queryAsync(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;
for (var i in this._conditions){
var sql = "INSERT INTO savedSearchConditions (savedSearchID, "
+ "searchConditionID, condition, operator, value, required) "
+ "VALUES (?,?,?,?,?,?)";
var i = 0;
var sql = "INSERT INTO savedSearchConditions "
+ "(savedSearchID, searchConditionID, condition, operator, value, required) "
+ "VALUES (?,?,?,?,?,?)";
for (let id in this._conditions) {
let condition = this._conditions[id];
// 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
let conditionString = condition.mode ?
condition.condition + '/' + condition.mode :
condition.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
conditionString,
condition.operator ? condition.operator : null,
condition.value ? condition.value : null,
condition.required ? 1 : null
];
yield Zotero.DB.queryAsync(sql, sqlParams);
i++;
}
});
@ -274,6 +262,7 @@ Zotero.Search.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env)
return id;
}
yield this.loadPrimaryData(true);
yield this.reload();
this._clearChanged();
@ -400,11 +389,13 @@ Zotero.Search.prototype.addCondition = function (condition, operator, value, req
mode: mode,
operator: operator,
value: value,
required: required
required: !!required
};
this._sql = null;
this._sqlParams = false;
this._markFieldChange('conditions', this._conditions);
this._changed.conditions = true;
return searchConditionID;
}
@ -426,8 +417,8 @@ Zotero.Search.prototype.setScope = function (searchObj, includeChildren) {
* @param {Boolean} [required]
* @return {Promise}
*/
Zotero.Search.prototype.updateCondition = Zotero.Promise.coroutine(function* (searchConditionID, condition, operator, value, required){
yield this.loadPrimaryData();
Zotero.Search.prototype.updateCondition = function (searchConditionID, condition, operator, value, required) {
this._requireData('conditions');
if (typeof this._conditions[searchConditionID] == 'undefined'){
throw new Error('Invalid searchConditionID ' + searchConditionID);
@ -447,23 +438,27 @@ Zotero.Search.prototype.updateCondition = Zotero.Promise.coroutine(function* (se
mode: mode,
operator: operator,
value: value,
required: required
required: !!required
};
this._sql = null;
this._sqlParams = false;
});
this._markFieldChange('conditions', this._conditions);
this._changed.conditions = true;
}
Zotero.Search.prototype.removeCondition = Zotero.Promise.coroutine(function* (searchConditionID){
yield this.loadPrimaryData();
Zotero.Search.prototype.removeCondition = function (searchConditionID) {
this._requireData('conditions');
if (typeof this._conditions[searchConditionID] == 'undefined'){
throw ('Invalid searchConditionID ' + searchConditionID + ' in removeCondition()');
}
delete this._conditions[searchConditionID];
});
this._markFieldChange('conditions', this._conditions);
this._changed.conditions = true;
}
/*
@ -896,38 +891,37 @@ Zotero.Search.prototype.loadConditions = Zotero.Promise.coroutine(function* (rel
this._maxSearchConditionID = conditions[conditions.length - 1].searchConditionID;
}
// Reindex conditions, in case they're not contiguous in the DB
var conditionID = 1;
this._conditions = {};
// Reindex conditions, in case they're not contiguous in the DB
for (let i=0; i<conditions.length; i++) {
// Parse "condition[/mode]"
var [condition, mode] =
Zotero.SearchConditions.parseCondition(conditions[i]['condition']);
let condition = conditions[i];
var cond = Zotero.SearchConditions.get(condition);
// Parse "condition[/mode]"
let [conditionName, mode] = Zotero.SearchConditions.parseCondition(condition.condition);
let cond = Zotero.SearchConditions.get(conditionName);
if (!cond || cond.noLoad) {
Zotero.debug("Invalid saved search condition '" + condition + "' -- skipping", 2);
Zotero.debug("Invalid saved search condition '" + conditionName + "' -- skipping", 2);
continue;
}
// Convert itemTypeID to itemType
//
// TEMP: This can be removed at some point
if (condition == 'itemTypeID') {
condition = 'itemType';
conditions[i].value = Zotero.ItemTypes.getName(conditions[i].value);
if (conditionName == 'itemTypeID') {
conditionName = 'itemType';
condition.value = Zotero.ItemTypes.getName(condition.value);
}
this._conditions[conditionID] = {
id: conditionID,
condition: condition,
this._conditions[i] = {
id: i,
condition: conditionName,
mode: mode,
operator: conditions[i].operator,
value: conditions[i].value,
required: conditions[i].required
operator: condition.operator,
value: condition.value,
required: !!condition.required
};
conditionID++;
}
this._loaded.conditions = true;

View file

@ -1809,7 +1809,7 @@ var ZoteroPane = new function()
});
this.editSelectedCollection = function () {
this.editSelectedCollection = Zotero.Promise.coroutine(function* () {
if (!this.canEdit()) {
this.displayCannotEditLibraryMessage();
return;
@ -1832,22 +1832,32 @@ var ZoteroPane = new function()
}
}
else {
var s = new Zotero.Search();
let s = new Zotero.Search();
s.id = row.ref.id;
s.loadPrimaryData()
.then(function () {
return s.loadConditions();
})
.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));
yield s.loadPrimaryData();
yield s.loadConditions();
let groups = [];
// Promises don't work in the modal dialog, so get the group name here, if
// applicable, and pass it in. We only need the group that this search belongs
// to, if any, since the library drop-down is disabled for saved searches.
if (Zotero.Libraries.getType(s.libraryID) == 'group') {
groups.push(yield Zotero.Groups.getByLibraryID(s.libraryID));
}
var io = {
dataIn: {
search: s,
name: row.getName(),
groups: groups
},
dataOut: null
};
window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io);
if (io.dataOut) {
this.onCollectionSelected(); //reload itemsView
}
}
}
}
});
this.copySelectedItemsToClipboard = function (asCitations) {

View file

@ -37,11 +37,61 @@ describe("Zotero.Search", function() {
yield s.loadConditions();
var conditions = s.getConditions();
assert.lengthOf(Object.keys(conditions), 1);
assert.property(conditions, "1"); // searchConditionIDs start at 1
var condition = conditions[1];
assert.property(conditions, "0");
var condition = conditions[0];
assert.propertyVal(condition, 'condition', 'title')
assert.propertyVal(condition, 'operator', 'is')
assert.propertyVal(condition, 'value', 'test')
assert.propertyVal(condition, 'required', false)
});
it("should add a condition to an existing search", function* () {
// Save search
var s = new Zotero.Search;
s.libraryID = Zotero.Libraries.userLibraryID;
s.name = "Test";
s.addCondition('title', 'is', 'test');
var id = yield s.save();
assert.typeOf(id, 'number');
// Add condition
s = yield Zotero.Searches.getAsync(id);
yield s.loadConditions();
s.addCondition('title', 'contains', 'foo');
var saved = yield s.save();
assert.isTrue(saved);
// Check saved search
s = yield Zotero.Searches.getAsync(id);
yield s.loadConditions();
var conditions = s.getConditions();
assert.lengthOf(Object.keys(conditions), 2);
});
it("should remove a condition from an existing search", function* () {
// Save search
var s = new Zotero.Search;
s.libraryID = Zotero.Libraries.userLibraryID;
s.name = "Test";
s.addCondition('title', 'is', 'test');
s.addCondition('title', 'contains', 'foo');
var id = yield s.save();
assert.typeOf(id, 'number');
// Remove condition
s = yield Zotero.Searches.getAsync(id);
yield s.loadConditions();
s.removeCondition(0);
var saved = yield s.save();
assert.isTrue(saved);
// Check saved search
s = yield Zotero.Searches.getAsync(id);
yield s.loadConditions();
var conditions = s.getConditions();
assert.lengthOf(Object.keys(conditions), 1);
assert.property(conditions, "0");
assert.propertyVal(conditions[0], 'value', 'foo')
});
});
});