Skip to content

Commit

Permalink
#9085 isNull Filter (#9086)
Browse files Browse the repository at this point in the history
* WIP fixing isNull filter

* Improved fix

* Update build/tests.webpack.js

* Fixed tests

* Fixed lint

* Update build/tests.webpack.js

Co-authored-by: Matteo V. <[email protected]>

* Update web/client/utils/__tests__/FilterUtils-test.js

---------

Co-authored-by: Matteo V. <[email protected]>
  • Loading branch information
offtherailz and MV88 authored Apr 11, 2023
1 parent 76e114f commit d0b2ab9
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 79 deletions.
3 changes: 1 addition & 2 deletions web/client/components/data/query/QueryToolbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,8 @@ class QueryToolbar extends React.Component {
let fieldsExceptions = this.props.filterFields.filter((field) => field.exception).length > 0;
// option allowEmptyFilter available only for the base toolbar, not advanced TODO: externalize this behaviour
const allowEmpty = (this.props.allowEmptyFilter && !this.props.advancedToolbar);

// this flag checks if there is any valid attribute fields (with value)
let hasValidAttributeFields = this.props.filterFields.filter((field) => field.value || field.value === 0).length > 0;
let hasValidAttributeFields = this.props.filterFields.filter((field) => checkOperatorValidity(field.value, field.operator)).length > 0;

const isCurrentFilterEmpty = isFilterEmpty(this.props);
const isAppliedFilterEmpty = isFilterEmpty(this.props.appliedFilter);
Expand Down
62 changes: 32 additions & 30 deletions web/client/utils/FilterUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export const cqlToOgc = (cqlFilter, fOpts) => {
return toFilter(read(cqlFilter));
};

import { get, isNil, isUndefined, isArray, find, findIndex, isString, flatten } from 'lodash';
import { get, isNil, isArray, find, findIndex, isString, flatten } from 'lodash';
let FilterUtils;

const wrapValueWithWildcard = (value, condition) => {
Expand All @@ -43,7 +43,7 @@ export const wrapIfNoWildcards = (value) => {
export const escapeCQLStrings = str => str && str.replace ? str.replace(/\'/g, "''") : str;

export const checkOperatorValidity = (value, operator) => {
return (!isNil(value) && operator !== "isNull" || !isUndefined(value) && operator === "isNull");
return ( operator === "isNull" || !isNil(value) );
};
/**
* Test if crossLayer filter is valid.
Expand Down Expand Up @@ -960,9 +960,7 @@ export const cqlStringField = function(attribute, operator, value) {
const wrappedAttr = wrapAttributeWithDoubleQuotes(attribute);
if (!isNil(value)) {
const processedValue = processCqlWildcards(value, operator);
if (operator === "isNull") {
fieldFilter = "isNull(" + wrappedAttr + ")=true";
} else if (["<>", "="].includes(operator)) {
if (["<>", "="].includes(operator)) {
fieldFilter = wrappedAttr + operator + processedValue;
} else if (operator === "ilike") {
fieldFilter = "strToLowerCase(" + wrappedAttr + ") LIKE " + processedValue;
Expand Down Expand Up @@ -1029,30 +1027,34 @@ export const processCQLFilterFields = function(group, objFilter) {
if (fields) {
fields.forEach((field) => {
let fieldFilter;

switch (field.type) {
case "date":
case "time":
case "date-time":
fieldFilter = FilterUtils.cqlDateField(field.attribute, field.operator, field.value);
break;
case "number":
fieldFilter = FilterUtils.cqlNumberField(field.attribute, field.operator, field.value);
break;
case "string":
fieldFilter = FilterUtils.cqlStringField(field.attribute, field.operator, field.value);
break;
case "boolean":
fieldFilter = FilterUtils.cqlBooleanField(field.attribute, field.operator, field.value);
break;
case "list":
fieldFilter = FilterUtils.cqlListField(field.attribute, field.operator, field.value);
break;
case "array":
fieldFilter = FilterUtils.cqlArrayField(field.attribute, field.operator, field.value);
break;
default:
break;
if (field.operator === "isNull") {
const wrappedAttr = wrapAttributeWithDoubleQuotes(field.attribute);
fieldFilter = "isNull(" + wrappedAttr + ")=true";
} else {
switch (field.type) {
case "date":
case "time":
case "date-time":
fieldFilter = FilterUtils.cqlDateField(field.attribute, field.operator, field.value);
break;
case "number":
fieldFilter = FilterUtils.cqlNumberField(field.attribute, field.operator, field.value);
break;
case "string":
fieldFilter = FilterUtils.cqlStringField(field.attribute, field.operator, field.value);
break;
case "boolean":
fieldFilter = FilterUtils.cqlBooleanField(field.attribute, field.operator, field.value);
break;
case "list":
fieldFilter = FilterUtils.cqlListField(field.attribute, field.operator, field.value);
break;
case "array":
fieldFilter = FilterUtils.cqlArrayField(field.attribute, field.operator, field.value);
break;
default:
break;
}
}
if (fieldFilter) {
filter.push(group.negateAll ? 'NOT (' + fieldFilter + ')' : fieldFilter);
Expand Down Expand Up @@ -1139,7 +1141,7 @@ export const getWFSFilterData = (filterObj, options) => {
};
export const isLikeOrIlike = (operator) => operator === "ilike" || operator === "like";
export const isFilterEmpty = ({ filterFields = [], spatialField = {}, crossLayerFilter = {}, filters = [] } = {}) =>
!(filterFields.filter((field) => field.value || field.value === 0).length > 0)
!(filterFields.filter((field) => field.value || field.value === 0 || field.operator === "isNull").length > 0)
&& !spatialField.geometry
&& !(crossLayerFilter && crossLayerFilter.attribute && crossLayerFilter.operation)
&& !(filters && filters.length > 0);
Expand Down
222 changes: 175 additions & 47 deletions web/client/utils/__tests__/FilterUtils-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import {
wrapIfNoWildcards,
mergeFiltersToOGC,
convertFiltersToOGC,
convertFiltersToCQL
convertFiltersToCQL,
isFilterEmpty
} from '../FilterUtils';


Expand Down Expand Up @@ -577,49 +578,57 @@ describe('FilterUtils', () => {
expect(filter).toEqual(expected);
});
it('Test checkOperatorValidity', () => {
let filterObj = {
filterFields: [{
attribute: "attributeNull",
groupId: 1,
exception: null,
operator: "=",
rowId: "1",
type: "string",
value: null
}, {
attribute: "attributeUndefined",
groupId: 1,
exception: null,
operator: "=",
rowId: "2",
type: "string",
value: undefined
}, {
attribute: "attributeNull2",
groupId: 1,
exception: null,
operator: "isNull",
rowId: "3",
type: "string",
value: undefined
}, {
attribute: "attributeUndefined2",
groupId: 1,
exception: null,
operator: "isNull",
rowId: "4",
type: "string",
value: null // valid value for isnull operator
}]
};

filterObj.filterFields.forEach((f, i) => {
let valid = checkOperatorValidity(f.value, f.operator);
if (i <= 2) {
expect(valid).toEqual(false);
} else {
expect(valid).toEqual(true);
}
const validFilterFields = [{
attribute: "operatorIsEqaual",
groupId: 1,
exception: null,
operator: "=",
rowId: "1",
type: "string",
value: "value"
}, {
attribute: "attributeNull2",
groupId: 1,
exception: null,
operator: "isNull",
rowId: "3",
type: "string",
value: undefined
}, {
attribute: "attributeUndefined2",
groupId: 1,
exception: null,
operator: "isNull",
rowId: "4",
type: "string",
value: null // valid value for isnull operator
}];
validFilterFields.forEach((f, i) => {
expect(checkOperatorValidity(f.value, f.operator)).toBe(true, `Failed on ${i}` );
});
const invalidFilterFields = [{
attribute: "attributeNull",
groupId: 1,
exception: null,
operator: "=",
rowId: "1",
type: "string",
value: null
}, {
attribute: "attributeUndefined",
groupId: 1,
exception: null,
operator: "=",
rowId: "2",
type: "string",
value: undefined
}, {
attribute: "attributeUndefined",
groupId: 1,
exception: null
}];
invalidFilterFields.forEach((f, i) => {
expect(checkOperatorValidity(f.value, f.operator)).toBe(false, `Failed on ${i}` );
});
});
it('getGetFeatureBase gets viewParams', () => {
Expand Down Expand Up @@ -811,7 +820,24 @@ describe('FilterUtils', () => {
value: "isNull"
}]
};
let expected = '<wfs:GetFeature service="WFS" version="2.0" xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:fes="http://www.opengis.net/fes/2.0" xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd http://www.opengis.net/gml/3.2 http://schemas.opengis.net/gml/3.2.1/gml.xsd"><wfs:Query typeNames="ft_name_test" srsName="EPSG:4326"><fes:Filter><fes:PropertyIsNull><fes:ValueReference>attributeNull</fes:ValueReference></fes:PropertyIsNull><fes:PropertyIsNull><fes:ValueReference>attributeValid</fes:ValueReference></fes:PropertyIsNull></fes:Filter></wfs:Query></wfs:GetFeature>';
let expected = '<wfs:GetFeature service="WFS" version="2.0" '
+ 'xmlns:wfs="http://www.opengis.net/wfs/2.0" '
+ 'xmlns:fes="http://www.opengis.net/fes/2.0" '
+ 'xmlns:gml="http://www.opengis.net/gml/3.2" '
+ 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
+ 'xsi:schemaLocation="http://www.opengis.net/wfs/2.0 '
+ 'http://schemas.opengis.net/wfs/2.0/wfs.xsd '
+ 'http://www.opengis.net/gml/3.2 '
+ 'http://schemas.opengis.net/gml/3.2.1/gml.xsd">'
+ '<wfs:Query '
+ 'typeNames="ft_name_test" srsName="EPSG:4326">'
+ '<fes:Filter>'
+ '<fes:PropertyIsNull><fes:ValueReference>attributeNull</fes:ValueReference></fes:PropertyIsNull>'
+ '<fes:PropertyIsNull><fes:ValueReference>attributeUndefined</fes:ValueReference></fes:PropertyIsNull>'
+ '<fes:PropertyIsNull><fes:ValueReference>attributeValid</fes:ValueReference></fes:PropertyIsNull>'
+ '</fes:Filter>'
+ '</wfs:Query>'
+ '</wfs:GetFeature>';
let filter = toOGCFilter("ft_name_test", filterObj);
expect(filter).toEqual(expected);
});
Expand Down Expand Up @@ -1196,8 +1222,6 @@ describe('FilterUtils', () => {
expect(cqlStringField("attribute_1", "=", "PRE'")).toBe("\"attribute_1\"='PRE'''");
// test <>
expect(cqlStringField("attribute_1", "<>", "Alabama")).toBe("\"attribute_1\"<>'Alabama'");
// test isNull
expect(cqlStringField("attribute_1", "isNull", "")).toBe("isNull(\"attribute_1\")=true");
// test ilike
expect(cqlStringField("attribute_1", "ilike", "A")).toBe("strToLowerCase(\"attribute_1\") LIKE '%a%'");
// test LIKE
Expand Down Expand Up @@ -1419,6 +1443,51 @@ describe('FilterUtils', () => {
};
expect(toCQLFilter(filterObject)).toBe("(\"STATE_NAME\"='Alabama' OR (\"STATE_NAME\"='Arizona' OR \"STATE_NAME\"='Arkansas'))");
});
it('isNull operator in CQL filter', () => {
const filterObj = {
"searchUrl": null,
"featureTypeConfigUrl": null,
"showGeneratedFilter": false,
"attributePanelExpanded": true,
"spatialPanelExpanded": true,
"crossLayerExpanded": true,
"showDetailsPanel": false,
"groupLevels": 5,
"useMapProjection": false,
"toolbarEnabled": true,
"groupFields": [
{
"id": 1,
"logic": "NOR",
"index": 0
}
],
"maxFeaturesWPS": 5,
"filterFields": [
{
"rowId": 1680880641587,
"groupId": 1,
"attribute": "STATE_NAME",
"operator": "isNull",
"value": null,
"type": "string",
"fieldOptions": {
"valuesCount": 0,
"currentPage": 1
},
"exception": null
}
],
"spatialField": {
"method": null,
"operation": "INTERSECTS",
"geometry": null,
"attribute": "the_geom"
}
};
expect(toCQLFilter(filterObj)).toBe('(NOT (isNull("STATE_NAME")=true))');

});
it('getCrossLayerCqlFilter', () => {
const filter = getCrossLayerCqlFilter({
collectGeometries: {
Expand Down Expand Up @@ -2042,6 +2111,26 @@ describe('FilterUtils', () => {
};
filter = processCQLFilterFields(group, objFilter);
expect(filter).toEqual("");
// test one operator
expect(processCQLFilterFields(group, {
filterFields: [{
groupId: 1,
attribute: "test",
type: "string",
operator: "=",
value: "test"
}]
})).toEqual(`"test"='test'`);
// test is null
expect(processCQLFilterFields(group, {
filterFields: [{
groupId: 1,
attribute: "test",
type: "string",
operator: "isNull"
}]
})).toEqual(`isNull("test")=true`);

});

it('wrapIfNoWildcards', () => {
Expand Down Expand Up @@ -2175,4 +2264,43 @@ describe('FilterUtils', () => {
});
});
});
it('isFilterEmpty', () => {
expect(isFilterEmpty({
filterFields: [],
spatialField: {},
crossLayerFilter: {},
filters: []
})).toBe(true);
expect(isFilterEmpty({
filterFields: [{value: 1}],
spatialField: {},
crossLayerFilter: {},
filters: []
})).toBe(false);
expect(isFilterEmpty({
filterFields: [{value: 1}],
spatialField: {geometry: {type: 'Point', coordinates: [1, 2]}},
crossLayerFilter: {},
filters: []
})).toBe(false);
expect(isFilterEmpty({
filterFields: [],
spatialField: {},
crossLayerFilter: {attribute: 'attr', operation: 'op'},
filters: []
})).toBe(false);
expect(isFilterEmpty({
filterFields: [],
spatialField: {},
crossLayerFilter: {},
filters: [{format: 'logic', logic: 'AND', filters: []}]
})).toBe(false);
expect(isFilterEmpty({
filterFields: [{operator: "isNull"}],
spatialField: {},
crossLayerFilter: {},
filters: []
})).toBe(false);

});
});

0 comments on commit d0b2ab9

Please sign in to comment.