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