From b0cf34dac30ad93151df53aa2e3dc337d6d50b4d Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Mon, 16 Feb 2015 21:43:30 -0700 Subject: [PATCH 01/11] Initial query builder interface for analytics queries. This uses the jQuery-QueryBuilder plugin to provide an easier query building interface for constructing analytics queries. This is intended to finally provide a nicer alternative to the raw "Advanced filters" query syntax, which is just a straight ElasticSearch query. I still need to figure out how the interface for this will fit in, but it basically seems to work. --- app/assets/javascripts/admin.js | 1 + .../controllers/stats/base_controller.js | 1 + .../controllers/stats/drilldown_controller.js | 2 +- .../controllers/stats/logs_controller.js | 2 +- .../admin/controllers/stats/map_controller.js | 2 +- .../controllers/stats/users_controller.js | 2 +- .../admin/routes/stats/base_route.js | 2 +- .../admin/routes/stats/drilldown_route.js | 2 +- .../admin/routes/stats/logs_route.js | 2 +- .../admin/routes/stats/map_route.js | 2 +- .../admin/routes/stats/users_route.js | 2 +- .../admin/templates/stats/_query_form.hbs | 7 +- .../admin/views/stats/logs_table_view.js | 2 +- .../admin/views/stats/query_form_view.js | 178 ++ .../admin/views/stats/users_table_view.js | 2 +- .../vendor/jQuery-QueryBuilder/i18n/da.js | 37 + .../vendor/jQuery-QueryBuilder/i18n/de.js | 37 + .../vendor/jQuery-QueryBuilder/i18n/en.js | 59 + .../vendor/jQuery-QueryBuilder/i18n/es.js | 59 + .../vendor/jQuery-QueryBuilder/i18n/fr.js | 59 + .../vendor/jQuery-QueryBuilder/i18n/it.js | 37 + .../vendor/jQuery-QueryBuilder/i18n/nl.js | 59 + .../vendor/jQuery-QueryBuilder/i18n/pl.js | 59 + .../vendor/jQuery-QueryBuilder/i18n/pt-BR.js | 59 + .../vendor/jQuery-QueryBuilder/i18n/ro.js | 37 + .../jQuery-QueryBuilder/query-builder.css | 131 + .../jQuery-QueryBuilder/query-builder.js | 2289 ++++++++++++++ .../jQuery-QueryBuilder/query-builder.min.css | 6 + .../jQuery-QueryBuilder/query-builder.min.js | 7 + .../query-builder.standalone.js | 2623 +++++++++++++++++ .../query-builder.standalone.min.js | 7 + app/assets/stylesheets/admin.css.scss | 1 + app/assets/stylesheets/admin/stats.css.scss | 24 + app/controllers/admin/stats_controller.rb | 4 + .../api/v1/analytics_controller.rb | 1 + app/models/log_search.rb | 98 + 36 files changed, 5888 insertions(+), 14 deletions(-) create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/da.js create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/de.js create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/en.js create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/es.js create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/fr.js create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/it.js create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/nl.js create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pl.js create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pt-BR.js create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/ro.js create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.css create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.js create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.min.css create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.min.js create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.standalone.js create mode 100644 app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.standalone.min.js diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 9f9ea6f9..16786edc 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -31,6 +31,7 @@ //= require livestampjs/livestamp //= require numeral //= require vendor/jquery.blockUI +//= require vendor/jQuery-QueryBuilder/query-builder.standalone.js //= require spinjs //= require vendor/dirtyforms/jquery.dirtyforms //= require vendor/jquery.truncate diff --git a/app/assets/javascripts/admin/controllers/stats/base_controller.js b/app/assets/javascripts/admin/controllers/stats/base_controller.js index f934d8bb..c4ae2648 100644 --- a/app/assets/javascripts/admin/controllers/stats/base_controller.js +++ b/app/assets/javascripts/admin/controllers/stats/base_controller.js @@ -5,6 +5,7 @@ Admin.StatsBaseController = Ember.ObjectController.extend({ actions: { submit: function() { + this.set('query.params.query', JSON.stringify($('#query_builder').queryBuilder('getRules'))); this.set('query.params.search', $('#filter_form input[name=search]').val()); }, }, diff --git a/app/assets/javascripts/admin/controllers/stats/drilldown_controller.js b/app/assets/javascripts/admin/controllers/stats/drilldown_controller.js index ed0cab65..dfc37f1a 100644 --- a/app/assets/javascripts/admin/controllers/stats/drilldown_controller.js +++ b/app/assets/javascripts/admin/controllers/stats/drilldown_controller.js @@ -24,7 +24,7 @@ Admin.StatsDrilldownController = Admin.StatsBaseController.extend({ downloadUrl: function() { return '/api-umbrella/v1/analytics/drilldown.csv?' + $.param(this.get('query.params')) + '&api_key=' + webAdminAjaxApiKey; - }.property('query.params', 'query.params.search', 'query.params.start_at', 'query.params.end_at', 'query.params.prefix'), + }.property('query.params', 'query.params.query', 'query.params.search', 'query.params.start_at', 'query.params.end_at', 'query.params.prefix'), }); Admin.StatsDrilldownDefaultController = Admin.StatsDrilldownController.extend({ diff --git a/app/assets/javascripts/admin/controllers/stats/logs_controller.js b/app/assets/javascripts/admin/controllers/stats/logs_controller.js index 3698c103..4ab3662e 100644 --- a/app/assets/javascripts/admin/controllers/stats/logs_controller.js +++ b/app/assets/javascripts/admin/controllers/stats/logs_controller.js @@ -1,7 +1,7 @@ Admin.StatsLogsController = Admin.StatsBaseController.extend({ downloadUrl: function() { return '/admin/stats/logs.csv?' + $.param(this.get('query.params')); - }.property('query.params', 'query.params.search', 'query.params.interval', 'query.params.start_at', 'query.params.end_at'), + }.property('query.params', 'query.params.query', 'query.params.search', 'query.params.interval', 'query.params.start_at', 'query.params.end_at'), }); Admin.StatsLogsDefaultController = Admin.StatsLogsController.extend({ diff --git a/app/assets/javascripts/admin/controllers/stats/map_controller.js b/app/assets/javascripts/admin/controllers/stats/map_controller.js index 95185ceb..2ee431c9 100644 --- a/app/assets/javascripts/admin/controllers/stats/map_controller.js +++ b/app/assets/javascripts/admin/controllers/stats/map_controller.js @@ -20,7 +20,7 @@ Admin.StatsMapController = Admin.StatsBaseController.extend({ downloadUrl: function() { return '/admin/stats/map.csv?' + $.param(this.get('query.params')); - }.property('query.params', 'query.params.search', 'query.params.start_at', 'query.params.end_at'), + }.property('query.params', 'query.params.query', 'query.params.search', 'query.params.start_at', 'query.params.end_at'), }); Admin.StatsMapDefaultController = Admin.StatsMapController.extend({ diff --git a/app/assets/javascripts/admin/controllers/stats/users_controller.js b/app/assets/javascripts/admin/controllers/stats/users_controller.js index 69fc8975..f73db4fc 100644 --- a/app/assets/javascripts/admin/controllers/stats/users_controller.js +++ b/app/assets/javascripts/admin/controllers/stats/users_controller.js @@ -1,7 +1,7 @@ Admin.StatsUsersController = Admin.StatsBaseController.extend({ downloadUrl: function() { return '/admin/stats/users.csv?' + $.param(this.get('query.params')); - }.property('query.params', 'query.params.search', 'query.params.start_at', 'query.params.end_at'), + }.property('query.params', 'query.params.query', 'query.params.search', 'query.params.start_at', 'query.params.end_at'), }); Admin.StatsUsersDefaultController = Admin.StatsUsersController.extend({ diff --git a/app/assets/javascripts/admin/routes/stats/base_route.js b/app/assets/javascripts/admin/routes/stats/base_route.js index b5f137e4..ab1fb38c 100644 --- a/app/assets/javascripts/admin/routes/stats/base_route.js +++ b/app/assets/javascripts/admin/routes/stats/base_route.js @@ -69,7 +69,7 @@ Admin.StatsBaseRoute = Ember.Route.extend({ this.transitionTo('stats.logs', $.param(newQueryParams)); } } - }.observes('query.params.search', 'query.params.interval', 'query.params.start_at', 'query.params.end_at'), + }.observes('query.params.query', 'query.params.search', 'query.params.interval', 'query.params.start_at', 'query.params.end_at'), actions: { error: function() { diff --git a/app/assets/javascripts/admin/routes/stats/drilldown_route.js b/app/assets/javascripts/admin/routes/stats/drilldown_route.js index 2f7bc817..cd7e6f92 100644 --- a/app/assets/javascripts/admin/routes/stats/drilldown_route.js +++ b/app/assets/javascripts/admin/routes/stats/drilldown_route.js @@ -23,7 +23,7 @@ Admin.StatsDrilldownRoute = Admin.StatsBaseRoute.extend({ this.transitionTo('stats.drilldown', $.param(newQueryParams)); } } - }.observes('query.params.search', 'query.params.interval', 'query.params.start_at', 'query.params.end_at'), + }.observes('query.params.query', 'query.params.search', 'query.params.interval', 'query.params.start_at', 'query.params.end_at'), validateOptions: function() { var valid = true; diff --git a/app/assets/javascripts/admin/routes/stats/logs_route.js b/app/assets/javascripts/admin/routes/stats/logs_route.js index fba4eab7..bcb9b77c 100644 --- a/app/assets/javascripts/admin/routes/stats/logs_route.js +++ b/app/assets/javascripts/admin/routes/stats/logs_route.js @@ -22,7 +22,7 @@ Admin.StatsLogsRoute = Admin.StatsBaseRoute.extend({ this.transitionTo('stats.logs', $.param(newQueryParams)); } } - }.observes('query.params.search', 'query.params.interval', 'query.params.start_at', 'query.params.end_at'), + }.observes('query.params.query', 'query.params.search', 'query.params.interval', 'query.params.start_at', 'query.params.end_at'), validateOptions: function() { var valid = true; diff --git a/app/assets/javascripts/admin/routes/stats/map_route.js b/app/assets/javascripts/admin/routes/stats/map_route.js index 39bf90d1..0996f4ce 100644 --- a/app/assets/javascripts/admin/routes/stats/map_route.js +++ b/app/assets/javascripts/admin/routes/stats/map_route.js @@ -18,7 +18,7 @@ Admin.StatsMapRoute = Admin.StatsBaseRoute.extend({ this.transitionTo('stats.map', $.param(newQueryParams)); } } - }.observes('query.params.search', 'query.params.start_at', 'query.params.end_at', 'query.params.region'), + }.observes('query.params.query', 'query.params.search', 'query.params.start_at', 'query.params.end_at', 'query.params.region'), }); Admin.StatsMapDefaultRoute = Admin.StatsMapRoute.extend({ diff --git a/app/assets/javascripts/admin/routes/stats/users_route.js b/app/assets/javascripts/admin/routes/stats/users_route.js index a824eb3a..ee371f28 100644 --- a/app/assets/javascripts/admin/routes/stats/users_route.js +++ b/app/assets/javascripts/admin/routes/stats/users_route.js @@ -12,7 +12,7 @@ Admin.StatsUsersRoute = Admin.StatsBaseRoute.extend({ this.transitionTo('stats.users', $.param(newQueryParams)); } } - }.observes('query.params.search', 'query.params.start_at', 'query.params.end_at'), + }.observes('query.params.query', 'query.params.search', 'query.params.start_at', 'query.params.end_at'), }); Admin.StatsUsersDefaultRoute = Admin.StatsUsersRoute.extend({ diff --git a/app/assets/javascripts/admin/templates/stats/_query_form.hbs b/app/assets/javascripts/admin/templates/stats/_query_form.hbs index 9430045c..fec9ab07 100644 --- a/app/assets/javascripts/admin/templates/stats/_query_form.hbs +++ b/app/assets/javascripts/admin/templates/stats/_query_form.hbs @@ -1,9 +1,10 @@
-
+
+
- +
Advanced filters use Lucene's Query Syntax. @@ -126,7 +127,7 @@
-
+
{{#if view.enableInterval}}
diff --git a/app/assets/javascripts/admin/views/stats/logs_table_view.js b/app/assets/javascripts/admin/views/stats/logs_table_view.js index 670edf10..1b0e3ba5 100644 --- a/app/assets/javascripts/admin/views/stats/logs_table_view.js +++ b/app/assets/javascripts/admin/views/stats/logs_table_view.js @@ -140,5 +140,5 @@ Admin.LogsTableView = Ember.View.extend({ refreshData: function() { this.$().DataTable().draw(); - }.observes('controller.query.params.search', 'controller.query.params.start_at', 'controller.query.params.end_at'), + }.observes('controller.query.params.query', 'controller.query.params.search', 'controller.query.params.start_at', 'controller.query.params.end_at'), }); diff --git a/app/assets/javascripts/admin/views/stats/query_form_view.js b/app/assets/javascripts/admin/views/stats/query_form_view.js index 260da16c..26578e9a 100644 --- a/app/assets/javascripts/admin/views/stats/query_form_view.js +++ b/app/assets/javascripts/admin/views/stats/query_form_view.js @@ -39,6 +39,184 @@ Admin.StatsQueryFormView = Ember.View.extend({ startDate: moment(this.get('controller.query.params.start_at'), 'YYYY-MM-DD'), endDate: moment(this.get('controller.query.params.end_at'), 'YYYY-MM-DD'), }, _.bind(this.handleDateRangeChange, this)); + + var stringOperators = [ + 'begins_with', + 'not_begins_with', + 'equal', + 'not_equal', + 'contains', + 'not_contains', + 'is_null', + 'is_not_null', + ]; + + var selectOperators = [ + 'equal', + 'not_equal', + 'is_null', + 'is_not_null', + ]; + + var numberOperators = [ + 'equal', + 'not_equal', + 'less', + 'less_or_equal', + 'greater', + 'greater_or_equal', + 'between', + 'is_null', + 'is_not_null', + ]; + + var query = this.get('controller.query.params.query'); + var rules; + if(query) { + var rules = JSON.parse(query); + } + + $('#query_builder').queryBuilder({ + plugins: { + 'filter-description': { + icon: 'fa fa-info-circle', + mode: 'bootbox', + }, + 'bt-tooltip-errors': null + }, + allow_empty: true, + allow_groups: false, + filters: [ + { + id: 'request_method', + label: 'Request: HTTP Method', + description: 'The HTTP method of the request.
Example: GET, POST, PUT, DELETE, etc.', + type: 'string', + operators: selectOperators, + input: 'select', + values: { + 'get': 'GET', + 'post': 'POST', + 'put': 'PUT', + 'delete': 'DELETE', + 'head': 'HEAD', + 'patch': 'PATCH', + 'options': 'OPTIONS', + }, + }, + { + id: 'request_scheme', + label: 'Request: URL Scheme', + description: 'The scheme of the original request URL.
Example:: http or https', + type: 'string', + operators: selectOperators, + input: 'select', + values: { + 'http': 'http', + 'https': 'https', + }, + }, + { + id: 'request_host', + label: 'Request: URL Host', + description: 'The host of the original request URL.
Example: example.com', + type: 'string', + operators: stringOperators, + }, + { + id: 'request_path', + label: 'Request: URL Path', + description: 'The path portion of the original request URL.
Example: /geocode/v1.json', + type: 'string', + operators: stringOperators, + }, + { + id: 'request_url', + label: 'Request: Full URL & Query String', + description: 'The original, complete request URL.
Example: http://example.com/geocode/v1.json?address=1617+Cole+Blvd+Golden+CO
Note: If you want to simply filter on the host or path portion of the URL, your queries will run better if you use the separate "Request: URL Path" or "Request: URL Host" fields.', + type: 'string', + operators: stringOperators, + }, + { + id: 'request_ip', + label: 'Request: IP Address', + description: 'The IP address of the requestor.
Example: 93.184.216.119', + type: 'string', + operators: stringOperators, + }, + { + id: 'request_ip_country', + label: 'Request: IP Country', + description: 'The 2 letter country code (ISO 3166-1) that the IP address geocoded to.
Example: US', + type: 'string', + operators: stringOperators, + }, + { + id: 'request_ip_region', + label: 'Request: IP State/Region', + description: 'The 2 letter state or region code (ISO 3166-2) that the IP address geocoded to.
Example: CO', + type: 'string', + operators: stringOperators, + }, + { + id: 'request_ip_city', + label: 'Request: IP City', + description: 'The name of the city that the IP address geocoded to.
Example: Golden', + type: 'string', + operators: stringOperators, + }, + { + id: 'request_user_agent', + label: 'Request: User Agent', + description: 'The user agent of the requestor.
Example: curl/7.33.0', + type: 'string', + operators: stringOperators, + }, + { + id: 'api_key', + label: 'User: API Key', + description: 'The API key used to make the request.
Example: vfcHB9tOyFKc6YbbdDsE8plxtFHvp9zXIJWAtaep', + type: 'string', + operators: stringOperators, + }, + { + id: 'user_email', + label: 'User: E-mail', + description: 'The e-mail address associated with the API key used to make the request.
Example: john.doe@example.com', + type: 'string', + operators: stringOperators, + }, + { + id: 'user_id', + label: 'User: ID', + description: 'The user ID associated with the API key used to make the request.
Example: ad2d94b6-e0f8-4e26-b1a6-1bc6b12f3d76', + type: 'string', + operators: stringOperators, + }, + { + id: 'response_status', + label: 'Response: HTTP Status Code', + description: 'The HTTP status code returned for the response.
Example: 200, 403, 429, etc.', + type: 'integer', + operators: numberOperators, + }, + { + id: 'response_time', + label: 'Response: Load Time', + description: 'The total amount of time taken to respond to the request (in milliseconds)', + type: 'integer', + operators: numberOperators, + }, + { + id: 'response_content_type', + label: 'Response: Content Type', + description: 'The content type of the response.
Example: application/json; charset=utf-8', + type: 'string', + operators: stringOperators, + }, + ], + rules: rules, + }); }, updateInterval: function() { diff --git a/app/assets/javascripts/admin/views/stats/users_table_view.js b/app/assets/javascripts/admin/views/stats/users_table_view.js index a354b269..d75a574f 100644 --- a/app/assets/javascripts/admin/views/stats/users_table_view.js +++ b/app/assets/javascripts/admin/views/stats/users_table_view.js @@ -89,5 +89,5 @@ Admin.StatsUsersTableView = Ember.View.extend({ refreshData: function() { this.$().DataTable().draw(); - }.observes('controller.query.params.search', 'controller.query.params.start_at', 'controller.query.params.end_at'), + }.observes('controller.query.params.query', 'controller.query.params.search', 'controller.query.params.start_at', 'controller.query.params.end_at'), }); diff --git a/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/da.js b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/da.js new file mode 100644 index 00000000..15630102 --- /dev/null +++ b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/da.js @@ -0,0 +1,37 @@ +/*! + * jQuery QueryBuilder + * Oversat af Jna Borup Coyle, github@coyle.dk + */ + +jQuery.fn.queryBuilder.defaults.set({ lang: { + "add_rule": "Tilføj regel", + "add_group": "Tilføj gruppe", + "delete_rule": "Slet regel", + "delete_group": "Slet gruppe", + + "condition_and": "OG", + "condition_or": "ELLER", + + "filter_select_placeholder": "------", + + "operators": { + "equal": "lig med", + "not_equal": "ikke lige med", + "in": "i", + "not_in": "ikke i", + "less": "mindre", + "less_or_equal": "mindre eller lig med", + "greater": "større", + "greater_or_equal": "større eller lig med", + "begins_with": "begynder med", + "not_begins_with": "begynder ikke med", + "contains": "indeholder", + "not_contains": "indeholder ikke", + "ends_with": "slutter med", + "not_ends_with": "slutter ikke med", + "is_empty": "er tom", + "is_not_empty": "er ikke tom", + "is_null": "er null", + "is_not_null": "er ikke null" + } +}}); \ No newline at end of file diff --git a/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/de.js b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/de.js new file mode 100644 index 00000000..988b7826 --- /dev/null +++ b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/de.js @@ -0,0 +1,37 @@ +/*! + * jQuery QueryBuilder + * German translation + */ + +jQuery.fn.queryBuilder.defaults.set({ lang: { + "add_rule": "neue Regel", + "add_group": "neue Gruppe", + "delete_rule": "löschen", + "delete_group": "löschen", + + "condition_and": "UND", + "condition_or": "ODER", + + "filter_select_placeholder": "------", + + "operators": { + "equal": "gleich", + "not_equal": "ungleich", + "in": "in", + "not_in": "nicht in", + "less": "kleiner", + "less_or_equal": "kleiner gleich", + "greater": "größer", + "greater_or_equal": "größer gleich", + "begins_with": "beginnt mit", + "not_begins_with": "beginnt nicht mit", + "contains": "enthält", + "not_contains": "enthält nicht", + "ends_with": "endet mit", + "not_ends_with": "endet nicht mit", + "is_empty": "ist leer", + "is_not_empty": "ist nicht leer", + "is_null": "ist null", + "is_not_null": "ist nicht null" + } +}}); \ No newline at end of file diff --git a/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/en.js b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/en.js new file mode 100644 index 00000000..513e915a --- /dev/null +++ b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/en.js @@ -0,0 +1,59 @@ +/*! + * jQuery QueryBuilder + * Reference language file + */ + +jQuery.fn.queryBuilder.defaults.set({ lang: { + "add_rule": "Add rule", + "add_group": "Add group", + "delete_rule": "Delete", + "delete_group": "Delete", + + "condition_and": "AND", + "condition_or": "OR", + + "filter_select_placeholder": "------", + + "operators": { + "equal": "equal", + "not_equal": "not equal", + "in": "in", + "not_in": "not in", + "less": "less", + "less_or_equal": "less or equal", + "greater": "greater", + "greater_or_equal": "greater or equal", + "between": "between", + "begins_with": "begins with", + "not_begins_with": "doesn't begin with", + "contains": "contains", + "not_contains": "doesn't contain", + "ends_with": "ends with", + "not_ends_with": "doesn't end with", + "is_empty": "is empty", + "is_not_empty": "is not empty", + "is_null": "is null", + "is_not_null": "is not null" + }, + + "errors": { + "no_filter": "No filter selected", + "empty_group": "The group is empty", + "radio_empty": "No value selected", + "checkbox_empty": "No value selected", + "select_empty": "No value selected", + "string_empty": "Empty value", + "string_exceed_min_length": "Must contain at least {0} characters", + "string_exceed_max_length": "Must not contain more than {0} characters", + "string_invalid_format": "Invalid format ({0})", + "number_nan": "Not a number", + "number_not_integer": "Not an integer", + "number_not_double": "Not a real number", + "number_exceed_min": "Must be greater than {0}", + "number_exceed_max": "Must be lower than {0}", + "number_wrong_step": "Must be a multiple of {0}", + "datetime_invalid": "Invalid date format ({0})", + "datetime_exceed_min": "Must be after {0}", + "datetime_exceed_max": "Must be before {0}" + } +}}); \ No newline at end of file diff --git a/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/es.js b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/es.js new file mode 100644 index 00000000..bc3ea3a7 --- /dev/null +++ b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/es.js @@ -0,0 +1,59 @@ +/*! + * jQuery QueryBuilder + * Spanish translation by "pyarza" + */ + +jQuery.fn.queryBuilder.defaults.set({ lang: { + "add_rule": "Añadir regla", + "add_group": "Añadir grupo", + "delete_rule": "Borrar", + "delete_group": "Borrar", + + "condition_and": "Y", + "condition_or": "O", + + "filter_select_placeholder": "------", + + "operators": { + "equal": "igual", + "not_equal": "distinto", + "in": "en", + "not_in": "no en", + "less": "menor", + "less_or_equal": "menor o igual", + "greater": "mayor", + "greater_or_equal": "mayor o igual", + "between": "entre", + "begins_with": "empieza por", + "not_begins_with": "no empieza por", + "contains": "contiene", + "not_contains": "no contiene", + "ends_with": "acaba con", + "not_ends_with": "no acaba con", + "is_empty": "esta vacio", + "is_not_empty": "no esta vacio", + "is_null": "es nulo", + "is_not_null": "no es nulo" + }, + + "errors": { + "no_filter": "No se ha seleccionado ningun filtro", + "empty_group": "El grupo esta vacio", + "radio_empty": "Ningun valor seleccionado", + "checkbox_empty": "Ningun valor seleccionado", + "select_empty": "Ningun valor seleccionado", + "string_empty": "Cadena vacia", + "string_exceed_min_length": "Debe contener al menos {0} caracteres", + "string_exceed_max_length": "No debe contener mas de {0} caracteres", + "string_invalid_format": "Formato invalido ({0})", + "number_nan": "No es un numero", + "number_not_integer": "No es un numero entero", + "number_not_double": "No es un numero real", + "number_exceed_min": "Debe ser mayor que {0}", + "number_exceed_max": "Debe ser menot que {0}", + "number_wrong_step": "Debe ser multiplo de {0}", + "datetime_invalid": "Formato de fecha invalido ({0})", + "datetime_exceed_min": "Debe ser posterior a {0}", + "datetime_exceed_max": "Debe ser anterior a {0}" + } +}}); diff --git a/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/fr.js b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/fr.js new file mode 100644 index 00000000..72373000 --- /dev/null +++ b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/fr.js @@ -0,0 +1,59 @@ +/*! + * jQuery QueryBuilder + * French translation by Damien "Mistic" Sorel + */ + +jQuery.fn.queryBuilder.defaults.set({ lang: { + "add_rule": "Ajouter une règle", + "add_group": "Ajouter un groupe", + "delete_rule": "Supprimer", + "delete_group": "Supprimer", + + "condition_and": "ET", + "condition_or": "OU", + + "filter_select_placeholder": "------", + + "operators": { + "equal": "égal", + "not_equal": "non égal", + "in": "dans", + "not_in": "pas dans", + "less": "inférieur", + "less_or_equal": "inférieur ou égal", + "greater": "supérieur", + "greater_or_equal": "supérieur ou égal", + "between": "entre", + "begins_with": "commence par", + "not_begins_with": "ne commence pas par", + "contains": "contient", + "not_contains": "ne contient pas", + "ends_with": "finit par", + "not_ends_with": "ne finit pas par", + "is_empty": "est vide", + "is_not_empty": "n'est pas vide", + "is_null": "est nul", + "is_not_null": "n'est pas nul" + }, + + "errors": { + "no_filter": "Aucun filtre sélectionné", + "empty_group": "Le groupe est vide", + "radio_empty": "Pas de valeur selectionnée", + "checkbox_empty": "Pas de valeur selectionnée", + "select_empty": "Pas de valeur selectionnée", + "string_empty": "Valeur vide", + "string_exceed_min_length": "Doit contenir au moins {0} caractères", + "string_exceed_max_length": "Ne doit pas contenir plus de {0} caractères", + "string_invalid_format": "Format invalide ({0})", + "number_nan": "N'est pas un nombre", + "number_not_integer": "N'est pas un entier", + "number_not_double": "N'est pas un nombre réel", + "number_exceed_min": "Doit être plus grand que {0}", + "number_exceed_max": "Doit être plus petit que {0}", + "number_wrong_step": "Doit être un multiple de {0}", + "datetime_invalid": "Fomat de date invalide ({0})", + "datetime_exceed_min": "Doit être après {0}", + "datetime_exceed_max": "Doit être avant {0}" + } +}}); \ No newline at end of file diff --git a/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/it.js b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/it.js new file mode 100644 index 00000000..87a100b1 --- /dev/null +++ b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/it.js @@ -0,0 +1,37 @@ +/*! + * jQuery QueryBuilder + * Italian translation + */ + +jQuery.fn.queryBuilder.defaults.set({ lang: { + "add_rule": "Aggiungi regola", + "add_group": "Aggiungi gruppo", + "delete_rule": "Elimina", + "delete_group": "Elimina", + + "condition_and": "E", + "condition_or": "O", + + "filter_select_placeholder": "------", + + "operators": { + "equal": "uguale", + "not_equal": "non uguale", + "in": "in", + "not_in": "non in", + "less": "minore", + "less_or_equal": "minore o uguale", + "greater": "maggiore", + "greater_or_equal": "maggiore o uguale", + "begins_with": "inizia con", + "not_begins_with": "non inizia con", + "contains": "contiene", + "not_contains": "non contiene", + "ends_with": "finisce con", + "not_ends_with": "non finisce con", + "is_empty": "è vuoto", + "is_not_empty": "non è vuoto", + "is_null": "è nullo", + "is_not_null": "non è nullo" + } +}}); \ No newline at end of file diff --git a/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/nl.js b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/nl.js new file mode 100644 index 00000000..3a591f1f --- /dev/null +++ b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/nl.js @@ -0,0 +1,59 @@ +/*! + * jQuery QueryBuilder + * Dutch translation by "Roywcm" + */ + +jQuery.fn.queryBuilder.defaults.set({ lang: { + "add_rule": "Nieuwe regel", + "add_group": "Nieuwe groep", + "delete_rule": "Verwijder", + "delete_group": "Verwijder", + + "condition_and": "EN", + "condition_or": "OF", + + "filter_select_placeholder": "------", + + "operators": { + "equal": "gelijk", + "not_equal": "niet gelijk", + "in": "in", + "not_in": "niet in", + "less": "minder", + "less_or_equal": "minder of gelijk", + "greater": "groter", + "greater_or_equal": "groter of gelijk", + "between": "tussen", + "begins_with": "begint met", + "not_begins_with": "begint niet met", + "contains": "bevat", + "not_contains": "bevat niet", + "ends_with": "eindigt met", + "not_ends_with": "eindigt niet met", + "is_empty": "is leeg", + "is_not_empty": "is niet leeg", + "is_null": "is null", + "is_not_null": "is niet null" + }, + + "errors": { + "no_filter": "Geen filter geselecteerd", + "empty_group": "De groep is leeg", + "radio_empty": "Geen waarde geselecteerd", + "checkbox_empty": "Geen waarde geselecteerd", + "select_empty": "Geen waarde geselecteerd", + "string_empty": "Lege waarde", + "string_exceed_min_length": "Dient minstens {0} karakters te bevatten", + "string_exceed_max_length": "Dient niet meer dan {0} karakters te bevatten", + "string_invalid_format": "Ongeldig format ({0})", + "number_nan": "Niet een nummer", + "number_not_integer": "Geen geheel getal", + "number_not_double": "Geen echt nummer", + "number_exceed_min": "Dient groter te zijn dan {0}", + "number_exceed_max": "Dient lager te zijn dan {0}", + "number_wrong_step": "Dient een veelvoud te zijn van {0}", + "datetime_invalid": "Ongeldige datumformat ({0})", + "datetime_exceed_min": "Dient na {0}", + "datetime_exceed_max": "Dient voor {0}" + } +}}); \ No newline at end of file diff --git a/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pl.js b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pl.js new file mode 100644 index 00000000..475d25e2 --- /dev/null +++ b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pl.js @@ -0,0 +1,59 @@ +/*! + * jQuery QueryBuilder + * Polish translation by Artur Smolarek + */ + +jQuery.fn.queryBuilder.defaults.set({ lang: { + "add_rule": "Dodaj regułę", + "add_group": "Dodaj grupę", + "delete_rule": "Usuń", + "delete_group": "Usuń", + + "condition_and": "AND", + "condition_or": "OR", + + "filter_select_placeholder": "------", + + "operators": { + "equal": "równa się", + "not_equal": "jest różne od", + "in": "zawiera", + "not_in": "nie zawiera", + "less": "mniejsze", + "less_or_equal": "mniejsze lub równe", + "greater": "większe", + "greater_or_equal": "większe lub równe", + "between": "pomiędzy", + "begins_with": "rozpoczyna się od", + "not_begins_with": "nie rozpoczyna się od", + "contains": "zawiera", + "not_contains": "nie zawiera", + "ends_with": "kończy się na", + "not_ends_with": "nie kończy się na", + "is_empty": "jest puste", + "is_not_empty": "nie jest puste", + "is_null": "jest niezdefiniowane", + "is_not_null": "nie jest niezdefiniowane" + }, + + "errors": { + "no_filter": "Nie wybrano żadnego filtra", + "empty_group": "Grupa jest pusta", + "radio_empty": "Nie wybrano wartości", + "checkbox_empty": "Nie wybrano wartości", + "select_empty": "Nie wybrano wartości", + "string_empty": "Nie wpisano wartości", + "string_exceed_min_length": "Minimalna długość to {0} znaków", + "string_exceed_max_length": "Maksymalna długość to {0} znaków", + "string_invalid_format": "Nieprawidłowy format ({0})", + "number_nan": "To nie jest liczba", + "number_not_integer": "To nie jest liczba całkowita", + "number_not_double": "To nie jest liczba rzeczywista", + "number_exceed_min": "Musi być większe niż {0}", + "number_exceed_max": "Musi być mniejsze niż {0}", + "number_wrong_step": "Musi być wielokrotnością {0}", + "datetime_invalid": "Nieprawidłowy format daty ({0})", + "datetime_exceed_min": "Musi być po {0}", + "datetime_exceed_max": "Musi być przed {0}" + } +}}); \ No newline at end of file diff --git a/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pt-BR.js b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pt-BR.js new file mode 100644 index 00000000..1aeed08e --- /dev/null +++ b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/pt-BR.js @@ -0,0 +1,59 @@ +/*! + * jQuery QueryBuilder + * Portuguese Brazilian translation by Leandro Gehlen (leandrogehlen@gmail.com) + */ + +jQuery.fn.queryBuilder.defaults.set({ lang: { + "add_rule": "Nova Regra", + "add_group": "Novo Gruop", + "delete_rule": "Excluir", + "delete_group": "Excluir", + + "condition_and": "E", + "condition_or": "OU", + + "filter_select_placeholder": "------", + + "operators": { + "equal": "Igual", + "not_equal": "Diferente", + "in": "Contido", + "not_in": "Não contido", + "less": "Menor", + "less_or_equal": "Menor ou igual", + "greater": "Maior", + "greater_or_equal": "Maior ou igual", + "between": "entre", + "begins_with": "Iniciando com", + "not_begins_with": "Não iniciando com", + "contains": "Contém", + "not_contains": "Não contém", + "ends_with": "Terminando com", + "not_ends_with": "Terminando sem", + "is_empty": "É vazio", + "is_not_empty": "Não é vazio", + "is_null": "É nulo", + "is_not_null": "Não é nulo" + }, + + "errors": { + "no_filter": "Nenhum filtro selecionado", + "empty_group": "O grupo está vazio", + "radio_empty": "Nenhum valor selecionado", + "checkbox_empty": "Nenhum valor selecionado", + "select_empty": "Nenhum valor selecionado", + "string_empty": "Valor vazio", + "string_exceed_min_length": "É necessário conter pelo menos {0} caracteres", + "string_exceed_max_length": "É necessário conterm mais de {0} caracteres", + "string_invalid_format": "Formato inválido ({0})", + "number_nan": "Não é um número", + "number_not_integer": "Não é um número inteiro", + "number_not_double": "Não é um número real", + "number_exceed_min": "É necessário ser maior que {0}", + "number_exceed_max": "É necessário ser menor que {0}", + "number_wrong_step": "É necessário ser múltiplo de {0}", + "datetime_invalid": "Formato de data inválido ({0})", + "datetime_exceed_min": "É necessário ser superior a {0}", + "datetime_exceed_max": "É necessário ser inferior a {0}" + } +}}); \ No newline at end of file diff --git a/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/ro.js b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/ro.js new file mode 100644 index 00000000..477d8cf6 --- /dev/null +++ b/app/assets/javascripts/vendor/jQuery-QueryBuilder/i18n/ro.js @@ -0,0 +1,37 @@ +/*! + * jQuery QueryBuilder + * Romanian translation by ArianServ + */ + +jQuery.fn.queryBuilder.defaults.set({ lang: { + "add_rule": "Adaugă regulă", + "add_group": "Adaugă grup", + "delete_rule": "Şterge", + "delete_group": "Şterge", + + "condition_and": "ŞI", + "condition_or": "SAU", + + "filter_select_placeholder": "------", + + "operators": { + "equal": "egal", + "not_equal": "diferit", + "in": "în", + "not_in": "nu în", + "less": "mai puţin", + "less_or_equal": "mai puţin sau egal", + "greater": "mai mare", + "greater_or_equal": "mai mare sau egal", + "begins_with": "începe cu", + "not_begins_with": "nu începe cu", + "contains": "conţine", + "not_contains": "nu conţine", + "ends_with": "se termină cu", + "not_ends_with": "nu se termină cu", + "is_empty": "este gol", + "is_not_empty": "nu este gol", + "is_null": "e nul", + "is_not_null": "nu e nul" + } +}}); \ No newline at end of file diff --git a/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.css b/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.css new file mode 100644 index 00000000..a194d84e --- /dev/null +++ b/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.css @@ -0,0 +1,131 @@ +/*! + * jQuery QueryBuilder 1.4.1 + * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) + * Licensed under MIT (http://opensource.org/licenses/MIT) + */ +.query-builder .rule-container, +.query-builder .rules-group-container, +.query-builder .rule-placeholder { + margin:4px 0; + border-radius:5px; + padding:5px; + border:1px solid #eee; + background:#fff; + background:rgba(255, 255, 255, 0.9); +} + +.query-builder .rules-group-container { + padding:10px 10px 5px 10px; + border:1px solid #DCC896; + background:#FCF9ED; + background:rgba(250, 240, 210, 0.5); +} + .query-builder .rules-group-header { + margin-bottom:10px; + } + .query-builder .rules-group-header input[name$=_cond] { + display:none; + } + .query-builder .rules-list { + list-style:none; + padding:0 0 0 20px; + margin:0; + } + +.query-builder .rule-container {} + .query-builder .rule-container>div:not(.rule-header) { + display:inline-block; + margin:0 5px 0 0; + vertical-align:top; + } + .query-builder .rule-value-container:not(:empty) { + border-left:1px solid #ddd; + padding-left:5px; + } + .query-builder .rule-value-container label { + margin-bottom:0; + } + .query-builder .rule-value-container label.block { + display:block; + } + .query-builder .rule-container select, + .query-builder .rule-container input[type=text], + .query-builder .rule-container input[type=number] { + padding:1px; + } + +.query-builder .has-error { + background:#fdd; + border-color:#f99; +} + +.query-builder .error-container { + display:none !important; + cursor:help; + color:red; +} + +.query-builder .has-error .error-container { + display:inline-block !important; +} + +.query-builder .rules-list>* { + position:relative; +} + .query-builder .rules-list>*:before, + .query-builder .rules-list>*:after { + content:''; + position:absolute; + left:-15px; + width:15px; + height:calc(50% + 4px); + border-color:#ccc; + border-style:solid; + } + + .query-builder .rules-list>*:before { + top:-2px; + border-width:0 0 2px 2px; + } + .query-builder .rules-list>*:after { + top:50%; + border-width:0 0 0 2px; + } + + .query-builder .rules-list>*:first-child:before { + top:-12px; + height:calc(50% + 14px); + } + .query-builder .rules-list>*:last-child:before { + border-radius:0 0 0 4px; + } + .query-builder .rules-list>*:last-child:after { + display:none; + } +.query-builder .tooltip-inner { + color:#fdd !important; +} +.query-builder p.filter-description { + margin:5px 0 0 0; + background:#D9EDF7; + border:1px solid #BCE8F1; + color:#31708F; + border-radius:4px; + padding:2px 5px; + font-size:0.8em; +} +.query-builder .drag-handle { + cursor:move; + display:inline-block; + vertical-align:middle; + margin-left:5px; +} + +.query-builder .dragged { + opacity:0.5; +} + +.query-builder .rule-placeholder { + border:1px dashed #bbb; + opacity:0.7; +} \ No newline at end of file diff --git a/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.js b/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.js new file mode 100644 index 00000000..b3486bb6 --- /dev/null +++ b/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.js @@ -0,0 +1,2289 @@ +/*! + * jQuery QueryBuilder 1.4.1 + * Copyright 2014-2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) + * Licensed under MIT (http://opensource.org/licenses/MIT) + */ + +// Modules: bt-selectpicker, bt-tooltip-errors, filter-description, mongodb-support, sortable, sql-support +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define(['jquery', 'microevent', 'jQuery.extendext'], factory); + } + else { + factory(root.jQuery, root.MicroEvent); + } +}(this, function($, MicroEvent) { + "use strict"; + + var types = [ + 'string', + 'integer', + 'double', + 'date', + 'time', + 'datetime' + ], + internalTypes = [ + 'string', + 'number', + 'datetime' + ], + inputs = [ + 'text', + 'textarea', + 'radio', + 'checkbox', + 'select' + ]; + + + var QueryBuilder = function($el, options) { + this.$el = $el; + this.init(options); + }; + + MicroEvent.mixin(QueryBuilder); + + + QueryBuilder.DEFAULTS = { + filters: [], + + plugins: null, + + onValidationError: null, + onAfterAddGroup: null, + onAfterAddRule: null, + + display_errors: true, + allow_groups: -1, + allow_empty: false, + conditions: ['AND', 'OR'], + default_condition: 'AND', + + default_rule_flags: { + filter_readonly: false, + operator_readonly: false, + value_readonly: false, + no_delete: false + }, + + template: { + group: null, + rule: null + }, + + lang: { + "add_rule": 'Add rule', + "add_group": 'Add group', + "delete_rule": 'Delete', + "delete_group": 'Delete', + + "condition_and": 'AND', + "condition_or": 'OR', + + "filter_select_placeholder": '------', + + "operators": { + "equal": "equal", + "not_equal": "not equal", + "in": "in", + "not_in": "not in", + "less": "less", + "less_or_equal": "less or equal", + "greater": "greater", + "greater_or_equal": "greater or equal", + "between": "between", + "begins_with": "begins with", + "not_begins_with": "doesn't begin with", + "contains": "contains", + "not_contains": "doesn't contain", + "ends_with": "ends with", + "not_ends_with": "doesn't end with", + "is_empty": "is empty", + "is_not_empty": "is not empty", + "is_null": "is null", + "is_not_null": "is not null" + }, + + "errors": { + "no_filter": "No filter selected", + "empty_group": "The group is empty", + "radio_empty": "No value selected", + "checkbox_empty": "No value selected", + "select_empty": "No value selected", + "string_empty": "Empty value", + "string_exceed_min_length": "Must contain at least {0} characters", + "string_exceed_max_length": "Must not contain more than {0} characters", + "string_invalid_format": "Invalid format ({0})", + "number_nan": "Not a number", + "number_not_integer": "Not an integer", + "number_not_double": "Not a real number", + "number_exceed_min": "Must be greater than {0}", + "number_exceed_max": "Must be lower than {0}", + "number_wrong_step": "Must be a multiple of {0}", + "datetime_invalid": "Invalid date format ({0})", + "datetime_exceed_min": "Must be after {0}", + "datetime_exceed_max": "Must be before {0}" + } + }, + + operators: [ + {type: 'equal', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, + {type: 'not_equal', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, + {type: 'in', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, + {type: 'not_in', accept_values: 1, apply_to: ['string', 'number', 'datetime']}, + {type: 'less', accept_values: 1, apply_to: ['number', 'datetime']}, + {type: 'less_or_equal', accept_values: 1, apply_to: ['number', 'datetime']}, + {type: 'greater', accept_values: 1, apply_to: ['number', 'datetime']}, + {type: 'greater_or_equal', accept_values: 1, apply_to: ['number', 'datetime']}, + {type: 'between', accept_values: 2, apply_to: ['number', 'datetime']}, + {type: 'begins_with', accept_values: 1, apply_to: ['string']}, + {type: 'not_begins_with', accept_values: 1, apply_to: ['string']}, + {type: 'contains', accept_values: 1, apply_to: ['string']}, + {type: 'not_contains', accept_values: 1, apply_to: ['string']}, + {type: 'ends_with', accept_values: 1, apply_to: ['string']}, + {type: 'not_ends_with', accept_values: 1, apply_to: ['string']}, + {type: 'is_empty', accept_values: 0, apply_to: ['string']}, + {type: 'is_not_empty', accept_values: 0, apply_to: ['string']}, + {type: 'is_null', accept_values: 0, apply_to: ['string', 'number', 'datetime']}, + {type: 'is_not_null', accept_values: 0, apply_to: ['string', 'number', 'datetime']} + ], + + icons: { + add_group: 'glyphicon glyphicon-plus-sign', + add_rule: 'glyphicon glyphicon-plus', + remove_group: 'glyphicon glyphicon-remove', + remove_rule: 'glyphicon glyphicon-remove', + error: 'glyphicon glyphicon-warning-sign' + } + }; + + + QueryBuilder.plugins = {}; + + /** + * Define a new plugin + * @param {string} + * @param {function} + */ + QueryBuilder.define = function(name, fct) { + QueryBuilder.plugins[name] = fct; + }; + + /** + * Add new methods + * @param {object} + */ + QueryBuilder.extend = function(methods) { + $.extend(QueryBuilder.prototype, methods); + }; + + /** + * Init plugins for an instance + */ + QueryBuilder.prototype.initPlugins = function() { + if (!this.settings.plugins) { + return; + } + + var that = this, + queue = {}; + + if ($.isArray(this.settings.plugins)) { + $.each(this.settings.plugins, function(i, plugin) { + queue[plugin] = {}; + }); + } + else { + $.each(this.settings.plugins, function(plugin, options) { + queue[plugin] = options; + }); + } + + $.each(queue, function(plugin, options) { + if (plugin in QueryBuilder.plugins) { + QueryBuilder.plugins[plugin].call(that, options); + } + else { + $.error('Unable to find plugin "' + plugin +'"'); + } + }); + }; + + + /** + * Init the builder + */ + QueryBuilder.prototype.init = function(options) { + // PROPERTIES + this.settings = $.extendext(true, 'replace', {}, QueryBuilder.DEFAULTS, options); + this.status = { + group_id: 0, + rule_id: 0, + generatedId: false, + has_optgroup: false + }; + + // "allow_groups" changed in 1.3.1 from boolean to int + if (this.settings.allow_groups === false) { + this.settings.allow_groups = 0; + } + else if (this.settings.allow_groups === true) { + this.settings.allow_groups = -1; + } + + this.filters = this.settings.filters; + this.lang = this.settings.lang; + this.icons = this.settings.icons; + this.operators = this.settings.operators; + this.template = this.settings.template; + + if (this.template.group === null) { + this.template.group = this.getGroupTemplate; + } + if (this.template.rule === null) { + this.template.rule = this.getRuleTemplate; + } + + // CHECK FILTERS + if (!this.filters || this.filters.length < 1) { + $.error('Missing filters list'); + } + this.checkFilters(); + + // ensure we have a container id + if (!this.$el.attr('id')) { + this.$el.attr('id', 'qb_'+Math.floor(Math.random()*99999)); + this.status.generatedId = true; + } + this.$el_id = this.$el.attr('id'); + + this.$el.addClass('query-builder'); + + // INIT + this.bindEvents(); + + this.initPlugins(); + + this.trigger('afterInit'); + + if (options.rules) { + this.setRules(options.rules); + } + else { + this.addGroup(this.$el); + } + }; + + /** + * Destroy the plugin + */ + QueryBuilder.prototype.destroy = function() { + this.trigger('beforeDestroy'); + + if (this.status.generatedId) { + this.$el.removeAttr('id'); + } + + this.$el.empty() + .off('click.queryBuilder change.queryBuilder') + .removeClass('query-builder') + .removeData('queryBuilder'); + }; + + /** + * Reset the plugin + */ + QueryBuilder.prototype.reset = function() { + this.status.group_id = 1; + this.status.rule_id = 0; + + this.$el.find('>.rules-group-container>.rules-group-body>.rules-list').empty(); + + this.addRule(this.$el.find('>.rules-group-container')); + + this.trigger('afterReset'); + }; + + /** + * Clear the plugin + */ + QueryBuilder.prototype.clear = function() { + this.status.group_id = 0; + this.status.rule_id = 0; + + this.$el.empty(); + + this.trigger('afterClear'); + }; + + /** + * Get an object representing current rules + * @return {object} + */ + QueryBuilder.prototype.getRules = function() { + this.clearErrors(); + + var $group = this.$el.find('>.rules-group-container'), + that = this; + + var rules = (function parse($group) { + var out = {}, + $elements = $group.find('>.rules-group-body>.rules-list>*'); + + out.condition = that.getGroupCondition($group); + out.rules = []; + + for (var i=0, l=$elements.length; i 1)) { + that.triggerValidationError(['empty_group'], $group, null, null, null); + return {}; + } + + return out; + }($group)); + + return this.change('getRules', rules); + }; + + /** + * Set rules from object + * @param data {object} + */ + QueryBuilder.prototype.setRules = function(data) { + this.clear(); + + if (!data || !data.rules || (data.rules.length===0 && !this.settings.allow_empty)) { + $.error('Incorrect data object passed'); + } + + data = this.change('setRules', data); + + var $container = this.$el, + that = this; + + (function add(data, $container){ + var $group = that.addGroup($container, false); + if ($group === null) { + return; + } + + var $buttons = $group.find('>.rules-group-header [name$=_cond]'); + + if (data.condition === undefined) { + data.condition = that.settings.default_condition; + } + + for (var i=0, l=that.settings.conditions.length; i0) { + if (that.settings.allow_groups !== -1 && that.settings.allow_groups < $group.data('queryBuilder').level) { + that.reset(); + $.error(fmt('No more than {0} groups are allowed', that.settings.allow_groups)); + } + else { + add(rule, $group); + } + } + else { + if (rule.id === undefined) { + $.error('Missing rule field id'); + } + if (rule.value === undefined) { + rule.value = ''; + } + if (rule.operator === undefined) { + rule.operator = 'equal'; + } + + var $rule = that.addRule($group); + if ($rule === null) { + return; + } + + var filter = that.getFilterById(rule.id), + operator = that.getOperatorByType(rule.operator); + + $rule.find('.rule-filter-container [name$=_filter]').val(rule.id).trigger('change'); + $rule.find('.rule-operator-container [name$=_operator]').val(rule.operator).trigger('change'); + + if (operator.accept_values !== 0) { + that.setRuleValue($rule, rule.value, filter, operator); + } + + that.applyRuleFlags($rule, rule); + } + }); + + }(data, $container)); + }; + + + /** + * Checks the configuration of each filter + */ + QueryBuilder.prototype.checkFilters = function() { + var definedFilters = [], + that = this; + + $.each(this.filters, function(i, filter) { + if (!filter.id) { + $.error('Missing filter id: '+ i); + } + if (definedFilters.indexOf(filter.id) != -1) { + $.error('Filter already defined: '+ filter.id); + } + definedFilters.push(filter.id); + + if (!filter.type) { + $.error('Missing filter type: '+ filter.id); + } + if (types.indexOf(filter.type) == -1) { + $.error('Invalid type: '+ filter.type); + } + + if (!filter.input) { + filter.input = 'text'; + } + else if (typeof filter.input != 'function' && inputs.indexOf(filter.input) == -1) { + $.error('Invalid input: '+ filter.input); + } + + if (!filter.field) { + filter.field = filter.id; + } + if (!filter.label) { + filter.label = filter.field; + } + + that.status.has_optgroup|= !!filter.optgroup; + if (!filter.optgroup) { + filter.optgroup = null; + } + + switch (filter.type) { + case 'string': + filter.internalType = 'string'; + break; + case 'integer': case 'double': + filter.internalType = 'number'; + break; + case 'date': case 'time': case 'datetime': + filter.internalType = 'datetime'; + break; + } + + switch (filter.input) { + case 'radio': case 'checkbox': + if (!filter.values || filter.values.length < 1) { + $.error('Missing values for filter: '+ filter.id); + } + break; + } + }); + + // group filters with same optgroup, preserving declaration order when possible + if (this.status.has_optgroup) { + var optgroups = [], + filters = []; + + $.each(this.filters, function(i, filter) { + var idx; + + if (filter.optgroup) { + idx = optgroups.lastIndexOf(filter.optgroup); + + if (idx == -1) { + idx = optgroups.length; + } + } + else { + idx = optgroups.length; + } + + optgroups.splice(idx, 0, filter.optgroup); + filters.splice(idx, 0, filter); + }); + + this.filters = filters; + } + + this.trigger('afterCheckFilters'); + }; + + /** + * Add all events listeners + */ + QueryBuilder.prototype.bindEvents = function() { + var that = this; + + // group condition change + this.$el.on('change.queryBuilder', '.rules-group-header [name$=_cond]', function() { + var $this = $(this); + + if ($this.is(':checked')) { + $this.parent().addClass('active').siblings().removeClass('active'); + } + }); + + // rule filter change + this.$el.on('change.queryBuilder', '.rule-filter-container [name$=_filter]', function() { + var $this = $(this), + $rule = $this.closest('.rule-container'); + + that.updateRuleFilter($rule, $this.val()); + }); + + // rule operator change + this.$el.on('change.queryBuilder', '.rule-operator-container [name$=_operator]', function() { + var $this = $(this), + $rule = $this.closest('.rule-container'); + + that.updateRuleOperator($rule, $this.val()); + }); + + // add rule button + this.$el.on('click.queryBuilder', '[data-add=rule]', function() { + var $this = $(this), + $group = $this.closest('.rules-group-container'); + + that.addRule($group); + }); + + // delete rule button + this.$el.on('click.queryBuilder', '[data-delete=rule]', function() { + var $this = $(this), + $rule = $this.closest('.rule-container'); + + that.deleteRule($rule); + }); + + if (this.settings.allow_groups !== 0) { + // add group button + this.$el.on('click.queryBuilder', '[data-add=group]', function() { + var $this = $(this), + $group = $this.closest('.rules-group-container'); + + that.addGroup($group); + }); + + // delete group button + this.$el.on('click.queryBuilder', '[data-delete=group]', function() { + var $this = $(this), + $group = $this.closest('.rules-group-container'); + + that.deleteGroup($group); + }); + } + }; + + /** + * Add a new rules group + * @param $parent {jQuery} + * @param addRule {bool} (optional - add a default empty rule) + * @return $group {jQuery} + */ + QueryBuilder.prototype.addGroup = function($parent, addRule) { + var group_id = this.nextGroupId(), + level = (($parent.data('queryBuilder') || {}).level || 0) + 1, + $container = level===1 ? $parent : $parent.find('>.rules-group-body>.rules-list'), + $group = $(this.template.group.call(this, group_id, level)); + + $group.data('queryBuilder', {level:level}); + + var e = $.Event('addGroup.queryBuilder', { + group_id: group_id, + level: level, + addRule: addRule, + group: $group, + parent: $parent, + builder: this + }); + + this.$el.trigger(e); + + if (e.isDefaultPrevented()) { + return null; + } + + $container.append($group); + + if (this.settings.onAfterAddGroup) { + this.settings.onAfterAddGroup.call(this, $group); + } + + this.trigger('afterAddGroup', $group); + + if (addRule === undefined || addRule === true) { + this.addRule($group); + } + + return $group; + }; + + /** + * Tries to delete a group. The group is not deleted if at least one rule is no_delete. + * @param $group {jQuery} + * @return {boolean} true if the group has been deleted + */ + QueryBuilder.prototype.deleteGroup = function($group) { + if ($group[0].id == this.$el_id + '_group_0') { + return; + } + + var e = $.Event('deleteGroup.queryBuilder', { + group_id: $group[0].id, + group: $group, + builder: this + }); + + this.$el.trigger(e); + + if (e.isDefaultPrevented()) { + return false; + } + + this.trigger('beforeDeleteGroup', $group); + + var that = this, + keepGroup = false; + + $group.find('>.rules-group-body>.rules-list>*').each(function() { + var $element = $(this); + + if ($element.hasClass('rule-container')) { + if ($element.data('queryBuilder').flags.no_delete) { + keepGroup = true; + } + else { + $element.remove(); + } + } + else { + keepGroup|= !that.deleteGroup($element); + } + }); + + if (!keepGroup) { + $group.remove(); + } + + return !keepGroup; + }; + + /** + * Add a new rule + * @param $parent {jQuery} + * @return $rule {jQuery} + */ + QueryBuilder.prototype.addRule = function($parent) { + var rule_id = this.nextRuleId(), + $container = $parent.find('>.rules-group-body>.rules-list'), + $rule = $(this.template.rule.call(this, rule_id)), + $filterSelect = $(this.getRuleFilterSelect(rule_id)); + + $rule.data('queryBuilder', {flags: {}}); + + var e = $.Event('addRule.queryBuilder', { + rule_id: rule_id, + rule: $rule, + parent: $parent, + builder: this + }); + + this.$el.trigger(e); + + if (e.isDefaultPrevented()) { + return null; + } + + $container.append($rule); + $rule.find('.rule-filter-container').append($filterSelect); + + if (this.settings.onAfterAddRule) { + this.settings.onAfterAddRule.call(this, $rule); + } + + this.trigger('afterAddRule', $rule); + + return $rule; + }; + + /** + * Delete a rule. + * @param $rule {jQuery} + * @return {boolean} true if the rule has been deleted + */ + QueryBuilder.prototype.deleteRule = function($rule) { + var e = $.Event('deleteRule.queryBuilder', { + rule_id: $rule[0].id, + rule: $rule, + builder: this + }); + + this.$el.trigger(e); + + if (e.isDefaultPrevented()) { + return false; + } + + this.trigger('beforeDeleteRule', $rule); + + $rule.remove(); + return true; + }; + + /** + * Create operators for a rule + * @param $rule {jQuery} (
  • element) + * @param filter {object} + */ + QueryBuilder.prototype.createRuleInput = function($rule, filter) { + var $valueContainer = $rule.find('.rule-value-container').empty(); + + if (filter === null) { + return; + } + + var operator = this.getOperatorByType(this.getRuleOperator($rule)); + + if (operator.accept_values === 0) { + return; + } + + var $inputs = $(); + + for (var i=0; i 0) $valueContainer.append(' , '); + $valueContainer.append($ruleInput); + $inputs = $inputs.add($ruleInput); + } + + $valueContainer.show(); + + if (filter.onAfterCreateRuleInput) { + filter.onAfterCreateRuleInput.call(this, $rule, filter); + } + + if (filter.plugin) { + $inputs[filter.plugin](filter.plugin_config || {}); + } + + if (filter.default_value !== undefined) { + this.setRuleValue($rule, filter.default_value, filter, operator); + } + + this.trigger('afterCreateRuleInput', $rule, filter, operator); + }; + + /** + * Perform action when rule's filter is changed + * @param $rule {jQuery} (
  • element) + * @param filterId {string} + */ + QueryBuilder.prototype.updateRuleFilter = function($rule, filterId) { + var filter = filterId != '-1' ? this.getFilterById(filterId) : null; + + this.createRuleOperators($rule, filter); + this.createRuleInput($rule, filter); + + $rule.data('queryBuilder').filter = filter; + + this.trigger('afterUpdateRuleFilter', $rule, filter); + }; + + /** + * Update main visibility when rule operator changes + * @param $rule {jQuery} (
  • element) + * @param operatorType {string} + */ + QueryBuilder.prototype.updateRuleOperator = function($rule, operatorType) { + var $valueContainer = $rule.find('.rule-value-container'), + filter = this.getFilterById(this.getRuleFilter($rule)), + operator = this.getOperatorByType(operatorType); + + if (operator.accept_values === 0) { + $valueContainer.hide(); + } + else { + $valueContainer.show(); + + var previousOperator = $rule.data('queryBuilder').operator; + + if ($valueContainer.is(':empty') || operator.accept_values != previousOperator.accept_values) { + this.createRuleInput($rule, filter); + } + } + + $rule.data('queryBuilder').operator = operator; + + if (filter.onAfterChangeOperator) { + filter.onAfterChangeOperator.call(this, $rule, filter, operator); + } + + this.trigger('afterChangeOperator', $rule, filter, operator); + }; + + /** + * Check if a value is correct for a filter + * @param $rule {jQuery} (
  • element) + * @param value {string|string[]|undefined} + * @param filter {object} + * @param operator {object} + * @return {array|true} + */ + QueryBuilder.prototype.validateValue = function($rule, value, filter, operator) { + var validation = filter.validation || {}, + result = true; + + if (operator.accept_values == 1) { + value = [value]; + } + else { + value = value; + } + + if (validation.callback) { + result = validation.callback.call(this, value, filter, operator, $rule); + return this.change('validateValue', result, $rule, value, filter, operator); + } + + for (var i=0; i validation.max) { + result = ['string_exceed_max_length', validation.max]; + break; + } + } + if (validation.format) { + if (!(validation.format.test(value[i]))) { + result = ['string_invalid_format', validation.format]; + break; + } + } + break; + + case 'number': + if (isNaN(value[i])) { + result = ['number_nan']; + break; + } + if (filter.type == 'integer') { + if (parseInt(value[i]) != value[i]) { + result = ['number_not_integer']; + break; + } + } + else { + if (parseFloat(value[i]) != value[i]) { + result = ['number_not_double']; + break; + } + } + if (validation.min !== undefined) { + if (value[i] < validation.min) { + result = ['number_exceed_min', validation.min]; + break; + } + } + if (validation.max !== undefined) { + if (value[i] > validation.max) { + result = ['number_exceed_max', validation.max]; + break; + } + } + if (validation.step !== undefined) { + var v = value[i]/validation.step; + if (parseInt(v) != v) { + result = ['number_wrong_step', validation.step]; + break; + } + } + break; + + case 'datetime': + // we need MomentJS + if (window.moment && validation.format) { + var datetime = moment(value[i], validation.format); + if (!datetime.isValid()) { + result = ['datetime_invalid']; + break; + } + else { + if (validation.min) { + if (datetime < moment(validation.min, validation.format)) { + result = ['datetime_exceed_min', validation.min]; + break; + } + } + if (validation.max) { + if (datetime > moment(validation.max, validation.format)) { + result = ['datetime_exceed_max', validation.max]; + break; + } + } + } + } + break; + } + } + + if (result !== true) { + break; + } + } + + return this.change('validateValue', result, $rule, value, filter, operator); + }; + + /** + * Remove 'has-error' from everything + */ + QueryBuilder.prototype.clearErrors = function() { + this.$el.find('.has-error').removeClass('has-error'); + }; + + /** + * Trigger a validation error event with custom params + * @param error {array} + * @param $target {jQuery} + * @param value {mixed} + * @param filter {object} + * @param operator {object} + */ + QueryBuilder.prototype.triggerValidationError = function(error, $target, value, filter, operator) { + if (!$.isArray(error)) { + error = [error]; + } + + if (filter && filter.onValidationError) { + filter.onValidationError.call(this, $target, error, value, filter, operator); + } + if (this.settings.onValidationError) { + this.settings.onValidationError.call(this, $target, error, value, filter, operator); + } + + var e = $.Event('validationError.queryBuilder', { + error: error, + filter: filter, + operator: operator, + value: value, + targetRule: $target[0], + builder: this + }); + + this.$el.trigger(e); + + if (this.settings.display_errors && !e.isDefaultPrevented()) { + // translate the text without modifying event array + var errorLoc = $.extend([], error, [ + this.lang.errors[error[0]] || error[0] + ]); + + $target.addClass('has-error'); + var $error = $target.find('.error-container').eq(0); + $error.attr('title', fmt.apply(null, errorLoc)); + } + + this.trigger('validationError', $target, error); + }; + + + /** + * Returns an incremented group ID + * @return {string} + */ + QueryBuilder.prototype.nextGroupId = function() { + return this.$el_id + '_group_' + (this.status.group_id++); + }; + + /** + * Returns an incremented rule ID + * @return {string} + */ + QueryBuilder.prototype.nextRuleId = function() { + return this.$el_id + '_rule_' + (this.status.rule_id++); + }; + + /** + * Returns the operators for a filter + * @param filter {string|object} (filter id name or filter object) + * @return {object[]} + */ + QueryBuilder.prototype.getOperators = function(filter) { + if (typeof filter === 'string') { + filter = this.getFilterById(filter); + } + + var result = []; + + for (var i=0, l=this.operators.length; i element) + * @return {string} + */ + QueryBuilder.prototype.getGroupCondition = function($group) { + return $group.find('>.rules-group-header [name$=_cond]:checked').val(); + }; + + /** + * Returns the selected filter of a rule + * @param $rule {jQuery} (
  • element) + * @return {string} + */ + QueryBuilder.prototype.getRuleFilter = function($rule) { + return $rule.find('.rule-filter-container [name$=_filter]').val(); + }; + + /** + * Returns the selected operator of a rule + * @param $rule {jQuery} (
  • element) + * @return {string} + */ + QueryBuilder.prototype.getRuleOperator = function($rule) { + return $rule.find('.rule-operator-container [name$=_operator]').val(); + }; + + /** + * Returns rule value + * @param $rule {jQuery} (
  • element) + * @param filter {object} (optional - current rule filter) + * @param operator {object} (optional - current rule operator) + * @return {string|string[]|undefined} + */ + QueryBuilder.prototype.getRuleValue = function($rule, filter, operator) { + filter = filter || this.getFilterById(this.getRuleFilter($rule)); + operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); + + var value = [], tmp, + $value = $rule.find('.rule-value-container'); + + for (var i=0; i element) + * @param value {mixed} + * @param filter {object} + * @param operator {object} + */ + QueryBuilder.prototype.setRuleValue = function($rule, value, filter, operator) { + filter = filter || this.getFilterById(this.getRuleFilter($rule)); + operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); + + this.trigger('beforeSetRuleValue', $rule, value, filter, operator); + + if (filter.valueSetter) { + filter.valueSetter.call(this, $rule, value, filter, operator); + } + else { + var $value = $rule.find('.rule-value-container'); + + if (operator.accept_values == 1) { + value = [value]; + } + else { + value = value; + } + + for (var i=0; i element) + * @param rule {object} + */ + QueryBuilder.prototype.applyRuleFlags = function($rule, rule) { + var flags = this.getRuleFlags(rule); + $rule.data('queryBuilder').flags = flags; + + if (flags.filter_readonly) { + $rule.find('[name$=_filter]').prop('disabled', true); + } + if (flags.operator_readonly) { + $rule.find('[name$=_operator]').prop('disabled', true); + } + if (flags.value_readonly) { + $rule.find('[name*=_value_]').prop('disabled', true); + } + if (flags.no_delete) { + $rule.find('[data-delete=rule]').remove(); + } + + this.trigger('afterApplyRuleFlags', $rule, rule, flags); + }; + + + /** + * Returns group HTML + * @param group_id {string} + * @param level {int} + * @return {string} + */ + QueryBuilder.prototype.getGroupTemplate = function(group_id, level) { + var h = '\ +
    \ +
    \ +
    \ + \ + '+ (this.settings.allow_groups===-1 || this.settings.allow_groups>=level ? + '' + :'') +' \ + '+ (level>1 ? + '' + : '') +' \ +
    \ +
    \ + '+ this.getGroupConditions(group_id) +' \ +
    \ + '+ (this.settings.display_errors ? + '
    ' + :'') +'\ +
    \ +
    \ +
      \ +
      \ +
      '; + + return this.change('getGroupTemplate', h, level); + }; + + /** + * Returns group conditions HTML + * @param group_id {string} + * @return {string} + */ + QueryBuilder.prototype.getGroupConditions = function(group_id) { + var h = ''; + + for (var i=0, l=this.settings.conditions.length; i \ + '+ label +' \ + '; + } + + return this.change('getGroupConditions', h); + }; + + /** + * Returns rule HTML + * @param rule_id {string} + * @return {string} + */ + QueryBuilder.prototype.getRuleTemplate = function(rule_id) { + var h = '\ +
    • \ +
      \ +
      \ + \ +
      \ +
      \ + '+ (this.settings.display_errors ? + '
      ' + :'') +'\ +
      \ +
      \ +
      \ +
    • '; + + return this.change('getRuleTemplate', h); + }; + + /** + * Returns rule filter '; + h+= ''; + + $.each(this.filters, function(i, filter) { + if (optgroup != filter.optgroup) { + if (optgroup !== null) h+= ''; + optgroup = filter.optgroup; + if (optgroup !== null) h+= ''; + } + + h+= ''; + }); + + if (optgroup !== null) h+= ''; + h+= ''; + + return this.change('getRuleFilterSelect', h); + }; + + /** + * Returns rule operator '; + + for (var i=0, l=operators.length; i'+ label +''; + } + + h+= ''; + + return this.change('getRuleOperatorSelect', h); + }; + + /** + * Return the rule value HTML + * @param $rule {jQuery} + * @param filter {object} + * @param value_id {int} + * @return {string} + */ + QueryBuilder.prototype.getRuleInput = function($rule, filter, value_id) { + var validation = filter.validation || {}, + name = $rule[0].id +'_value_'+ value_id, + h = '', c; + + if (typeof filter.input === 'function') { + h = filter.input.call(this, $rule, filter, name); + } + else { + switch (filter.input) { + case 'radio': + c = filter.vertical ? ' class=block' : ''; + iterateOptions(filter.values, function(key, val) { + h+= ' '+ val +' '; + }); + break; + + case 'checkbox': + c = filter.vertical ? ' class=block' : ''; + iterateOptions(filter.values, function(key, val) { + h+= ' '+ val +' '; + }); + break; + + case 'select': + h+= ''; + break; + + case 'textarea': + h+= '";break;default:switch(c.internalType){case"number":h+='1&&$.error("Unable to initialize on multiple target");var b=this.data("queryBuilder"),c="object"==typeof a&&a||{};return b||"destroy"!=a?(b||this.data("queryBuilder",new j(this,c)),"string"==typeof a?b[a].apply(b,Array.prototype.slice.call(arguments,1)):this):this},$.fn.queryBuilder.defaults={set:function(a){$.extendext(!0,"replace",j.DEFAULTS,a)},get:function(a){var b=j.DEFAULTS;return a&&(b=b[a]),$.extend(!0,{},b)}},$.fn.queryBuilder.constructor=j,$.fn.queryBuilder.extend=j.extend,$.fn.queryBuilder.define=j.define,$.fn.queryBuilder.define("bt-selectpicker",function(a){$.fn.selectpicker&&$.fn.selectpicker.Constructor||$.error('Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'),a=$.extend({container:"body",style:"btn-inverse btn-xs",width:"auto",showIcon:!1},a||{}),this.on("afterAddRule",function(b){b.find(".rule-filter-container select").selectpicker(a)}),this.on("afterCreateRuleOperators",function(b){b.find(".rule-operator-container select").selectpicker(a)})}),$.fn.queryBuilder.define("bt-tooltip-errors",function(a){$.fn.tooltip&&$.fn.tooltip.Constructor&&$.fn.tooltip.Constructor.prototype.fixTitle||$.error('Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'),a=$.extend({placement:"right"},a||{}),this.on("getRuleTemplate",function(a){return a.replace('class="error-container"','class="error-container" data-toggle="tooltip"')}),this.on("validationError",function(b){b.find(".error-container").eq(0).tooltip(a).tooltip("hide").tooltip("fixTitle")})}),$.fn.queryBuilder.define("filter-description",function(a){a=$.extend({icon:"glyphicon glyphicon-info-sign",mode:"popover"},a||{}),"inline"===a.mode?this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("p.filter-description");c&&c.description?(0===d.length?(d=$('

      '),d.appendTo(b)):d.show(),d.html(' '+c.description)):d.hide()}):"popover"===a.mode?($.fn.popover&&$.fn.popover.Constructor&&$.fn.popover.Constructor.prototype.fixTitle||$.error('Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("button.filter-description");c&&c.description?(0===d.length?(d=$(''),d.prependTo(b.find(".rule-actions")),d.popover({placement:"left",container:"body",html:!0}),d.on("mouseout",function(){d.popover("hide")})):d.show(),d.data("bs.popover").options.content=c.description,d.attr("aria-describedby")&&d.popover("show")):(d.hide(),d.data("bs.popover")&&d.popover("hide"))})):"bootbox"===a.mode&&(window.bootbox||$.error('Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("button.filter-description");c&&c.description?(0===d.length&&(d=$(''),d.prependTo(b.find(".rule-actions")),d.on("click",function(){bootbox.alert(d.data("description"))})),d.data("description",c.description)):d.hide()}))}),$.fn.queryBuilder.defaults.set({mongoOperators:{equal:function(a){return a[0]},not_equal:function(a){return{$ne:a[0]}},"in":function(a){return{$in:a}},not_in:function(a){return{$nin:a}},less:function(a){return{$lt:a[0]}},less_or_equal:function(a){return{$lte:a[0]}},greater:function(a){return{$gt:a[0]}},greater_or_equal:function(a){return{$gte:a[0]}},between:function(a){return{$gte:a[0],$lte:a[1]}},begins_with:function(a){return{$regex:"^"+e(a[0])}},not_begins_with:function(a){return{$regex:"^(?!"+e(a[0])+")"}},contains:function(a){return{$regex:e(a[0])}},not_contains:function(a){return{$regex:"^((?!"+e(a[0])+").)*$",$options:"s"}},ends_with:function(a){return{$regex:e(a[0])+"$"}},not_ends_with:function(a){return{$regex:"(?0)e.push(c(f));else{var g=b.settings.mongoOperators[f.operator],h=b.getOperatorByType(f.operator),i=[];void 0===g&&$.error("MongoDB operation unknown for operator "+f.operator),h.accept_values&&(f.value instanceof Array||(f.value=[f.value]),f.value.forEach(function(a){i.push(d(a,f.type))}));var j={};j[f.field]=g.call(b,i),e.push(j)}});var f={};return e.length>0&&(f["$"+a.condition.toLowerCase()]=e),f}(a)}}),$.fn.queryBuilder.define("sortable",function(a){a=$.extend({default_no_sortable:!1,icon:"glyphicon glyphicon-sort"},a||{}),this.on("afterInit",function(){$.event.props.push("dataTransfer");var a,b,c=this;this.$el.on("mouseover",".drag-handle",function(){c.$el.find(".rule-container, .rules-group-container").attr("draggable",!0)}),this.$el.on("mouseout",".drag-handle",function(){c.$el.find(".rule-container, .rules-group-container").removeAttr("draggable")}),this.$el.on("dragstart","[draggable]",function(c){c.stopPropagation(),c.dataTransfer.setData("text","drag"),b=$(c.target),a=$('
       
      '),a.css("min-height",b.height()),a.insertAfter(b),setTimeout(function(){b.hide()},0)}),this.$el.on("dragenter","[draggable]",function(b){b.preventDefault(),b.stopPropagation(),f(a,$(b.target))}),this.$el.on("dragover","[draggable]",function(a){a.preventDefault(),a.stopPropagation()}),this.$el.on("drop",function(a){a.preventDefault(),a.stopPropagation(),f(b,$(a.target))}),this.$el.on("dragend","[draggable]",function(d){d.preventDefault(),d.stopPropagation(),b.show(),a.remove(),b=a=null,c.$el.find(".rule-container, .rules-group-container").removeAttr("draggable")})}),this.on("getRuleFlags",function(b){return void 0===b.no_sortable&&(b.no_sortable=a.default_no_sortable),b}),this.on("afterApplyRuleFlags",function(a,b,c){c.no_sortable&&a.find(".drag-handle").remove()}),this.on("getGroupTemplate",function(b,c){if(c>1){var d=$(b);d.find(".group-conditions").after('
      '),b=d.prop("outerHTML")}return b}),this.on("getRuleTemplate",function(b){var c=$(b);return c.find(".rule-header").after('
      '),c.prop("outerHTML")})}),$.fn.queryBuilder.defaults.set({sqlOperators:{equal:"= ?",not_equal:"!= ?","in":{op:"IN(?)",list:!0,sep:", "},not_in:{op:"NOT IN(?)",list:!0,sep:", "},less:"< ?",less_or_equal:"<= ?",greater:"> ?",greater_or_equal:">= ?",between:{op:"BETWEEN ?",list:!0,sep:" AND "},begins_with:{op:"LIKE(?)",fn:function(a){return a+"%"}},not_begins_with:{op:"NOT LIKE(?)",fn:function(a){return a+"%"}},contains:{op:"LIKE(?)",fn:function(a){return"%"+a+"%"}},not_contains:{op:"NOT LIKE(?)",fn:function(a){return"%"+a+"%"}},ends_with:{op:"LIKE(?)",fn:function(a){return"%"+a}},not_ends_with:{op:"NOT LIKE(?)",fn:function(a){return"%"+a}},is_empty:'== ""',is_not_empty:'!= ""',is_null:"IS NULL",is_not_null:"IS NOT NULL"}}),$.fn.queryBuilder.extend({getSQL:function(a,b,c){c=void 0===c?this.getRules():c,a=a===!0||void 0===a?"question_mark":a,b=b||void 0===b?"\n":" "; +var e=this,f=1,h=[],i=function j(c){if(c.condition||(c.condition=e.settings.default_condition),-1===["AND","OR"].indexOf(c.condition.toUpperCase())&&$.error("Unable to build SQL query with "+c.condition+" condition"),!c.rules)return"";var i=[];return $.each(c.rules,function(c,k){if(k.rules&&k.rules.length>0)i.push("("+b+j(k)+b+")"+b);else{var l=e.getSqlOperator(k.operator),m=e.getOperatorByType(k.operator),n="";l===!1&&$.error("SQL operation unknown for operator "+k.operator),m.accept_values&&(k.value instanceof Array?!l.list&&k.value.length>1&&$.error("Operator "+k.operator+" cannot accept multiple values"):k.value=[k.value],k.value.forEach(function(b,c){c>0&&(n+=l.sep),"integer"==k.type||"double"==k.type?b=d(b,k.type):a||(b=g(b)),l.fn&&(b=l.fn(b)),a?(n+="question_mark"==a?"?":"$"+f,h.push(b),f++):("string"==typeof b&&(b="'"+b+"'"),n+=b)})),i.push(k.field+" "+l.op.replace(/\?/,n))}}),i.join(" "+c.condition+b)}(c);return a?{sql:i,params:h}:{sql:i}},getSqlOperator:function(a){var b=this.settings.sqlOperators[a];return void 0===b?!1:("string"==typeof b&&(b={op:b}),b.list||(b.list=!1),b.list&&!b.sep&&(b.sep=", "),b)}})}); \ No newline at end of file diff --git a/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.standalone.js b/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.standalone.js new file mode 100644 index 00000000..0e32f20b --- /dev/null +++ b/app/assets/javascripts/vendor/jQuery-QueryBuilder/query-builder.standalone.js @@ -0,0 +1,2623 @@ +/*! + * MicroEvent - to make any js object an event emitter + * Copyright 2011 Jerome Etienne (http://jetienne.com) + * Copyright 2015 Damien "Mistic" Sorel (http://www.strangeplanet.fr) + * Licensed under MIT (http://opensource.org/licenses/MIT) + */ + +(function(root, factory) { + if (typeof module !== 'undefined' && module.exports) { + module.exports = factory(); + } + else if (typeof define === 'function' && define.amd) { + define('microevent', [], factory); + } + else { + root.MicroEvent = factory(); + } +}(this, function() { + "use strict"; + + var MicroEvent = function(){}; + + MicroEvent.prototype = { + /** + * Add one or many event handlers + * + * @param {String,Object} events + * @param {Function} optional, callback + * + * obj.on('event', callback) + * obj.on('event1 event2', callback) + * obj.on({ event1: callback1, event2: callback2 }) + */ + on: function (events, fct) { + this._events = this._events || {}; + + if (typeof events === 'object') { + for (var event in events) { + if (events.hasOwnProperty(event)) { + this._events[event] = this._events[event] || []; + this._events[event].push(events[event]); + } + } + } + else { + events.split(' ').forEach(function(event) { + this._events[event] = this._events[event] || []; + this._events[event].push(fct); + }, this); + } + + return this; + }, + + /** + * Remove one or many or all event handlers + * + * @param {String,Object} optional, events + * @param {Function} optional, callback + * + * obj.off('event') + * obj.off('event', callback) + * obj.off('event1 event2') + * obj.off({ event1: callback1, event2: callback2 }) + * obj.off() + */ + off: function (events, fct) { + this._events = this._events || {}; + + if (typeof events === 'object') { + for (var event in events) { + if (events.hasOwnProperty(event) && (event in this._events)) { + var index = this._events[event].indexOf(events[event]); + if (index !== -1) this._events[event].splice(index, 1); + } + } + } + else if (!!events) { + events.split(' ').forEach(function(event) { + if (event in this._events) { + if (fct) { + var index = this._events[event].indexOf(fct); + if (index !== -1) this._events[event].splice(index, 1); + } + else { + this._events[event] = []; + } + } + }, this); + } + else { + this._events = {}; + } + + return this; + }, + + /** + * Add one or many event handlers that will be called only once + * This handlers are only applicable to "trigger", not "change" + * + * @param {String,Object} events + * @param {Function} optional, callback + * + * obj.once('event', callback) + * obj.once('event1 event2', callback) + * obj.once({ event1: callback1, event2: callback2 }) + */ + once: function (events, fct) { + this._once = this._once || {}; + + if (typeof events === 'object') { + for (var event in events) { + if (events.hasOwnProperty(event)) { + this._once[event] = this._once[event] || []; + this._once[event].push(events[event]); + } + } + } + else { + events.split(' ').forEach(function(event) { + this._once[event] = this._once[event] || []; + this._once[event].push(fct); + }, this); + } + + return this; + }, + + /** + * Trigger all handlers for an event + * + * @param {String} event name + * @param {Mixed...} optional, arguments + */ + trigger: function (event /* , args... */) { + this._events = this._events || {}; + this._once = this._once || {}; + + var args = Array.prototype.slice.call(arguments, 1), + callbacks; + + if (event in this._events) { + callbacks = this._events[event].slice(); + while (callbacks.length) { + callbacks.shift().apply(this, args); + } + } + + if (event in this._once) { + callbacks = this._once[event].slice(); + while (callbacks.length) { + callbacks.shift().apply(this, args); + } + delete this._once[event]; + } + + return this; + }, + + /** + * Trigger all modificators for an event, each handler must return a value + * + * @param {String} event name + * @param {Mixed} event value + * @param {Mixed...} optional, arguments + */ + change: function(event, value /* , args... */) { + this._events = this._events || {}; + + if (event in this._events) { + var args = Array.prototype.slice.call(arguments, 1); + + for (var i=0, l=this._events[event].length; i.rules-group-container>.rules-group-body>.rules-list').empty(); + + this.addRule(this.$el.find('>.rules-group-container')); + + this.trigger('afterReset'); + }; + + /** + * Clear the plugin + */ + QueryBuilder.prototype.clear = function() { + this.status.group_id = 0; + this.status.rule_id = 0; + + this.$el.empty(); + + this.trigger('afterClear'); + }; + + /** + * Get an object representing current rules + * @return {object} + */ + QueryBuilder.prototype.getRules = function() { + this.clearErrors(); + + var $group = this.$el.find('>.rules-group-container'), + that = this; + + var rules = (function parse($group) { + var out = {}, + $elements = $group.find('>.rules-group-body>.rules-list>*'); + + out.condition = that.getGroupCondition($group); + out.rules = []; + + for (var i=0, l=$elements.length; i 1)) { + that.triggerValidationError(['empty_group'], $group, null, null, null); + return {}; + } + + return out; + }($group)); + + return this.change('getRules', rules); + }; + + /** + * Set rules from object + * @param data {object} + */ + QueryBuilder.prototype.setRules = function(data) { + this.clear(); + + if (!data || !data.rules || (data.rules.length===0 && !this.settings.allow_empty)) { + $.error('Incorrect data object passed'); + } + + data = this.change('setRules', data); + + var $container = this.$el, + that = this; + + (function add(data, $container){ + var $group = that.addGroup($container, false); + if ($group === null) { + return; + } + + var $buttons = $group.find('>.rules-group-header [name$=_cond]'); + + if (data.condition === undefined) { + data.condition = that.settings.default_condition; + } + + for (var i=0, l=that.settings.conditions.length; i0) { + if (that.settings.allow_groups !== -1 && that.settings.allow_groups < $group.data('queryBuilder').level) { + that.reset(); + $.error(fmt('No more than {0} groups are allowed', that.settings.allow_groups)); + } + else { + add(rule, $group); + } + } + else { + if (rule.id === undefined) { + $.error('Missing rule field id'); + } + if (rule.value === undefined) { + rule.value = ''; + } + if (rule.operator === undefined) { + rule.operator = 'equal'; + } + + var $rule = that.addRule($group); + if ($rule === null) { + return; + } + + var filter = that.getFilterById(rule.id), + operator = that.getOperatorByType(rule.operator); + + $rule.find('.rule-filter-container [name$=_filter]').val(rule.id).trigger('change'); + $rule.find('.rule-operator-container [name$=_operator]').val(rule.operator).trigger('change'); + + if (operator.accept_values !== 0) { + that.setRuleValue($rule, rule.value, filter, operator); + } + + that.applyRuleFlags($rule, rule); + } + }); + + }(data, $container)); + }; + + + /** + * Checks the configuration of each filter + */ + QueryBuilder.prototype.checkFilters = function() { + var definedFilters = [], + that = this; + + $.each(this.filters, function(i, filter) { + if (!filter.id) { + $.error('Missing filter id: '+ i); + } + if (definedFilters.indexOf(filter.id) != -1) { + $.error('Filter already defined: '+ filter.id); + } + definedFilters.push(filter.id); + + if (!filter.type) { + $.error('Missing filter type: '+ filter.id); + } + if (types.indexOf(filter.type) == -1) { + $.error('Invalid type: '+ filter.type); + } + + if (!filter.input) { + filter.input = 'text'; + } + else if (typeof filter.input != 'function' && inputs.indexOf(filter.input) == -1) { + $.error('Invalid input: '+ filter.input); + } + + if (!filter.field) { + filter.field = filter.id; + } + if (!filter.label) { + filter.label = filter.field; + } + + that.status.has_optgroup|= !!filter.optgroup; + if (!filter.optgroup) { + filter.optgroup = null; + } + + switch (filter.type) { + case 'string': + filter.internalType = 'string'; + break; + case 'integer': case 'double': + filter.internalType = 'number'; + break; + case 'date': case 'time': case 'datetime': + filter.internalType = 'datetime'; + break; + } + + switch (filter.input) { + case 'radio': case 'checkbox': + if (!filter.values || filter.values.length < 1) { + $.error('Missing values for filter: '+ filter.id); + } + break; + } + }); + + // group filters with same optgroup, preserving declaration order when possible + if (this.status.has_optgroup) { + var optgroups = [], + filters = []; + + $.each(this.filters, function(i, filter) { + var idx; + + if (filter.optgroup) { + idx = optgroups.lastIndexOf(filter.optgroup); + + if (idx == -1) { + idx = optgroups.length; + } + } + else { + idx = optgroups.length; + } + + optgroups.splice(idx, 0, filter.optgroup); + filters.splice(idx, 0, filter); + }); + + this.filters = filters; + } + + this.trigger('afterCheckFilters'); + }; + + /** + * Add all events listeners + */ + QueryBuilder.prototype.bindEvents = function() { + var that = this; + + // group condition change + this.$el.on('change.queryBuilder', '.rules-group-header [name$=_cond]', function() { + var $this = $(this); + + if ($this.is(':checked')) { + $this.parent().addClass('active').siblings().removeClass('active'); + } + }); + + // rule filter change + this.$el.on('change.queryBuilder', '.rule-filter-container [name$=_filter]', function() { + var $this = $(this), + $rule = $this.closest('.rule-container'); + + that.updateRuleFilter($rule, $this.val()); + }); + + // rule operator change + this.$el.on('change.queryBuilder', '.rule-operator-container [name$=_operator]', function() { + var $this = $(this), + $rule = $this.closest('.rule-container'); + + that.updateRuleOperator($rule, $this.val()); + }); + + // add rule button + this.$el.on('click.queryBuilder', '[data-add=rule]', function() { + var $this = $(this), + $group = $this.closest('.rules-group-container'); + + that.addRule($group); + }); + + // delete rule button + this.$el.on('click.queryBuilder', '[data-delete=rule]', function() { + var $this = $(this), + $rule = $this.closest('.rule-container'); + + that.deleteRule($rule); + }); + + if (this.settings.allow_groups !== 0) { + // add group button + this.$el.on('click.queryBuilder', '[data-add=group]', function() { + var $this = $(this), + $group = $this.closest('.rules-group-container'); + + that.addGroup($group); + }); + + // delete group button + this.$el.on('click.queryBuilder', '[data-delete=group]', function() { + var $this = $(this), + $group = $this.closest('.rules-group-container'); + + that.deleteGroup($group); + }); + } + }; + + /** + * Add a new rules group + * @param $parent {jQuery} + * @param addRule {bool} (optional - add a default empty rule) + * @return $group {jQuery} + */ + QueryBuilder.prototype.addGroup = function($parent, addRule) { + var group_id = this.nextGroupId(), + level = (($parent.data('queryBuilder') || {}).level || 0) + 1, + $container = level===1 ? $parent : $parent.find('>.rules-group-body>.rules-list'), + $group = $(this.template.group.call(this, group_id, level)); + + $group.data('queryBuilder', {level:level}); + + var e = $.Event('addGroup.queryBuilder', { + group_id: group_id, + level: level, + addRule: addRule, + group: $group, + parent: $parent, + builder: this + }); + + this.$el.trigger(e); + + if (e.isDefaultPrevented()) { + return null; + } + + $container.append($group); + + if (this.settings.onAfterAddGroup) { + this.settings.onAfterAddGroup.call(this, $group); + } + + this.trigger('afterAddGroup', $group); + + if (addRule === undefined || addRule === true) { + this.addRule($group); + } + + return $group; + }; + + /** + * Tries to delete a group. The group is not deleted if at least one rule is no_delete. + * @param $group {jQuery} + * @return {boolean} true if the group has been deleted + */ + QueryBuilder.prototype.deleteGroup = function($group) { + if ($group[0].id == this.$el_id + '_group_0') { + return; + } + + var e = $.Event('deleteGroup.queryBuilder', { + group_id: $group[0].id, + group: $group, + builder: this + }); + + this.$el.trigger(e); + + if (e.isDefaultPrevented()) { + return false; + } + + this.trigger('beforeDeleteGroup', $group); + + var that = this, + keepGroup = false; + + $group.find('>.rules-group-body>.rules-list>*').each(function() { + var $element = $(this); + + if ($element.hasClass('rule-container')) { + if ($element.data('queryBuilder').flags.no_delete) { + keepGroup = true; + } + else { + $element.remove(); + } + } + else { + keepGroup|= !that.deleteGroup($element); + } + }); + + if (!keepGroup) { + $group.remove(); + } + + return !keepGroup; + }; + + /** + * Add a new rule + * @param $parent {jQuery} + * @return $rule {jQuery} + */ + QueryBuilder.prototype.addRule = function($parent) { + var rule_id = this.nextRuleId(), + $container = $parent.find('>.rules-group-body>.rules-list'), + $rule = $(this.template.rule.call(this, rule_id)), + $filterSelect = $(this.getRuleFilterSelect(rule_id)); + + $rule.data('queryBuilder', {flags: {}}); + + var e = $.Event('addRule.queryBuilder', { + rule_id: rule_id, + rule: $rule, + parent: $parent, + builder: this + }); + + this.$el.trigger(e); + + if (e.isDefaultPrevented()) { + return null; + } + + $container.append($rule); + $rule.find('.rule-filter-container').append($filterSelect); + + if (this.settings.onAfterAddRule) { + this.settings.onAfterAddRule.call(this, $rule); + } + + this.trigger('afterAddRule', $rule); + + return $rule; + }; + + /** + * Delete a rule. + * @param $rule {jQuery} + * @return {boolean} true if the rule has been deleted + */ + QueryBuilder.prototype.deleteRule = function($rule) { + var e = $.Event('deleteRule.queryBuilder', { + rule_id: $rule[0].id, + rule: $rule, + builder: this + }); + + this.$el.trigger(e); + + if (e.isDefaultPrevented()) { + return false; + } + + this.trigger('beforeDeleteRule', $rule); + + $rule.remove(); + return true; + }; + + /** + * Create operators for a rule + * @param $rule {jQuery} (
    • element) + * @param filter {object} + */ + QueryBuilder.prototype.createRuleInput = function($rule, filter) { + var $valueContainer = $rule.find('.rule-value-container').empty(); + + if (filter === null) { + return; + } + + var operator = this.getOperatorByType(this.getRuleOperator($rule)); + + if (operator.accept_values === 0) { + return; + } + + var $inputs = $(); + + for (var i=0; i 0) $valueContainer.append(' , '); + $valueContainer.append($ruleInput); + $inputs = $inputs.add($ruleInput); + } + + $valueContainer.show(); + + if (filter.onAfterCreateRuleInput) { + filter.onAfterCreateRuleInput.call(this, $rule, filter); + } + + if (filter.plugin) { + $inputs[filter.plugin](filter.plugin_config || {}); + } + + if (filter.default_value !== undefined) { + this.setRuleValue($rule, filter.default_value, filter, operator); + } + + this.trigger('afterCreateRuleInput', $rule, filter, operator); + }; + + /** + * Perform action when rule's filter is changed + * @param $rule {jQuery} (
    • element) + * @param filterId {string} + */ + QueryBuilder.prototype.updateRuleFilter = function($rule, filterId) { + var filter = filterId != '-1' ? this.getFilterById(filterId) : null; + + this.createRuleOperators($rule, filter); + this.createRuleInput($rule, filter); + + $rule.data('queryBuilder').filter = filter; + + this.trigger('afterUpdateRuleFilter', $rule, filter); + }; + + /** + * Update main visibility when rule operator changes + * @param $rule {jQuery} (
    • element) + * @param operatorType {string} + */ + QueryBuilder.prototype.updateRuleOperator = function($rule, operatorType) { + var $valueContainer = $rule.find('.rule-value-container'), + filter = this.getFilterById(this.getRuleFilter($rule)), + operator = this.getOperatorByType(operatorType); + + if (operator.accept_values === 0) { + $valueContainer.hide(); + } + else { + $valueContainer.show(); + + var previousOperator = $rule.data('queryBuilder').operator; + + if ($valueContainer.is(':empty') || operator.accept_values != previousOperator.accept_values) { + this.createRuleInput($rule, filter); + } + } + + $rule.data('queryBuilder').operator = operator; + + if (filter.onAfterChangeOperator) { + filter.onAfterChangeOperator.call(this, $rule, filter, operator); + } + + this.trigger('afterChangeOperator', $rule, filter, operator); + }; + + /** + * Check if a value is correct for a filter + * @param $rule {jQuery} (
    • element) + * @param value {string|string[]|undefined} + * @param filter {object} + * @param operator {object} + * @return {array|true} + */ + QueryBuilder.prototype.validateValue = function($rule, value, filter, operator) { + var validation = filter.validation || {}, + result = true; + + if (operator.accept_values == 1) { + value = [value]; + } + else { + value = value; + } + + if (validation.callback) { + result = validation.callback.call(this, value, filter, operator, $rule); + return this.change('validateValue', result, $rule, value, filter, operator); + } + + for (var i=0; i validation.max) { + result = ['string_exceed_max_length', validation.max]; + break; + } + } + if (validation.format) { + if (!(validation.format.test(value[i]))) { + result = ['string_invalid_format', validation.format]; + break; + } + } + break; + + case 'number': + if (isNaN(value[i])) { + result = ['number_nan']; + break; + } + if (filter.type == 'integer') { + if (parseInt(value[i]) != value[i]) { + result = ['number_not_integer']; + break; + } + } + else { + if (parseFloat(value[i]) != value[i]) { + result = ['number_not_double']; + break; + } + } + if (validation.min !== undefined) { + if (value[i] < validation.min) { + result = ['number_exceed_min', validation.min]; + break; + } + } + if (validation.max !== undefined) { + if (value[i] > validation.max) { + result = ['number_exceed_max', validation.max]; + break; + } + } + if (validation.step !== undefined) { + var v = value[i]/validation.step; + if (parseInt(v) != v) { + result = ['number_wrong_step', validation.step]; + break; + } + } + break; + + case 'datetime': + // we need MomentJS + if (window.moment && validation.format) { + var datetime = moment(value[i], validation.format); + if (!datetime.isValid()) { + result = ['datetime_invalid']; + break; + } + else { + if (validation.min) { + if (datetime < moment(validation.min, validation.format)) { + result = ['datetime_exceed_min', validation.min]; + break; + } + } + if (validation.max) { + if (datetime > moment(validation.max, validation.format)) { + result = ['datetime_exceed_max', validation.max]; + break; + } + } + } + } + break; + } + } + + if (result !== true) { + break; + } + } + + return this.change('validateValue', result, $rule, value, filter, operator); + }; + + /** + * Remove 'has-error' from everything + */ + QueryBuilder.prototype.clearErrors = function() { + this.$el.find('.has-error').removeClass('has-error'); + }; + + /** + * Trigger a validation error event with custom params + * @param error {array} + * @param $target {jQuery} + * @param value {mixed} + * @param filter {object} + * @param operator {object} + */ + QueryBuilder.prototype.triggerValidationError = function(error, $target, value, filter, operator) { + if (!$.isArray(error)) { + error = [error]; + } + + if (filter && filter.onValidationError) { + filter.onValidationError.call(this, $target, error, value, filter, operator); + } + if (this.settings.onValidationError) { + this.settings.onValidationError.call(this, $target, error, value, filter, operator); + } + + var e = $.Event('validationError.queryBuilder', { + error: error, + filter: filter, + operator: operator, + value: value, + targetRule: $target[0], + builder: this + }); + + this.$el.trigger(e); + + if (this.settings.display_errors && !e.isDefaultPrevented()) { + // translate the text without modifying event array + var errorLoc = $.extend([], error, [ + this.lang.errors[error[0]] || error[0] + ]); + + $target.addClass('has-error'); + var $error = $target.find('.error-container').eq(0); + $error.attr('title', fmt.apply(null, errorLoc)); + } + + this.trigger('validationError', $target, error); + }; + + + /** + * Returns an incremented group ID + * @return {string} + */ + QueryBuilder.prototype.nextGroupId = function() { + return this.$el_id + '_group_' + (this.status.group_id++); + }; + + /** + * Returns an incremented rule ID + * @return {string} + */ + QueryBuilder.prototype.nextRuleId = function() { + return this.$el_id + '_rule_' + (this.status.rule_id++); + }; + + /** + * Returns the operators for a filter + * @param filter {string|object} (filter id name or filter object) + * @return {object[]} + */ + QueryBuilder.prototype.getOperators = function(filter) { + if (typeof filter === 'string') { + filter = this.getFilterById(filter); + } + + var result = []; + + for (var i=0, l=this.operators.length; i element) + * @return {string} + */ + QueryBuilder.prototype.getGroupCondition = function($group) { + return $group.find('>.rules-group-header [name$=_cond]:checked').val(); + }; + + /** + * Returns the selected filter of a rule + * @param $rule {jQuery} (
    • element) + * @return {string} + */ + QueryBuilder.prototype.getRuleFilter = function($rule) { + return $rule.find('.rule-filter-container [name$=_filter]').val(); + }; + + /** + * Returns the selected operator of a rule + * @param $rule {jQuery} (
    • element) + * @return {string} + */ + QueryBuilder.prototype.getRuleOperator = function($rule) { + return $rule.find('.rule-operator-container [name$=_operator]').val(); + }; + + /** + * Returns rule value + * @param $rule {jQuery} (
    • element) + * @param filter {object} (optional - current rule filter) + * @param operator {object} (optional - current rule operator) + * @return {string|string[]|undefined} + */ + QueryBuilder.prototype.getRuleValue = function($rule, filter, operator) { + filter = filter || this.getFilterById(this.getRuleFilter($rule)); + operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); + + var value = [], tmp, + $value = $rule.find('.rule-value-container'); + + for (var i=0; i element) + * @param value {mixed} + * @param filter {object} + * @param operator {object} + */ + QueryBuilder.prototype.setRuleValue = function($rule, value, filter, operator) { + filter = filter || this.getFilterById(this.getRuleFilter($rule)); + operator = operator || this.getOperatorByType(this.getRuleOperator($rule)); + + this.trigger('beforeSetRuleValue', $rule, value, filter, operator); + + if (filter.valueSetter) { + filter.valueSetter.call(this, $rule, value, filter, operator); + } + else { + var $value = $rule.find('.rule-value-container'); + + if (operator.accept_values == 1) { + value = [value]; + } + else { + value = value; + } + + for (var i=0; i element) + * @param rule {object} + */ + QueryBuilder.prototype.applyRuleFlags = function($rule, rule) { + var flags = this.getRuleFlags(rule); + $rule.data('queryBuilder').flags = flags; + + if (flags.filter_readonly) { + $rule.find('[name$=_filter]').prop('disabled', true); + } + if (flags.operator_readonly) { + $rule.find('[name$=_operator]').prop('disabled', true); + } + if (flags.value_readonly) { + $rule.find('[name*=_value_]').prop('disabled', true); + } + if (flags.no_delete) { + $rule.find('[data-delete=rule]').remove(); + } + + this.trigger('afterApplyRuleFlags', $rule, rule, flags); + }; + + + /** + * Returns group HTML + * @param group_id {string} + * @param level {int} + * @return {string} + */ + QueryBuilder.prototype.getGroupTemplate = function(group_id, level) { + var h = '\ +
      \ +
      \ +
      \ + \ + '+ (this.settings.allow_groups===-1 || this.settings.allow_groups>=level ? + '' + :'') +' \ + '+ (level>1 ? + '' + : '') +' \ +
      \ +
      \ + '+ this.getGroupConditions(group_id) +' \ +
      \ + '+ (this.settings.display_errors ? + '
      ' + :'') +'\ +
      \ +
      \ +
        \ +
        \ +
        '; + + return this.change('getGroupTemplate', h, level); + }; + + /** + * Returns group conditions HTML + * @param group_id {string} + * @return {string} + */ + QueryBuilder.prototype.getGroupConditions = function(group_id) { + var h = ''; + + for (var i=0, l=this.settings.conditions.length; i \ + '+ label +' \ + '; + } + + return this.change('getGroupConditions', h); + }; + + /** + * Returns rule HTML + * @param rule_id {string} + * @return {string} + */ + QueryBuilder.prototype.getRuleTemplate = function(rule_id) { + var h = '\ +
      • \ +
        \ +
        \ + \ +
        \ +
        \ + '+ (this.settings.display_errors ? + '
        ' + :'') +'\ +
        \ +
        \ +
        \ +
      • '; + + return this.change('getRuleTemplate', h); + }; + + /** + * Returns rule filter '; + h+= ''; + + $.each(this.filters, function(i, filter) { + if (optgroup != filter.optgroup) { + if (optgroup !== null) h+= ''; + optgroup = filter.optgroup; + if (optgroup !== null) h+= ''; + } + + h+= ''; + }); + + if (optgroup !== null) h+= ''; + h+= ''; + + return this.change('getRuleFilterSelect', h); + }; + + /** + * Returns rule operator '; + + for (var i=0, l=operators.length; i'+ label +''; + } + + h+= ''; + + return this.change('getRuleOperatorSelect', h); + }; + + /** + * Return the rule value HTML + * @param $rule {jQuery} + * @param filter {object} + * @param value_id {int} + * @return {string} + */ + QueryBuilder.prototype.getRuleInput = function($rule, filter, value_id) { + var validation = filter.validation || {}, + name = $rule[0].id +'_value_'+ value_id, + h = '', c; + + if (typeof filter.input === 'function') { + h = filter.input.call(this, $rule, filter, name); + } + else { + switch (filter.input) { + case 'radio': + c = filter.vertical ? ' class=block' : ''; + iterateOptions(filter.values, function(key, val) { + h+= ' '+ val +' '; + }); + break; + + case 'checkbox': + c = filter.vertical ? ' class=block' : ''; + iterateOptions(filter.values, function(key, val) { + h+= ' '+ val +' '; + }); + break; + + case 'select': + h+= ''; + break; + + case 'textarea': + h+= '";break;default:switch(c.internalType){case"number":h+='1&&$.error("Unable to initialize on multiple target");var b=this.data("queryBuilder"),c="object"==typeof a&&a||{};return b||"destroy"!=a?(b||this.data("queryBuilder",new j(this,c)),"string"==typeof a?b[a].apply(b,Array.prototype.slice.call(arguments,1)):this):this},$.fn.queryBuilder.defaults={set:function(a){$.extendext(!0,"replace",j.DEFAULTS,a)},get:function(a){var b=j.DEFAULTS;return a&&(b=b[a]),$.extend(!0,{},b)}},$.fn.queryBuilder.constructor=j,$.fn.queryBuilder.extend=j.extend,$.fn.queryBuilder.define=j.define,$.fn.queryBuilder.define("bt-selectpicker",function(a){$.fn.selectpicker&&$.fn.selectpicker.Constructor||$.error('Bootstrap Select is required to use "bt-selectpicker" plugin. Get it here: http://silviomoreto.github.io/bootstrap-select'),a=$.extend({container:"body",style:"btn-inverse btn-xs",width:"auto",showIcon:!1},a||{}),this.on("afterAddRule",function(b){b.find(".rule-filter-container select").selectpicker(a)}),this.on("afterCreateRuleOperators",function(b){b.find(".rule-operator-container select").selectpicker(a)})}),$.fn.queryBuilder.define("bt-tooltip-errors",function(a){$.fn.tooltip&&$.fn.tooltip.Constructor&&$.fn.tooltip.Constructor.prototype.fixTitle||$.error('Bootstrap Tooltip is required to use "bt-tooltip-errors" plugin. Get it here: http://getbootstrap.com'),a=$.extend({placement:"right"},a||{}),this.on("getRuleTemplate",function(a){return a.replace('class="error-container"','class="error-container" data-toggle="tooltip"')}),this.on("validationError",function(b){b.find(".error-container").eq(0).tooltip(a).tooltip("hide").tooltip("fixTitle")})}),$.fn.queryBuilder.define("filter-description",function(a){a=$.extend({icon:"glyphicon glyphicon-info-sign",mode:"popover"},a||{}),"inline"===a.mode?this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("p.filter-description");c&&c.description?(0===d.length?(d=$('

        '),d.appendTo(b)):d.show(),d.html(' '+c.description)):d.hide()}):"popover"===a.mode?($.fn.popover&&$.fn.popover.Constructor&&$.fn.popover.Constructor.prototype.fixTitle||$.error('Bootstrap Popover is required to use "filter-description" plugin. Get it here: http://getbootstrap.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("button.filter-description");c&&c.description?(0===d.length?(d=$(''),d.prependTo(b.find(".rule-actions")),d.popover({placement:"left",container:"body",html:!0}),d.on("mouseout",function(){d.popover("hide")})):d.show(),d.data("bs.popover").options.content=c.description,d.attr("aria-describedby")&&d.popover("show")):(d.hide(),d.data("bs.popover")&&d.popover("hide"))})):"bootbox"===a.mode&&(window.bootbox||$.error('Bootbox is required to use "filter-description" plugin. Get it here: http://bootboxjs.com'),this.on("afterUpdateRuleFilter",function(b,c){var d=b.find("button.filter-description");c&&c.description?(0===d.length&&(d=$(''),d.prependTo(b.find(".rule-actions")),d.on("click",function(){bootbox.alert(d.data("description"))})),d.data("description",c.description)):d.hide()}))}),$.fn.queryBuilder.defaults.set({mongoOperators:{equal:function(a){return a[0]},not_equal:function(a){return{$ne:a[0]}},"in":function(a){return{$in:a}},not_in:function(a){return{$nin:a}},less:function(a){return{$lt:a[0]}},less_or_equal:function(a){return{$lte:a[0]}},greater:function(a){return{$gt:a[0]}},greater_or_equal:function(a){return{$gte:a[0]}},between:function(a){return{$gte:a[0],$lte:a[1]}},begins_with:function(a){return{$regex:"^"+e(a[0])}},not_begins_with:function(a){return{$regex:"^(?!"+e(a[0])+")"}},contains:function(a){return{$regex:e(a[0])}},not_contains:function(a){return{$regex:"^((?!"+e(a[0])+").)*$",$options:"s"}},ends_with:function(a){return{$regex:e(a[0])+"$"}},not_ends_with:function(a){return{$regex:"(?0)e.push(c(f));else{var g=b.settings.mongoOperators[f.operator],h=b.getOperatorByType(f.operator),i=[];void 0===g&&$.error("MongoDB operation unknown for operator "+f.operator),h.accept_values&&(f.value instanceof Array||(f.value=[f.value]),f.value.forEach(function(a){i.push(d(a,f.type))}));var j={};j[f.field]=g.call(b,i),e.push(j)}});var f={};return e.length>0&&(f["$"+a.condition.toLowerCase()]=e),f}(a)}}),$.fn.queryBuilder.define("sortable",function(a){a=$.extend({default_no_sortable:!1,icon:"glyphicon glyphicon-sort"},a||{}),this.on("afterInit",function(){$.event.props.push("dataTransfer");var a,b,c=this;this.$el.on("mouseover",".drag-handle",function(){c.$el.find(".rule-container, .rules-group-container").attr("draggable",!0)}),this.$el.on("mouseout",".drag-handle",function(){c.$el.find(".rule-container, .rules-group-container").removeAttr("draggable")}),this.$el.on("dragstart","[draggable]",function(c){c.stopPropagation(),c.dataTransfer.setData("text","drag"),b=$(c.target),a=$('
         
        '),a.css("min-height",b.height()),a.insertAfter(b),setTimeout(function(){b.hide()},0)}),this.$el.on("dragenter","[draggable]",function(b){b.preventDefault(),b.stopPropagation(),f(a,$(b.target))}),this.$el.on("dragover","[draggable]",function(a){a.preventDefault(),a.stopPropagation()}),this.$el.on("drop",function(a){a.preventDefault(),a.stopPropagation(),f(b,$(a.target))}),this.$el.on("dragend","[draggable]",function(d){d.preventDefault(),d.stopPropagation(),b.show(),a.remove(),b=a=null,c.$el.find(".rule-container, .rules-group-container").removeAttr("draggable")})}),this.on("getRuleFlags",function(b){return void 0===b.no_sortable&&(b.no_sortable=a.default_no_sortable),b}),this.on("afterApplyRuleFlags",function(a,b,c){c.no_sortable&&a.find(".drag-handle").remove()}),this.on("getGroupTemplate",function(b,c){if(c>1){var d=$(b);d.find(".group-conditions").after('
        '),b=d.prop("outerHTML")}return b}),this.on("getRuleTemplate",function(b){var c=$(b);return c.find(".rule-header").after('
        '),c.prop("outerHTML")})}),$.fn.queryBuilder.defaults.set({sqlOperators:{equal:"= ?",not_equal:"!= ?","in":{op:"IN(?)",list:!0,sep:", "},not_in:{op:"NOT IN(?)",list:!0,sep:", "},less:"< ?",less_or_equal:"<= ?",greater:"> ?",greater_or_equal:">= ?",between:{op:"BETWEEN ?",list:!0,sep:" AND "},begins_with:{op:"LIKE(?)",fn:function(a){return a+"%"}},not_begins_with:{op:"NOT LIKE(?)",fn:function(a){return a+"%"}},contains:{op:"LIKE(?)",fn:function(a){return"%"+a+"%"}},not_contains:{op:"NOT LIKE(?)",fn:function(a){return"%"+a+"%"}},ends_with:{op:"LIKE(?)",fn:function(a){return"%"+a}},not_ends_with:{op:"NOT LIKE(?)",fn:function(a){return"%"+a}},is_empty:'== ""',is_not_empty:'!= ""',is_null:"IS NULL",is_not_null:"IS NOT NULL"}}),$.fn.queryBuilder.extend({getSQL:function(a,b,c){c=void 0===c?this.getRules():c,a=a===!0||void 0===a?"question_mark":a,b=b||void 0===b?"\n":" ";var e=this,f=1,h=[],i=function j(c){if(c.condition||(c.condition=e.settings.default_condition),-1===["AND","OR"].indexOf(c.condition.toUpperCase())&&$.error("Unable to build SQL query with "+c.condition+" condition"),!c.rules)return"";var i=[];return $.each(c.rules,function(c,k){if(k.rules&&k.rules.length>0)i.push("("+b+j(k)+b+")"+b);else{var l=e.getSqlOperator(k.operator),m=e.getOperatorByType(k.operator),n="";l===!1&&$.error("SQL operation unknown for operator "+k.operator),m.accept_values&&(k.value instanceof Array?!l.list&&k.value.length>1&&$.error("Operator "+k.operator+" cannot accept multiple values"):k.value=[k.value],k.value.forEach(function(b,c){c>0&&(n+=l.sep),"integer"==k.type||"double"==k.type?b=d(b,k.type):a||(b=g(b)),l.fn&&(b=l.fn(b)),a?(n+="question_mark"==a?"?":"$"+f,h.push(b),f++):("string"==typeof b&&(b="'"+b+"'"),n+=b)})),i.push(k.field+" "+l.op.replace(/\?/,n))}}),i.join(" "+c.condition+b)}(c);return a?{sql:i,params:h}:{sql:i}},getSqlOperator:function(a){var b=this.settings.sqlOperators[a];return void 0===b?!1:("string"==typeof b&&(b={op:b}),b.list||(b.list=!1),b.list&&!b.sep&&(b.sep=", "),b)}})}); \ No newline at end of file diff --git a/app/assets/stylesheets/admin.css.scss b/app/assets/stylesheets/admin.css.scss index 77e98a47..0557bb55 100644 --- a/app/assets/stylesheets/admin.css.scss +++ b/app/assets/stylesheets/admin.css.scss @@ -5,6 +5,7 @@ @import "bootstrap-responsive"; @import "fontawesome"; +@import "vendor/jQuery-QueryBuilder/query-builder.css"; @import "qtip2/jquery.qtip"; @import "bootstrap-daterangepicker/daterangepicker-bs2"; @import "selectize/selectize.default"; diff --git a/app/assets/stylesheets/admin/stats.css.scss b/app/assets/stylesheets/admin/stats.css.scss index 28347043..cb8aab38 100644 --- a/app/assets/stylesheets/admin/stats.css.scss +++ b/app/assets/stylesheets/admin/stats.css.scss @@ -78,4 +78,28 @@ } } +.query-builder { + .btn-xs { + font-size: 12px; + padding: 1px 5px; + } + + .rule-container input[type=number], + .rule-container input[type=text], + .rule-container select { + font-size: 11px; + height: 24px; + margin: 0px; + box-sizing: border-box; + } + .rule-container input[type=number] { + width: 102px; + } + + .rules-group-container { + background: #edf5fc; + background: rgba(237, 245, 252, 0.5); + border-color: #96bedc; + } +} diff --git a/app/controllers/admin/stats_controller.rb b/app/controllers/admin/stats_controller.rb index ae36387c..a7a32609 100644 --- a/app/controllers/admin/stats_controller.rb +++ b/app/controllers/admin/stats_controller.rb @@ -18,6 +18,7 @@ def search policy_scope(@search) @search.search!(params[:search]) + @search.query!(params[:query]) @search.filter_by_date_range! @search.aggregate_by_interval! @search.aggregate_by_users!(10) @@ -43,6 +44,7 @@ def logs end @search.search!(params[:search]) + @search.query!(params[:query]) @search.filter_by_date_range! @search.offset!(offset) @search.limit!(limit) @@ -127,6 +129,7 @@ def users end @search.search!(params[:search]) + @search.query!(params[:query]) @search.filter_by_date_range! @search.aggregate_by_user_stats!(aggregation_options) @@ -193,6 +196,7 @@ def map policy_scope(@search) @search.search!(params[:search]) + @search.query!(params[:query]) @search.filter_by_date_range! @search.aggregate_by_region! diff --git a/app/controllers/api/v1/analytics_controller.rb b/app/controllers/api/v1/analytics_controller.rb index 1474efa2..9952ab62 100644 --- a/app/controllers/api/v1/analytics_controller.rb +++ b/app/controllers/api/v1/analytics_controller.rb @@ -12,6 +12,7 @@ def drilldown policy_scope(@search) @search.search!(params[:search]) + @search.query!(params[:query]) @search.filter_by_date_range! drilldown_size = if(request.format == "csv") then 0 else 500 end diff --git a/app/models/log_search.rb b/app/models/log_search.rb index e61ef6a6..a80fe9b1 100644 --- a/app/models/log_search.rb +++ b/app/models/log_search.rb @@ -97,6 +97,104 @@ def search!(query_string) end end + def query!(query) + if(query.kind_of?(String)) + query = MultiJson.load(query) + end + + if(query.present?) + filters = [] + query["rules"].each do |rule| + filter = {} + case(rule["operator"]) + when "equal", "not_equal" + filter = { + :term => { + rule["field"] => rule["value"], + }, + } + when "begins_with", "not_begins_with" + filter = { + :prefix => { + rule["field"] => rule["value"], + }, + } + when "contains", "not_contains" + filter = { + :regexp => { + rule["field"] => ".*#{Regexp.escape(rule["value"])}.*", + }, + } + when "is_null", "is_not_null" + filter = { + :exists => { + "field" => rule["field"], + }, + } + when "less" + filter = { + :range => { + rule["field"] => { + "lt" => rule["value"].to_f, + }, + }, + } + when "less_or_equal" + filter = { + :range => { + rule["field"] => { + "lte" => rule["value"].to_f, + }, + }, + } + when "greater" + filter = { + :range => { + rule["field"] => { + "gt" => rule["value"].to_f, + }, + }, + } + when "greater_or_equal" + filter = { + :range => { + rule["field"] => { + "gte" => rule["value"].to_f, + }, + }, + } + when "between" + values = rule["value"].map { |v| v.to_f }.sort + filter = { + :range => { + rule["field"] => { + "gte" => values[0], + "lte" => values[1], + }, + }, + } + else + raise "unknown filter operator: #{rule["operator"]} (rule: #{rule.inspect})" + end + + if(rule["operator"] =~ /(^not|^is_null)/ && filter.present?) + filter = { :not => filter } + end + + filters << filter + end + + if(filters.present?) + condition = if(query["condition"] == "OR") then :or else :and end + filter = { + condition => filters + } + + @query[:query][:filtered][:filter][:bool][:must] << filter + end + end + end + def offset!(from) @query_options[:from] = from end From 3b21a58464435ec51919fab8b64bb8c342a7638c Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Thu, 19 Feb 2015 13:07:12 -0700 Subject: [PATCH 02/11] Various tweaks for the new query builder UI display. --- .../controllers/stats/base_controller.js | 9 +- .../admin/templates/stats/_query_form.hbs | 285 ++++++++++-------- .../admin/views/stats/query_form_view.js | 58 +++- app/assets/stylesheets/admin/stats.css.scss | 34 +++ app/models/log_search.rb | 2 +- 5 files changed, 249 insertions(+), 139 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/stats/base_controller.js b/app/assets/javascripts/admin/controllers/stats/base_controller.js index c4ae2648..14d2349b 100644 --- a/app/assets/javascripts/admin/controllers/stats/base_controller.js +++ b/app/assets/javascripts/admin/controllers/stats/base_controller.js @@ -5,8 +5,13 @@ Admin.StatsBaseController = Ember.ObjectController.extend({ actions: { submit: function() { - this.set('query.params.query', JSON.stringify($('#query_builder').queryBuilder('getRules'))); - this.set('query.params.search', $('#filter_form input[name=search]').val()); + if($('#filter_type_advanced').css('display') === 'none') { + this.set('query.params.search', null); + this.set('query.params.query', JSON.stringify($('#query_builder').queryBuilder('getRules'))); + } else { + this.set('query.params.query', null); + this.set('query.params.search', $('#filter_form input[name=search]').val()); + } }, }, }); diff --git a/app/assets/javascripts/admin/templates/stats/_query_form.hbs b/app/assets/javascripts/admin/templates/stats/_query_form.hbs index fec9ab07..03b7f3ac 100644 --- a/app/assets/javascripts/admin/templates/stats/_query_form.hbs +++ b/app/assets/javascripts/admin/templates/stats/_query_form.hbs @@ -1,133 +1,9 @@
        -
        -
        -
        -
        - - -
        - Advanced filters use Lucene's Query Syntax. - -
        + - -
        +
        {{#if view.enableInterval}}
        @@ -139,7 +15,7 @@ {{/if}}
        +
        +
        + +
        +
        diff --git a/app/assets/javascripts/admin/views/stats/query_form_view.js b/app/assets/javascripts/admin/views/stats/query_form_view.js index 26578e9a..eda25d6f 100644 --- a/app/assets/javascripts/admin/views/stats/query_form_view.js +++ b/app/assets/javascripts/admin/views/stats/query_form_view.js @@ -70,13 +70,7 @@ Admin.StatsQueryFormView = Ember.View.extend({ 'is_not_null', ]; - var query = this.get('controller.query.params.query'); - var rules; - if(query) { - var rules = JSON.parse(query); - } - - $('#query_builder').queryBuilder({ + var $queryBuilder = $('#query_builder').queryBuilder({ plugins: { 'filter-description': { icon: 'fa fa-info-circle', @@ -215,10 +209,39 @@ Admin.StatsQueryFormView = Ember.View.extend({ operators: stringOperators, }, ], - rules: rules, }); + + var query = this.get('controller.query.params.query'); + var rules; + if(query) { + rules = JSON.parse(query); + } + + if(rules) { + $queryBuilder.queryBuilder('setRules', rules); + this.send('toggleFilters'); + this.send('toggleFilterType', 'builder'); + } else if(this.get('controller.query.params.search')) { + this.send('toggleFilters'); + this.send('toggleFilterType', 'advanced'); + } }, + updateQueryBuilderRules: function() { + console.info('updateQueryBuilderRules'); + var query = this.get('controller.query.params.query'); + var rules; + if(query) { + rules = JSON.parse(query); + } + + if(rules) { + $('#query_builder').queryBuilder('setRules', rules); + } else { + $('#query_builder').queryBuilder('reset'); + } + }.observes('controller.query.params.query'), + updateInterval: function() { var interval = this.get('controller.query.params.interval'); $('#interval_buttons').find('button[value="' + interval + '"]').button('toggle'); @@ -239,6 +262,25 @@ Admin.StatsQueryFormView = Ember.View.extend({ }, actions: { + toggleFilters: function() { + var $container = $('#filters_ui'); + var $icon = $('#filter_toggle .fa'); + if($container.is(':visible')) { + $icon.addClass('fa-caret-right'); + $icon.removeClass('fa-caret-down'); + } else { + $icon.addClass('fa-caret-down'); + $icon.removeClass('fa-caret-right'); + } + + $container.slideToggle(100); + }, + + toggleFilterType: function(type) { + $('.filter-type').hide(); + $('#filter_type_' + type).show(); + }, + clickInterval: function(interval) { this.set('controller.query.params.interval', interval); }, diff --git a/app/assets/stylesheets/admin/stats.css.scss b/app/assets/stylesheets/admin/stats.css.scss index cb8aab38..b9cc2130 100644 --- a/app/assets/stylesheets/admin/stats.css.scss +++ b/app/assets/stylesheets/admin/stats.css.scss @@ -78,6 +78,40 @@ } } +#filter_toggle .fa { + width: 8px; +} + +#filters_ui { + max-width: 960px; + + .filter-type-toggle { + font-size: 12px; + text-align: right; + } + + #search_field { + .input-append { + display: block; + + .full-width-input, + .full-width-submit { + display: table-cell; + } + + .full-width-input { + width: 100%; + } + + .full-width-input input { + width: 100%; + box-sizing: border-box; + height: 26px; + } + } + } +} + .query-builder { .btn-xs { font-size: 12px; diff --git a/app/models/log_search.rb b/app/models/log_search.rb index a80fe9b1..1df41f0d 100644 --- a/app/models/log_search.rb +++ b/app/models/log_search.rb @@ -98,7 +98,7 @@ def search!(query_string) end def query!(query) - if(query.kind_of?(String)) + if(query.kind_of?(String) && query.present?) query = MultiJson.load(query) end From cb81a3f28582f51417dec9362fa07847ae6efa30 Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Thu, 19 Feb 2015 13:12:53 -0700 Subject: [PATCH 03/11] Whoops, remove debug console line. --- app/assets/javascripts/admin/views/stats/query_form_view.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/admin/views/stats/query_form_view.js b/app/assets/javascripts/admin/views/stats/query_form_view.js index eda25d6f..39c8ebbd 100644 --- a/app/assets/javascripts/admin/views/stats/query_form_view.js +++ b/app/assets/javascripts/admin/views/stats/query_form_view.js @@ -228,7 +228,6 @@ Admin.StatsQueryFormView = Ember.View.extend({ }, updateQueryBuilderRules: function() { - console.info('updateQueryBuilderRules'); var query = this.get('controller.query.params.query'); var rules; if(query) { From a4e052c757626f8906635bb1ea60e7a68b9cb580 Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Thu, 19 Feb 2015 13:17:09 -0700 Subject: [PATCH 04/11] Make sure to ignore the /public/test-assets directory from Rubocop. Since we're now precompiling the assets during tests, we need to exclude this directory from Rubocop so it doesn't pick up files in there on subsequent runs. --- .rubocop.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.rubocop.yml b/.rubocop.yml index 3970f8eb..5b2caaf7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,6 +4,7 @@ AllCops: - vendor/**/* - config/compass.rb - public/web-assets/**/* + - public/test-assets/**/* RunRailsCops: true Lint/LiteralInInterpolation: From b0383f8104daa9716b7456f4afd71aa76aff3c5b Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Thu, 19 Feb 2015 13:25:42 -0700 Subject: [PATCH 05/11] Fix tests due to changes in admin filtering interface. --- app/assets/javascripts/admin_test.js | 2 ++ spec/features/admin/stats_logs_spec.rb | 3 +++ 2 files changed, 5 insertions(+) diff --git a/app/assets/javascripts/admin_test.js b/app/assets/javascripts/admin_test.js index 6ce85cb2..ef37f870 100644 --- a/app/assets/javascripts/admin_test.js +++ b/app/assets/javascripts/admin_test.js @@ -8,6 +8,8 @@ // The other part of this is altering the CSS to disable transitions in // admin_test.css. $.support.transition = false; +$.fx.off = true; $(document).ready(function() { $.support.transition = false; + $.fx.off = true; }); diff --git a/spec/features/admin/stats_logs_spec.rb b/spec/features/admin/stats_logs_spec.rb index b2e67130..3c5de717 100644 --- a/spec/features/admin/stats_logs_spec.rb +++ b/spec/features/admin/stats_logs_spec.rb @@ -40,6 +40,8 @@ "interval" => "day", }) + find("a", :text => /Filter Results/).click + find("a", :text => /Switch to advanced filters/).click fill_in "search", :with => "response_status:200" click_button "Filter" link = find_link("Download CSV") @@ -51,6 +53,7 @@ "start_at" => "2015-01-13", "end_at" => "2015-01-18", "interval" => "day", + "query" => "", }) end From d040fc22f5dc2f01da497882f6374d4befc0267b Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Thu, 19 Feb 2015 15:10:24 -0700 Subject: [PATCH 06/11] Fix datatables progress loader specs due to disabled animations in tests Since we disabled jquery animations in b0383f8104daa9716b7456f4afd71aa76aff3c5b this progress loader test began failing. I think this updated approach should be safe. While we're at it, update the jQuery BlockUI library. Also disable animations in non-test environments for BlockUI by default, since it doesn't really seem like we need the delays of fading in and out. --- app/assets/javascripts/admin.js | 3 +++ app/assets/javascripts/vendor/jquery.blockUI.js | 7 ++++--- spec/features/admin/datatables_spec.rb | 12 ++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 16786edc..2b1bf2bd 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -79,6 +79,9 @@ $(document).ready(function() { event.preventDefault(); }); + $.blockUI.defaults.fadeIn = 0; + $.blockUI.defaults.fadeOut = 0; + $(document).on('click', 'a[rel=popover]', function(event) { $(this).qtip({ overwrite: false, diff --git a/app/assets/javascripts/vendor/jquery.blockUI.js b/app/assets/javascripts/vendor/jquery.blockUI.js index 605c37b1..90ce5d64 100644 --- a/app/assets/javascripts/vendor/jquery.blockUI.js +++ b/app/assets/javascripts/vendor/jquery.blockUI.js @@ -1,6 +1,6 @@ /*! * jQuery blockUI plugin - * Version 2.66.0-2013.10.09 + * Version 2.70.0-2014.11.23 * Requires jQuery v1.7 or later * * Examples at: http://malsup.com/jquery/block/ @@ -107,7 +107,7 @@ }); }; - $.blockUI.version = 2.66; // 2nd generation blocking at no extra cost! + $.blockUI.version = 2.70; // 2nd generation blocking at no extra cost! // override these in your code to change the default behavior and style $.blockUI.defaults = { @@ -426,7 +426,7 @@ if (msg) lyr3.show(); if (opts.onBlock) - opts.onBlock(); + opts.onBlock.bind(lyr3)(); } // bind key and mouse events @@ -515,6 +515,7 @@ if (data && data.el) { data.el.style.display = data.display; data.el.style.position = data.position; + data.el.style.cursor = 'default'; // #59 if (data.parent) data.parent.appendChild(data.el); $el.removeData('blockUI.history'); diff --git a/spec/features/admin/datatables_spec.rb b/spec/features/admin/datatables_spec.rb index 00d9ccb6..2260a98d 100644 --- a/spec/features/admin/datatables_spec.rb +++ b/spec/features/admin/datatables_spec.rb @@ -23,22 +23,18 @@ describe "processing" do it "displays a spinner on initial load" do visit "/admin/#/api_users" - page.should have_selector(".dataTables_wrapper .blockOverlay") - page.should have_selector(".dataTables_wrapper .blockMsg .fa-spinner") - # Waiting for ajax + # We can't reliably check for the spinner on page load (it might + # disappear too quickly), so just ensure it eventualy disappears. page.should_not have_selector(".dataTables_wrapper .blockOverlay") page.should_not have_selector(".dataTables_wrapper .blockMsg") end it "displays a spinner when server side processing" do visit "/admin/#/api_users" - # Waiting for ajax - page.should_not have_selector(".dataTables_wrapper .blockOverlay") - page.should_not have_selector(".dataTables_wrapper .blockMsg") + # Ensure that clicking a header triggers a server-side refresh which in + # turn should briefly show the spinner before then disappearing. find("thead tr:first-child").click - page.should have_selector(".dataTables_wrapper .blockOverlay") page.should have_selector(".dataTables_wrapper .blockMsg .fa-spinner") - # Waiting for ajax page.should_not have_selector(".dataTables_wrapper .blockOverlay") page.should_not have_selector(".dataTables_wrapper .blockMsg") end From 2f54850632d55622e4a80f0181bd38dd0f9e380e Mon Sep 17 00:00:00 2001 From: Nick Muerdter Date: Thu, 19 Feb 2015 16:13:46 -0700 Subject: [PATCH 07/11] i18n-ify the text in the analytics filtering UI. --- .../admin/templates/stats/_query_form.hbs | 35 +++++++++---------- config/locales/en.yml | 16 +++++++++ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/admin/templates/stats/_query_form.hbs b/app/assets/javascripts/admin/templates/stats/_query_form.hbs index 03b7f3ac..8177ed39 100644 --- a/app/assets/javascripts/admin/templates/stats/_query_form.hbs +++ b/app/assets/javascripts/admin/templates/stats/_query_form.hbs @@ -1,16 +1,16 @@
        {{#if view.enableInterval}}
        - - - - - + + + + +
        {{/if}} @@ -30,10 +30,10 @@
        @@ -41,30 +41,30 @@
        - +
        - +
        - Advanced filters use Lucene's Query Syntax. + {{{t 'admin.stats.advanced_filters_tip'}}}