1 /** 2 * @fileOverview 3 * @author <a href="https://www.labkey.org">LabKey</a> (<a href="mailto:info@labkey.com">info@labkey.com</a>) 4 * @license Copyright (c) 2012-2019 LabKey Corporation 5 * <p/> 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * <p/> 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * <p/> 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 * <p/> 18 */ 19 20 /** 21 * @class LABKEY.Filter 22 * @namespace Filter static class to describe and create filters. 23 * <p>Additional Documentation: 24 * <ul> 25 * <li><a href="https://www.labkey.org/Documentation/wiki-page.view?name=filteringData">Filter via the LabKey UI</a></li> 26 * <li><a href="https://www.labkey.org/Documentation/wiki-page.view?name=tutorialActionURL">Tutorial: Basics: Building URLs and Filters</a></li> 27 * </ul> 28 * </p> 29 * @property {Object} Types Types static class to describe different types of filters. 30 * @property {LABKEY.Filter.FilterDefinition} Types.EQUAL Finds rows where the column value matches the given filter value. Case-sensitivity depends upon how your underlying relational database was configured. 31 * @property {LABKEY.Filter.FilterDefinition} Types.DATE_EQUAL Finds rows where the date portion of a datetime column matches the filter value (ignoring the time portion). 32 * @property {LABKEY.Filter.FilterDefinition} Types.DATE_NOT_EQUAL Finds rows where the date portion of a datetime column does not match the filter value (ignoring the time portion). 33 * @property {LABKEY.Filter.FilterDefinition} Types.NOT_EQUAL_OR_MISSING Finds rows where the column value does not equal the filter value, or is missing (null). 34 * @property {LABKEY.Filter.FilterDefinition} Types.NOT_EQUAL Finds rows where the column value does not equal the filter value. 35 * @property {LABKEY.Filter.FilterDefinition} Types.MISSING Finds rows where the column value is missing (null). Note that no filter value is required with this operator. 36 * @property {LABKEY.Filter.FilterDefinition} Types.NOT_MISSING Finds rows where the column value is not missing (is not null). Note that no filter value is required with this operator. 37 * @property {LABKEY.Filter.FilterDefinition} Types.GREATER_THAN Finds rows where the column value is greater than the filter value. 38 * @property {LABKEY.Filter.FilterDefinition} Types.LESS_THAN Finds rows where the column value is less than the filter value. 39 * @property {LABKEY.Filter.FilterDefinition} Types.GREATER_THAN_OR_EQUAL Finds rows where the column value is greater than or equal to the filter value. 40 * @property {LABKEY.Filter.FilterDefinition} Types.LESS_THAN_OR_EQUAL Finds rows where the column value is less than or equal to the filter value. 41 * @property {LABKEY.Filter.FilterDefinition} Types.CONTAINS Finds rows where the column value contains the filter value. Note that this may result in a slow query as this cannot use indexes. 42 * @property {LABKEY.Filter.FilterDefinition} Types.DOES_NOT_CONTAIN Finds rows where the column value does not contain the filter value. Note that this may result in a slow query as this cannot use indexes. 43 * @property {LABKEY.Filter.FilterDefinition} Types.DOES_NOT_START_WITH Finds rows where the column value does not start with the filter value. 44 * @property {LABKEY.Filter.FilterDefinition} Types.STARTS_WITH Finds rows where the column value starts with the filter value. 45 * @property {LABKEY.Filter.FilterDefinition} Types.IN Finds rows where the column value equals one of the supplied filter values. The values should be supplied as a semi-colon-delimited list (example usage: a;b;c). 46 * @property {LABKEY.Filter.FilterDefinition} Types.NOT_IN Finds rows where the column value is not in any of the supplied filter values. The values should be supplied as a semi-colon-delimited list (example usage: a;b;c). 47 * @property {LABKEY.Filter.FilterDefinition} Types.MEMBER_OF Finds rows where the column value contains a user id that is a member of the group id of the supplied filter value. 48 * @property {LABKEY.Filter.FilterDefinition} Types.CONTAINS_ONE_OF Finds rows where the column value contains any of the supplied filter values. The values should be supplied as a semi-colon-delimited list (example usage: a;b;c). 49 * @property {LABKEY.Filter.FilterDefinition} Types.CONTAINS_NONE_OF Finds rows where the column value does not contain any of the supplied filter values. The values should be supplied as a semi-colon-delimited list (example usage: a;b;c). 50 * @property {LABKEY.Filter.FilterDefinition} Types.BETWEEN Finds rows where the column value is between the two filter values, inclusive. The values should be supplied as a comma-delimited list (example usage: -4,4). 51 * @property {LABKEY.Filter.FilterDefinition} Types.NOT_BETWEEN Finds rows where the column value is not between the two filter values, exclusive. The values should be supplied as a comma-delimited list (example usage: -4,4). 52 * 53 */ 54 LABKEY.Filter = new function() 55 { 56 // validate the component items of the value 57 // returns the string representation of the filter value (see .getURLParameterValue) 58 function validateMultiple(filterType, type, value, colName, sep, minOccurs, maxOccurs) 59 { 60 var values; 61 try 62 { 63 values = filterType.parseValue(value); 64 } 65 catch (x) 66 { 67 alert("Failed to validate filter: " + x.toString()); 68 return undefined; 69 } 70 71 var result = []; 72 for (var i = 0; i < values.length; i++) 73 { 74 value = validate(type, values[i].trim(), colName); 75 if (value == undefined) 76 return undefined; 77 78 result.push(value); 79 } 80 81 if (minOccurs !== undefined && minOccurs > 0) 82 { 83 if (values.length < minOccurs) 84 { 85 alert("At least " + minOccurs + " '" + sep + "' separated values are required"); 86 return undefined; 87 } 88 } 89 90 if (maxOccurs !== undefined && maxOccurs > 0) 91 { 92 if (values.length > maxOccurs) 93 { 94 alert("At most " + maxOccurs + " '" + sep + "' separated values are allowed"); 95 return undefined; 96 } 97 } 98 99 return filterType.getURLParameterValue(values); 100 } 101 102 function validate(type, value, colName) 103 { 104 if (type == "int") 105 { 106 var intVal = parseInt(value); 107 if (isNaN(intVal)) 108 { 109 alert(value + " is not a valid integer for field '" + colName + "'."); 110 return undefined; 111 } 112 else 113 return "" + intVal; 114 } 115 else if (type == "float") 116 { 117 var decVal = parseFloat(value); 118 if (isNaN(decVal)) 119 { 120 alert(value + " is not a valid decimal number for field '" + colName + "'."); 121 return undefined; 122 } 123 else 124 return "" + decVal; 125 } 126 else if (type == "date") 127 { 128 var year, month, day, hour, minute; 129 hour = 0; 130 minute = 0; 131 132 //Javascript does not parse ISO dates, but if date matches we're done 133 if (value.match(/^\s*(\d\d\d\d)-(\d\d)-(\d\d)\s*$/) || 134 value.match(/^\s*(\d\d\d\d)-(\d\d)-(\d\d)\s*(\d\d):(\d\d)\s*$/)) 135 { 136 return value; 137 } 138 else 139 { 140 var dateVal = new Date(value); 141 if (isNaN(dateVal)) 142 { 143 //filters can use relative dates, in the format +1d, -5H, etc. we try to identfy those here 144 //this is fairly permissive and does not attempt to parse this value into a date. See CompareType.asDate() 145 //for server-side parsing 146 if (value.match(/^(-|\+)/i)) 147 { 148 return value; 149 } 150 151 alert(value + " is not a valid date for field '" + colName + "'."); 152 return undefined; 153 } 154 //Try to do something decent with 2 digit years! 155 //if we have mm/dd/yy (but not mm/dd/yyyy) in the date 156 //fix the broken date parsing 157 if (value.match(/\d+\/\d+\/\d{2}(\D|$)/)) 158 { 159 if (dateVal.getFullYear() < new Date().getFullYear() - 80) 160 dateVal.setFullYear(dateVal.getFullYear() + 100); 161 } 162 year = dateVal.getFullYear(); 163 month = dateVal.getMonth() + 1; 164 day = dateVal.getDate(); 165 hour = dateVal.getHours(); 166 minute = dateVal.getMinutes(); 167 } 168 var str = "" + year + "-" + twoDigit(month) + "-" + twoDigit(day); 169 if (hour != 0 || minute != 0) 170 str += " " + twoDigit(hour) + ":" + twoDigit(minute); 171 172 return str; 173 } 174 else if (type == "boolean") 175 { 176 var upperVal = value.toUpperCase(); 177 if (upperVal == "TRUE" || value == "1" || upperVal == "YES" || upperVal == "Y" || upperVal == "ON" || upperVal == "T") 178 return "1"; 179 if (upperVal == "FALSE" || value == "0" || upperVal == "NO" || upperVal == "N" || upperVal == "OFF" || upperVal == "F") 180 return "0"; 181 else 182 { 183 alert(value + " is not a valid boolean for field '" + colName + "'. Try true,false; yes,no; y,n; on,off; or 1,0."); 184 return undefined; 185 } 186 } 187 else 188 return value; 189 } 190 191 function twoDigit(num) 192 { 193 if (num < 10) 194 return "0" + num; 195 else 196 return "" + num; 197 } 198 199 var urlMap = {}; 200 var oppositeMap = { 201 //HAS_ANY_VALUE: null, 202 eq: 'neqornull', 203 dateeq : 'dateneq', 204 dateneq : 'dateeq', 205 neqornull : 'eq', 206 neq : 'eq', 207 isblank : 'isnonblank', 208 isnonblank : 'isblank', 209 gt : 'lte', 210 dategt : 'datelte', 211 lt : 'gte', 212 datelt : 'dategte', 213 gte : 'lt', 214 dategte : 'datelt', 215 lte : 'gt', 216 datelte : 'dategt', 217 contains : 'doesnotcontain', 218 doesnotcontain : 'contains', 219 doesnotstartwith : 'startswith', 220 startswith : 'doesnotstartwith', 221 'in' : 'notin', 222 notin : 'in', 223 memberof : 'memberof', 224 containsoneof : 'containsnoneof', 225 containsnoneof : 'containsoneof', 226 hasmvvalue : 'nomvvalue', 227 nomvvalue : 'hasmvvalue', 228 between : 'notbetween', 229 notbetween : 'between' 230 }; 231 232 //NOTE: these maps contains the unambiguous pairings of single- and multi-valued filters 233 //due to NULLs, one cannot easily convert neq to notin 234 var multiValueToSingleMap = { 235 'in' : 'eq', 236 containsoneof : 'contains', 237 containsnoneof : 'doesnotcontain', 238 between: 'gte', 239 notbetween: 'lt' 240 }; 241 242 var singleValueToMultiMap = { 243 eq : 'in', 244 neq : 'notin', 245 neqornull: 'notin', 246 doesnotcontain : 'containsnoneof', 247 contains : 'containsoneof' 248 }; 249 250 function createNoValueFilterType(displayText, displaySymbol, urlSuffix, longDisplayText) 251 { 252 return createFilterType(displayText, displaySymbol, urlSuffix, false, false, null, longDisplayText); 253 } 254 255 function createSingleValueFilterType(displayText, displaySymbol, urlSuffix, longDisplayText) 256 { 257 return createFilterType(displayText, displaySymbol, urlSuffix, true, false, null, longDisplayText); 258 } 259 260 function createMultiValueFilterType(displayText, displaySymbol, urlSuffix, longDisplayText, multiValueSeparator, minOccurs, maxOccurs) 261 { 262 return createFilterType(displayText, displaySymbol, urlSuffix, true, false, multiValueSeparator, longDisplayText, minOccurs, maxOccurs); 263 } 264 265 function createTableFilterType(displayText, displaySymbol, urlSuffix, longDisplayText) 266 { 267 return createFilterType(displayText, displaySymbol, urlSuffix, true, true, null, longDisplayText); 268 } 269 270 function createFilterType(displayText, displaySymbol, urlSuffix, dataValueRequired, isTableWise, multiValueSeparator, longDisplayText, minOccurs, maxOccurs) 271 { 272 var result = { 273 getDisplaySymbol : function() { return displaySymbol }, 274 getDisplayText : function() { return displayText }, 275 getLongDisplayText : function() { return longDisplayText || displayText }, 276 getURLSuffix : function() { return urlSuffix }, 277 isDataValueRequired : function() { return dataValueRequired === true }, 278 isMultiValued : function() { return multiValueSeparator != null; }, 279 isTableWise : function() { return isTableWise === true }, 280 getMultiValueSeparator : function() { return multiValueSeparator }, 281 getMultiValueMinOccurs : function() { return minOccurs }, 282 getMultiValueMaxOccurs : function() { return maxOccurs; }, 283 getOpposite : function() {return oppositeMap[urlSuffix] ? urlMap[oppositeMap[urlSuffix]] : null}, 284 getSingleValueFilter : function() {return this.isMultiValued() ? urlMap[multiValueToSingleMap[urlSuffix]] : this}, 285 getMultiValueFilter : function() {return this.isMultiValued() ? null : urlMap[singleValueToMultiMap[urlSuffix]]}, 286 287 /** 288 * Note: this is an experimental API that may change unexpectedly in future releases. 289 * Parse a filter String or Array value appropriately for this filter type. 290 * @return {String|Array} For multi-valued filter types, an Array of values, otherwise the filter value. 291 */ 292 parseValue: function (value) { 293 if (this.isMultiValued() && LABKEY.Utils.isString(value)) 294 { 295 if (value.indexOf("{json:") === 0 && value.indexOf("}") === value.length-1) { 296 value = JSON.parse(value.substring("{json:".length, value.length - 1)); 297 } 298 else { 299 value = value.split(this.getMultiValueSeparator()); 300 } 301 } 302 303 if (!this.isMultiValued() && LABKEY.Utils.isArray(value)) 304 throw new Error("Array of values not supported for '" + this.getDisplayText() + "' filter: " + value); 305 306 return value; 307 }, 308 309 /** 310 * Note: this is an experimental API that may change unexpectedly in future releases. 311 * Returns the (unencoded) value that will be put on URL. 312 * @returns {String} 313 */ 314 getURLParameterValue: function (value) { 315 if (!this.isDataValueRequired()) 316 return ''; 317 318 if (this.isMultiValued() && LABKEY.Utils.isArray(value)) 319 { 320 var sep = this.getMultiValueSeparator(); 321 var containsSep = value.some(function (v) { 322 return LABKEY.Utils.isString(v) && v.indexOf(sep) !== -1; 323 }); 324 if (containsSep) 325 return "{json:" + JSON.stringify(value) + "}"; 326 else 327 return value.join(sep); 328 } 329 330 return value; 331 }, 332 333 /** 334 * Note: this is an experimental API that may change unexpectedly in future releases. 335 * Validate a form value against the json type. Error alerts will be displayed. 336 * @param type The json type ("int", "float", "date", or "boolean") 337 * @param value The value to test. 338 * @param colName The column name to use in error messages. 339 * @return undefined if not valid otherwise a normalized string value for the type. 340 */ 341 validate : function (value, type, colName) { 342 if (!dataValueRequired) 343 return true; 344 345 var f = filterTypes[type]; 346 var found = false; 347 for (var i = 0; !found && i < f.length; i++) 348 { 349 if (f[i].getURLSuffix() == urlSuffix) 350 found = true; 351 } 352 if (!found) { 353 alert("Filter type '" + displayText + "' can't be applied to " + type + " types."); 354 return undefined; 355 } 356 357 if (this.isMultiValued()) 358 return validateMultiple(this, type, value, colName, multiValueSeparator, minOccurs, maxOccurs); 359 else 360 return validate(type, value, colName); 361 } 362 }; 363 urlMap[urlSuffix] = result; 364 return result; 365 } 366 367 var ret = /** @scope LABKEY.Filter */{ 368 369 // WARNING: Keep in sync and in order with all other client apis and docs 370 // - server: CompareType.java 371 // - java: Filter.java 372 // - js: Filter.js 373 // - R: makeFilter.R, makeFilter.Rd 374 // - SAS: labkeymakefilter.sas, labkey.org SAS docs 375 // - Python & Perl don't have an filter operator enum 376 // - EXPERIMENTAL: Added an optional displaySymbol() for filters that want to support it 377 Types : { 378 379 HAS_ANY_VALUE : createNoValueFilterType("Has Any Value", null, "", null), 380 381 // 382 // These operators require a data value 383 // 384 385 EQUAL : createSingleValueFilterType("Equals", "=", "eq", null), 386 DATE_EQUAL : createSingleValueFilterType("Equals", "=", "dateeq", null), 387 388 NEQ : createSingleValueFilterType("Does Not Equal", "<>", "neq", null), 389 NOT_EQUAL : createSingleValueFilterType("Does Not Equal", "<>", "neq", null), 390 DATE_NOT_EQUAL : createSingleValueFilterType("Does Not Equal", "<>", "dateneq", null), 391 392 NEQ_OR_NULL : createSingleValueFilterType("Does Not Equal", "<>", "neqornull", null), 393 NOT_EQUAL_OR_MISSING : createSingleValueFilterType("Does Not Equal", "<>", "neqornull", null), 394 395 GT : createSingleValueFilterType("Is Greater Than", ">", "gt", null), 396 GREATER_THAN : createSingleValueFilterType("Is Greater Than", ">", "gt", null), 397 DATE_GREATER_THAN : createSingleValueFilterType("Is Greater Than", ">", "dategt", null), 398 399 LT : createSingleValueFilterType("Is Less Than", "<", "lt", null), 400 LESS_THAN : createSingleValueFilterType("Is Less Than", "<", "lt", null), 401 DATE_LESS_THAN : createSingleValueFilterType("Is Less Than", "<", "datelt", null), 402 403 GTE : createSingleValueFilterType("Is Greater Than or Equal To", ">=", "gte", null), 404 GREATER_THAN_OR_EQUAL : createSingleValueFilterType("Is Greater Than or Equal To", ">=", "gte", null), 405 DATE_GREATER_THAN_OR_EQUAL : createSingleValueFilterType("Is Greater Than or Equal To", ">=", "dategte", null), 406 407 LTE : createSingleValueFilterType("Is Less Than or Equal To", "=<", "lte", null), 408 LESS_THAN_OR_EQUAL : createSingleValueFilterType("Is Less Than or Equal To", "=<", "lte", null), 409 DATE_LESS_THAN_OR_EQUAL : createSingleValueFilterType("Is Less Than or Equal To", "=<", "datelte", null), 410 411 STARTS_WITH : createSingleValueFilterType("Starts With", null, "startswith", null), 412 DOES_NOT_START_WITH : createSingleValueFilterType("Does Not Start With", null, "doesnotstartwith", null), 413 414 CONTAINS : createSingleValueFilterType("Contains", null, "contains", null), 415 DOES_NOT_CONTAIN : createSingleValueFilterType("Does Not Contain", null, "doesnotcontain", null), 416 417 CONTAINS_ONE_OF : createMultiValueFilterType("Contains One Of", null, "containsoneof", 'Contains One Of (example usage: a;b;c)', ";"), 418 CONTAINS_NONE_OF : createMultiValueFilterType("Does Not Contain Any Of", null, "containsnoneof", 'Does Not Contain Any Of (example usage: a;b;c)', ";"), 419 420 IN : createMultiValueFilterType("Equals One Of", null, "in", 'Equals One Of (example usage: a;b;c)', ";"), 421 //NOTE: for some reason IN is aliased as EQUALS_ONE_OF. not sure if this is for legacy purposes or it was determined EQUALS_ONE_OF was a better phrase 422 //to follow this pattern I did the same for IN_OR_MISSING 423 EQUALS_ONE_OF : createMultiValueFilterType("Equals One Of", null, "in", 'Equals One Of (example usage: a;b;c)', ";"), 424 425 NOT_IN: createMultiValueFilterType("Does Not Equal Any Of", null, "notin", 'Does Not Equal Any Of (example usage: a;b;c)', ";"), 426 EQUALS_NONE_OF: createMultiValueFilterType("Does Not Equal Any Of", null, "notin", 'Does Not Equal Any Of (example usage: a;b;c)', ";"), 427 428 BETWEEN : createMultiValueFilterType("Between", null, "between", 'Between, Inclusive (example usage: -4,4)', ",", 2, 2), 429 NOT_BETWEEN : createMultiValueFilterType("Not Between", null, "notbetween", 'Not Between, Exclusive (example usage: -4,4)', ",", 2, 2), 430 431 MEMBER_OF : createSingleValueFilterType("Member Of", null, "memberof", 'Member Of'), 432 433 // 434 // These are the "no data value" operators 435 // 436 437 ISBLANK : createNoValueFilterType("Is Blank", null, "isblank", null), 438 MISSING : createNoValueFilterType("Is Blank", null, "isblank", null), 439 NONBLANK : createNoValueFilterType("Is Not Blank", null, "isnonblank", null), 440 NOT_MISSING : createNoValueFilterType("Is Not Blank", null, "isnonblank", null), 441 442 HAS_MISSING_VALUE : createNoValueFilterType("Has a missing value indicator", null, "hasmvvalue", null), 443 DOES_NOT_HAVE_MISSING_VALUE : createNoValueFilterType("Does not have a missing value indicator", null, "nomvvalue", null), 444 445 EXP_PARENT_OF : createSingleValueFilterType("Is Parent Of", null, "exp:parentof", " is parent of" ), 446 EXP_CHILD_OF : createSingleValueFilterType("Is Child Of", null, "exp:childof", " is child of" ), 447 448 // 449 // Table/Query-wise operators 450 // 451 Q : createTableFilterType("Search", null, "q", "Search across all columns") 452 }, 453 454 /** @private create a js object suitable for Query.selectRows, etc */ 455 appendFilterParams : function (params, filterArray, dataRegionName) 456 { 457 dataRegionName = dataRegionName || "query"; 458 params = params || {}; 459 if (filterArray) 460 { 461 for (var i = 0; i < filterArray.length; i++) 462 { 463 var filter = filterArray[i]; 464 // 10.1 compatibility: treat ~eq=null as a NOOP (ref 10482) 465 if (filter.getFilterType().isDataValueRequired() && null == filter.getURLParameterValue()) 466 continue; 467 468 // Create an array of filter values if there is more than one filter for the same column and filter type. 469 var paramName = filter.getURLParameterName(dataRegionName); 470 var paramValue = filter.getURLParameterValue(); 471 if (params[paramName] !== undefined) 472 { 473 var values = params[paramName]; 474 if (!LABKEY.Utils.isArray(values)) 475 values = [ values ]; 476 values.push(paramValue); 477 paramValue = values; 478 } 479 params[paramName] = paramValue; 480 } 481 } 482 return params; 483 }, 484 485 /** @private create a js object suitable for QueryWebPart, etc */ 486 appendAggregateParams : function (params, aggregateArray, dataRegionName) 487 { 488 dataRegionName = dataRegionName || "query"; 489 params = params || {}; 490 if (aggregateArray) 491 { 492 for (var idx = 0; idx < aggregateArray.length; ++idx) 493 { 494 var aggregate = aggregateArray[idx]; 495 var value = "type=" + aggregate.type; 496 if (aggregate.label) 497 value = value + "&label=" + aggregate.label; 498 if (aggregate.type && aggregate.column) 499 { 500 // Create an array of aggregate values if there is more than one aggregate for the same column. 501 var paramName = dataRegionName + '.analytics.' + aggregate.column; 502 var paramValue = encodeURIComponent(value); 503 if (params[paramName] !== undefined) 504 { 505 var values = params[paramName]; 506 if (!LABKEY.Utils.isArray(values)) 507 values = [ values ]; 508 values.push(paramValue); 509 paramValue = values; 510 } 511 params[paramName] = paramValue; 512 } 513 514 } 515 } 516 517 return params; 518 }, 519 520 521 /** 522 * Creates a filter 523 * @param {String} columnName String name of the column to filter 524 * @param value Value used as the filter criterion or an Array of values. 525 * @param {LABKEY.Filter#Types} [filterType] Type of filter to apply to the 'column' using the 'value' 526 * @example Example: <pre name="code" class="xml"> 527 <script type="text/javascript"> 528 function onFailure(errorInfo, options, responseObj) 529 { 530 if(errorInfo && errorInfo.exception) 531 alert("Failure: " + errorInfo.exception); 532 else 533 alert("Failure: " + responseObj.statusText); 534 } 535 536 function onSuccess(data) 537 { 538 alert("Success! " + data.rowCount + " rows returned."); 539 } 540 541 LABKEY.Query.selectRows({ 542 schemaName: 'lists', 543 queryName: 'People', 544 success: onSuccess, 545 failure: onFailure, 546 filterArray: [ 547 LABKEY.Filter.create('FirstName', 'Johnny'), 548 LABKEY.Filter.create('Age', 15, LABKEY.Filter.Types.LESS_THAN_OR_EQUAL) 549 LABKEY.Filter.create('LastName', ['A', 'B'], LABKEY.Filter.Types.DOES_NOT_START_WITH) 550 ] 551 }); 552 </script> </pre> 553 */ 554 555 create : function(columnName, value, filterType) 556 { 557 return new LABKEY.Query.Filter(columnName, value, filterType); 558 }, 559 560 /** 561 * Not for public use. Can be changed or dropped at any time. 562 * @param typeName 563 * @param displayText 564 * @param urlSuffix 565 * @param isMultiType 566 * @private 567 */ 568 _define : function(typeName, displayText, urlSuffix, isMultiType) { 569 if (!LABKEY.Filter.Types[typeName]) { 570 if (isMultiType) { 571 LABKEY.Filter.Types[typeName] = createMultiValueFilterType(displayText, null, urlSuffix, null); 572 } 573 else { 574 LABKEY.Filter.Types[typeName] = createSingleValueFilterType(displayText, null, urlSuffix, null); 575 } 576 } 577 }, 578 579 /** 580 * Given an array of filter objects, return a new filterArray with old filters from a column removed and new filters for the column added 581 * If new filters are null, simply remove all old filters from baseFilters that refer to this column 582 * @param {Array} baseFilters Array of existing filters created by {@link LABKEY.Filter.create} 583 * @param {String} columnName Column name of filters to replace 584 * @param {Array} columnFilters Array of new filters created by {@link LABKEY.Filter.create}. Will replace any filters referring to columnName 585 */ 586 merge : function(baseFilters, columnName, columnFilters) 587 { 588 var newFilters = []; 589 if (null != baseFilters) 590 for (var i = 0; i < baseFilters.length; i++) 591 { 592 var filt = baseFilters[i]; 593 if (filt.getColumnName() != columnName) 594 newFilters.push(filt); 595 } 596 597 return null == columnFilters ? newFilters : newFilters.concat(columnFilters); 598 }, 599 600 /** 601 * Convert from URL syntax filters to a human readable description, like "Is Greater Than 10 AND Is Less Than 100" 602 * @param {String} url URL containing the filter parameters 603 * @param {String} dataRegionName String name of the data region the column is a part of 604 * @param {String} columnName String name of the column to filter 605 * @return {String} human readable version of the filter 606 */ 607 getFilterDescription : function(url, dataRegionName, columnName) 608 { 609 var params = LABKEY.ActionURL.getParameters(url); 610 var result = ""; 611 var separator = ""; 612 for (var paramName in params) 613 { 614 // Look for parameters that have the right prefix 615 if (paramName.indexOf(dataRegionName + "." + columnName + "~") == 0) 616 { 617 var filterType = paramName.substring(paramName.indexOf("~") + 1); 618 var values = params[paramName]; 619 if (!LABKEY.Utils.isArray(values)) 620 { 621 values = [values]; 622 } 623 // Get the human readable version, like "Is Less Than" 624 var friendly = urlMap[filterType]; 625 var displayText; 626 if (!friendly) 627 { 628 displayText = filterType; 629 } 630 else 631 { 632 displayText = friendly.getDisplayText(); 633 } 634 635 for (var j = 0; j < values.length; j++) 636 { 637 // If the same type of filter is applied twice, it will have multiple values 638 result += separator; 639 separator = " AND "; 640 641 result += displayText; 642 result += " "; 643 result += values[j]; 644 } 645 } 646 } 647 return result; 648 }, 649 650 // Create an array of LABKEY.Filter objects from the filter parameters on the URL 651 getFiltersFromUrl : function(url, dataRegionName) 652 { 653 dataRegionName = dataRegionName || 'query'; 654 var params = LABKEY.ActionURL.getParameters(url); 655 var filterArray = []; 656 657 for (var paramName in params) 658 { 659 if (params.hasOwnProperty(paramName)) { 660 // Look for parameters that have the right prefix 661 if (paramName.indexOf(dataRegionName + ".") == 0) 662 { 663 var tilde = paramName.indexOf("~"); 664 665 if (tilde != -1) 666 { 667 var columnName = paramName.substring(dataRegionName.length + 1, tilde); 668 var filterName = paramName.substring(tilde + 1); 669 var filterType = LABKEY.Filter.getFilterTypeForURLSuffix(filterName); 670 var values = params[paramName]; 671 // Create separate Filter objects if the filter parameter appears on the URL more than once. 672 if (LABKEY.Utils.isArray(values)) 673 { 674 for (var i = 0; i < values.length; i++) { 675 filterArray.push(LABKEY.Filter.create(columnName, values[i], filterType)); 676 } 677 } 678 else 679 { 680 filterArray.push(LABKEY.Filter.create(columnName, values, filterType)); 681 } 682 } 683 } 684 } 685 } 686 return filterArray; 687 }, 688 689 getSortFromUrl : function(url, dataRegionName) 690 { 691 dataRegionName = dataRegionName || 'query'; 692 693 var params = LABKEY.ActionURL.getParameters(url); 694 return params[dataRegionName + "." + "sort"]; 695 }, 696 697 getQueryParamsFromUrl : function(url, dataRegionName) 698 { 699 dataRegionName = dataRegionName || 'query'; 700 701 var queryParams = {}; 702 var params = LABKEY.ActionURL.getParameters(url); 703 for (var paramName in params) 704 { 705 if (params.hasOwnProperty(paramName)) 706 { 707 if (paramName.indexOf(dataRegionName + "." + "param.") == 0) 708 { 709 var queryParamName = paramName.substring((dataRegionName + "." + "param.").length); 710 queryParams[queryParamName] = params[paramName]; 711 } 712 } 713 } 714 715 return queryParams; 716 }, 717 718 getFilterTypeForURLSuffix : function (urlSuffix) 719 { 720 return urlMap[urlSuffix]; 721 } 722 }; 723 724 var ft = ret.Types; 725 var filterTypes = { 726 "int":[ft.HAS_ANY_VALUE, ft.EQUAL, ft.NEQ_OR_NULL, ft.ISBLANK, ft.NONBLANK, ft.GT, ft.LT, ft.GTE, ft.LTE, ft.IN, ft.NOT_IN, ft.BETWEEN, ft.NOT_BETWEEN], 727 "string":[ft.HAS_ANY_VALUE, ft.EQUAL, ft.NEQ_OR_NULL, ft.ISBLANK, ft.NONBLANK, ft.GT, ft.LT, ft.GTE, ft.LTE, ft.CONTAINS, ft.DOES_NOT_CONTAIN, ft.DOES_NOT_START_WITH, ft.STARTS_WITH, ft.IN, ft.NOT_IN, ft.CONTAINS_ONE_OF, ft.CONTAINS_NONE_OF, ft.BETWEEN, ft.NOT_BETWEEN], 728 "boolean":[ft.HAS_ANY_VALUE, ft.EQUAL, ft.NEQ_OR_NULL, ft.ISBLANK, ft.NONBLANK], 729 "float":[ft.HAS_ANY_VALUE, ft.EQUAL, ft.NEQ_OR_NULL, ft.ISBLANK, ft.NONBLANK, ft.GT, ft.LT, ft.GTE, ft.LTE, ft.IN, ft.NOT_IN, ft.BETWEEN, ft.NOT_BETWEEN], 730 "date":[ft.HAS_ANY_VALUE, ft.DATE_EQUAL, ft.DATE_NOT_EQUAL, ft.ISBLANK, ft.NONBLANK, ft.DATE_GREATER_THAN, ft.DATE_LESS_THAN, ft.DATE_GREATER_THAN_OR_EQUAL, ft.DATE_LESS_THAN_OR_EQUAL] 731 }; 732 733 var defaultFilter = { 734 "int": ft.EQUAL, 735 "string": ft.CONTAINS, 736 "boolean": ft.EQUAL, 737 "float": ft.EQUAL, 738 "date": ft.DATE_EQUAL 739 }; 740 741 /** @private Returns an Array of filter types that can be used with the given json type ("int", "double", "string", "boolean", "date") */ 742 ret.getFilterTypesForType = function (type, mvEnabled) 743 { 744 var types = []; 745 if (filterTypes[type]) 746 types = types.concat(filterTypes[type]); 747 748 if (mvEnabled) 749 { 750 types.push(ft.HAS_MISSING_VALUE); 751 types.push(ft.DOES_NOT_HAVE_MISSING_VALUE); 752 } 753 754 return types; 755 }; 756 757 /** @private Return the default LABKEY.Filter.Type for a json type ("int", "double", "string", "boolean", "date"). */ 758 ret.getDefaultFilterForType = function (type) 759 { 760 if (defaultFilter[type]) 761 return defaultFilter[type]; 762 763 return ft.EQUAL; 764 }; 765 766 return ret; 767 }; 768 769 /** 770 * @name LABKEY.Filter.FilterDefinition 771 * @description Static class that defines the functions that describe how a particular 772 * type of filter is identified and operates. See {@link LABKEY.Filter}. 773 * <p>Additional Documentation: 774 * <ul> 775 * <li><a href="https://www.labkey.org/Documentation/wiki-page.view?name=filteringData">Filter via the LabKey UI</a></li> 776 * </ul> 777 * </p> 778 * @class Static class that defines the functions that describe how a particular 779 * type of filter is identified and operates. See {@link LABKEY.Filter}. 780 * <p>Additional Documentation: 781 * <ul> 782 * <li><a href="https://www.labkey.org/Documentation/wiki-page.view?name=filteringData">Filter via the LabKey UI</a></li> 783 * </ul> 784 * </p> 785 */ 786 787 /**#@+ 788 * @methodOf LABKEY.Filter.FilterDefinition# 789 */ 790 791 /** 792 * Get the string displayed for this filter. 793 * @name getDisplayText 794 * @type String 795 */ 796 797 /** 798 * Get the more descriptive string displayed for this filter. This is used in filter dialogs. 799 * @name getLongDisplayText 800 * @type String 801 */ 802 803 /** 804 * Get the URL suffix used to identify this filter. 805 * @name getURLSuffix 806 * @type String 807 */ 808 809 /** 810 * Get the Boolean that indicates whether a data value is required. 811 * @name isDataValueRequired 812 * @type Boolean 813 */ 814 815 /** 816 * Get the Boolean that indicates whether the filter supports a string with multiple filter values (ie. contains one of, not in, etc). 817 * @name isMultiValued 818 * @type Boolean 819 */ 820 821 /** 822 * Get the LABKEY.Filter.FilterDefinition the represents the opposite of this filter type. 823 * @name getOpposite 824 * @type LABKEY.Filter.FilterDefinition 825 */ 826 827 /**#@-*/ 828