turbo_filter 0.0.1 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YWVhNGFlM2E1YmIyYjEwN2EwNTZjYzE3NTA2OGU1M2U5Yzk3MDk2Mg==
5
+ data.tar.gz: !binary |-
6
+ ZmQwOTE5ZmY0ODdiY2QyZTYzMGQ4YTk1NmI2NmU3MzlhOTQ2MzQyNQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ MGYyZmUwZjNkZTY5ODMxY2IzNmNiMGJhYjMyMzA4NDI3Nzg3ZDMyMjUzMzEy
10
+ OTdiZDgwYTBjOWExZDk4ZTUzYmVjYTMzNDhiZjAwZDM5OWY1NTA1N2FmOGQx
11
+ ZGJiYTVjNzFjYWUyOTYwZDgyNjBhYTlhYTE2NzhmZTI2YTkyNWQ=
12
+ data.tar.gz: !binary |-
13
+ ZGYzNmY0NTkzNzQ5NTYyMDg4YjJiNjNlZWQxY2Q2MzMyNzRjYjU2MTI3MDc5
14
+ MTE5N2M4YjFlMDA2YzJmYTIwNmUzNTg1ZGY4YTNiZWQ1NmYxOTMxZTFhMzg3
15
+ ZTYyMWQ3OWE4YzlhMWMwYjUzZDZiOWE5YTUxMGE2YmY2ZDBmNjM=
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # TurboFilter
2
2
 
3
- TODO: Write a gem description
3
+ Filters your ActiveRecord table records.
4
4
 
5
5
  ## Installation
6
6
 
@@ -18,7 +18,38 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage
20
20
 
21
- TODO: Write usage instructions here
21
+ Add turbo_filter to application.js
22
+
23
+ //= require turbo_filter
24
+
25
+ In Controller:
26
+
27
+ class ArticlesController < ApplicationController
28
+ ...
29
+ def index
30
+ retrieve_turbo_filter_query(Article)
31
+ @articles = Article.where(@turbo_filter_query.statement).paginate(page: params[:page], per_page: 100)
32
+ end
33
+ ...
34
+ end
35
+
36
+ In Models: Add `to_s` method like below to `Article -> belongs_to` assocation classes.
37
+
38
+ class User < ActiveRecord::Base
39
+ ...
40
+ def to_s
41
+ name
42
+ end
43
+ ...
44
+ end
45
+
46
+ In Views: Add `turbo_filters` helper method to `app/views/articles/index.html.erb` view filters.
47
+
48
+ <%= turbo_filters %>
49
+
50
+ UI Compatibility: boostrap compatible.
51
+ Requires jQueryDatePicker, Turbo links enabled.
52
+
22
53
 
23
54
  ## Contributing
24
55
 
@@ -0,0 +1,362 @@
1
+ function checkAll(id, checked) {
2
+ $('#'+id).find('input[type=checkbox]:enabled').prop('checked', checked);
3
+ }
4
+
5
+ function toggleCheckboxesBySelector(selector) {
6
+ var all_checked = true;
7
+ $(selector).each(function(index) {
8
+ if (!$(this).is(':checked')) { all_checked = false; }
9
+ });
10
+ $(selector).prop('checked', !all_checked);
11
+ }
12
+
13
+ function showAndScrollTo(id, focus) {
14
+ $('#'+id).show();
15
+ if (focus !== null) {
16
+ $('#'+focus).focus();
17
+ }
18
+ $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
19
+ }
20
+
21
+ function toggleRowGroup(el) {
22
+ var tr = $(el).parents('tr').first();
23
+ var n = tr.next();
24
+ tr.toggleClass('open');
25
+ while (n.length && !n.hasClass('group')) {
26
+ n.toggle();
27
+ n = n.next('tr');
28
+ }
29
+ }
30
+
31
+ function collapseAllRowGroups(el) {
32
+ var tbody = $(el).parents('tbody').first();
33
+ tbody.children('tr').each(function(index) {
34
+ if ($(this).hasClass('group')) {
35
+ $(this).removeClass('open');
36
+ } else {
37
+ $(this).hide();
38
+ }
39
+ });
40
+ }
41
+
42
+ function expandAllRowGroups(el) {
43
+ var tbody = $(el).parents('tbody').first();
44
+ tbody.children('tr').each(function(index) {
45
+ if ($(this).hasClass('group')) {
46
+ $(this).addClass('open');
47
+ } else {
48
+ $(this).show();
49
+ }
50
+ });
51
+ }
52
+
53
+ function toggleAllRowGroups(el) {
54
+ var tr = $(el).parents('tr').first();
55
+ if (tr.hasClass('open')) {
56
+ collapseAllRowGroups(el);
57
+ } else {
58
+ expandAllRowGroups(el);
59
+ }
60
+ }
61
+
62
+ function toggleFieldset(el) {
63
+ var fieldset = $(el).parents('fieldset').first();
64
+ fieldset.toggleClass('collapsed');
65
+ fieldset.children('div').toggle();
66
+ }
67
+
68
+ function hideFieldset(el) {
69
+ var fieldset = $(el).parents('fieldset').first();
70
+ fieldset.toggleClass('collapsed');
71
+ fieldset.children('div').hide();
72
+ }
73
+
74
+ function initFilters() {
75
+ $('#add_filter_select').change(function() {
76
+ addFilter($(this).val(), '', []);
77
+ });
78
+ $('#filters-table td.field input[type=checkbox]').each(function() {
79
+ toggleFilter($(this).val());
80
+ });
81
+ $('#filters-table').on('click', 'td.field input[type=checkbox]', function() {
82
+ toggleFilter($(this).val());
83
+ });
84
+ $('#filters-table').on('click', '.toggle-multiselect', function() {
85
+ toggleMultiSelect($(this).siblings('select'));
86
+ });
87
+ $('#filters-table').on('keypress', 'input[type=text]', function(e) {
88
+ if (e.keyCode == 13) submit_turbo_filter_query_form("turbo_filter_query_form");
89
+ });
90
+ }
91
+
92
+ function addFilter(field, operator, values) {
93
+ var fieldId = field.replace('.', '_');
94
+ var tr = $('#tr_'+fieldId);
95
+ if (tr.length > 0) {
96
+ tr.show();
97
+ } else {
98
+ buildFilterRow(field, operator, values);
99
+ }
100
+ $('#cb_'+fieldId).prop('checked', true);
101
+ toggleFilter(field);
102
+ $('#add_filter_select').val('').children('option').each(function() {
103
+ if ($(this).attr('value') == field) {
104
+ $(this).attr('disabled', true);
105
+ }
106
+ });
107
+ }
108
+
109
+ function buildFilterRow(field, operator, values) {
110
+ var fieldId = field.replace('.', '_');
111
+ var filterTable = $("#filters-table");
112
+ var filterOptions = availableFilters[field];
113
+ if (!filterOptions) return;
114
+ var operators = operatorByType[filterOptions['type']];
115
+ var filterValues = filterOptions['values'];
116
+ var datepickerOptions = {dateFormat: 'yy-mm-dd', showButtonPanel: true, changeMonth: true, changeYear: true};
117
+ var i, select;
118
+
119
+ var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
120
+ '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
121
+ '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
122
+ '<td class="values"></td>'
123
+ );
124
+ filterTable.append(tr);
125
+
126
+ select = tr.find('td.operator select');
127
+ for (i = 0; i < operators.length; i++) {
128
+ var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
129
+ if (operators[i] == operator) { option.attr('selected', true); }
130
+ select.append(option);
131
+ }
132
+ select.change(function(){ toggleOperator(field); });
133
+
134
+ switch (filterOptions['type']) {
135
+ case "list":
136
+ case "list_optional":
137
+ case "list_status":
138
+ case "list_subprojects":
139
+ tr.find('td.values').append(
140
+ '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
141
+ ' <span class="toggle-multiselect">&nbsp;</span></span>'
142
+ );
143
+ select = tr.find('td.values select');
144
+ if (values.length > 1) { select.attr('multiple', true); }
145
+ for (i = 0; i < filterValues.length; i++) {
146
+ var filterValue = filterValues[i];
147
+ var option = $('<option>');
148
+ if ($.isArray(filterValue)) {
149
+ option.val(filterValue[1]).text(filterValue[0]);
150
+ if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
151
+ } else {
152
+ option.val(filterValue).text(filterValue);
153
+ if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
154
+ }
155
+ select.append(option);
156
+ }
157
+ break;
158
+ case "date":
159
+ case "date_past":
160
+ tr.find('td.values').append(
161
+ '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
162
+ ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
163
+ ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
164
+ );
165
+ $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
166
+ $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
167
+ $('#values_'+fieldId).val(values[0]);
168
+ break;
169
+ case "string":
170
+ case "text":
171
+ tr.find('td.values').append(
172
+ '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
173
+ );
174
+ $('#values_'+fieldId).val(values[0]);
175
+ break;
176
+ case "relation":
177
+ tr.find('td.values').append(
178
+ '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
179
+ '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
180
+ );
181
+ $('#values_'+fieldId).val(values[0]);
182
+ select = tr.find('td.values select');
183
+ for (i = 0; i < allProjects.length; i++) {
184
+ var filterValue = allProjects[i];
185
+ var option = $('<option>');
186
+ option.val(filterValue[1]).text(filterValue[0]);
187
+ if (values[0] == filterValue[1]) { option.attr('selected', true); }
188
+ select.append(option);
189
+ }
190
+ break;
191
+ case "integer":
192
+ case "float":
193
+ tr.find('td.values').append(
194
+ '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' +
195
+ ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>'
196
+ );
197
+ $('#values_'+fieldId+'_1').val(values[0]);
198
+ $('#values_'+fieldId+'_2').val(values[1]);
199
+ break;
200
+ }
201
+ }
202
+
203
+ function toggleFilter(field) {
204
+ var fieldId = field.replace('.', '_');
205
+ if ($('#cb_' + fieldId).is(':checked')) {
206
+ $("#operators_" + fieldId).show().removeAttr('disabled');
207
+ toggleOperator(field);
208
+ } else {
209
+ $("#operators_" + fieldId).hide().attr('disabled', true);
210
+ enableValues(field, []);
211
+ }
212
+ }
213
+
214
+ function enableValues(field, indexes) {
215
+ var fieldId = field.replace('.', '_');
216
+ $('#tr_'+fieldId+' td.values .value').each(function(index) {
217
+ if ($.inArray(index, indexes) >= 0) {
218
+ $(this).removeAttr('disabled');
219
+ $(this).parents('span').first().show();
220
+ } else {
221
+ $(this).val('');
222
+ $(this).attr('disabled', true);
223
+ $(this).parents('span').first().hide();
224
+ }
225
+
226
+ if ($(this).hasClass('group')) {
227
+ $(this).addClass('open');
228
+ } else {
229
+ $(this).show();
230
+ }
231
+ });
232
+ }
233
+
234
+ function toggleOperator(field) {
235
+ var fieldId = field.replace('.', '_');
236
+ var operator = $("#operators_" + fieldId);
237
+ switch (operator.val()) {
238
+ case "!*":
239
+ case "*":
240
+ case "t":
241
+ case "ld":
242
+ case "w":
243
+ case "lw":
244
+ case "l2w":
245
+ case "m":
246
+ case "lm":
247
+ case "y":
248
+ case "o":
249
+ case "c":
250
+ enableValues(field, []);
251
+ break;
252
+ case "><":
253
+ enableValues(field, [0,1]);
254
+ break;
255
+ case "<t+":
256
+ case ">t+":
257
+ case "><t+":
258
+ case "t+":
259
+ case ">t-":
260
+ case "<t-":
261
+ case "><t-":
262
+ case "t-":
263
+ enableValues(field, [2]);
264
+ break;
265
+ case "=p":
266
+ case "=!p":
267
+ case "!p":
268
+ enableValues(field, [1]);
269
+ break;
270
+ default:
271
+ enableValues(field, [0]);
272
+ break;
273
+ }
274
+ }
275
+
276
+ function toggleMultiSelect(el) {
277
+ if (el.attr('multiple')) {
278
+ el.removeAttr('multiple');
279
+ el.attr('size', 1);
280
+ } else {
281
+ el.attr('multiple', true);
282
+ if (el.children().length > 10)
283
+ el.attr('size', 10);
284
+ else
285
+ el.attr('size', 4);
286
+ }
287
+ }
288
+
289
+ function validate_turbo_filters(id) {
290
+ var arr = 0;
291
+ $(".value:visible").each(function(){
292
+ if(!$(this).val().trim()){
293
+ $(this).css({"border-color": "RED"});
294
+ $(this).attr('placeholder', "can't be empty.");
295
+ arr += 1;
296
+ }
297
+ });
298
+ return arr;
299
+ }
300
+
301
+ function submit_turbo_filter_query_form(id) {
302
+ if(validate_turbo_filters(id) === 0)
303
+ $('#'+id).submit();
304
+ }
305
+
306
+ function showModal(id, width) {
307
+ var el = $('#'+id).first();
308
+ if (el.length === 0 || el.is(':visible')) {return;}
309
+ var title = el.find('h3.title').text();
310
+ el.dialog({
311
+ width: width,
312
+ modal: true,
313
+ resizable: false,
314
+ dialogClass: 'modal',
315
+ title: title
316
+ });
317
+ el.find("input[type=text], input[type=submit]").first().focus();
318
+ }
319
+
320
+ function hideModal(el) {
321
+ var modal;
322
+ if (el) {
323
+ modal = $(el).parents('.ui-dialog-content');
324
+ } else {
325
+ modal = $('#ajax-modal');
326
+ }
327
+ modal.dialog("close");
328
+ }
329
+
330
+ function setupAjaxIndicator() {
331
+ $(document).bind('ajaxSend', function(event, xhr, settings) {
332
+ if ($('.ajax-loading').length === 0 && settings.contentType != 'application/octet-stream') {
333
+ $('#ajax-indicator').show();
334
+ }
335
+ });
336
+ $(document).bind('ajaxStop', function() {
337
+ $('#ajax-indicator').hide();
338
+ });
339
+ }
340
+
341
+ function hideOnLoad() {
342
+ $('.hol').hide();
343
+ }
344
+
345
+ function addFormObserversForDoubleSubmit() {
346
+ $('form[method=post]').each(function() {
347
+ if (!$(this).hasClass('multiple-submit')) {
348
+ $(this).submit(function(form_submission) {
349
+ if ($(form_submission.target).attr('data-submitted')) {
350
+ form_submission.preventDefault();
351
+ } else {
352
+ $(form_submission.target).attr('data-submitted', true);
353
+ }
354
+ });
355
+ }
356
+ });
357
+ }
358
+
359
+ // $(document).ready(setupAjaxIndicator);
360
+ // $(document).ready(hideOnLoad);
361
+ // $(document).ready(addFormObserversForDoubleSubmit);
362
+
@@ -0,0 +1,31 @@
1
+ <script type="text/javascript">
2
+ var operatorLabels = <%= raw_json TurboFilter::TurboFilterQuery.operators_labels %>;
3
+ var operatorByType = <%= raw_json TurboFilter::TurboFilterQuery.operators_by_filter_type %>;
4
+ var availableFilters = <%= raw_json turbo_filter_query.available_filters_as_json %>;
5
+ var labelDayPlural = <%= raw_json t(:label_day_plural) %>;
6
+ $(document).on('ready page:load', function () {
7
+ initFilters();
8
+ <% turbo_filter_query.filters.each do |field, options| %>
9
+ addFilter("<%= field %>", <%= raw_json turbo_filter_query.operator_for(field) %>, <%= raw_json turbo_filter_query.values_for(field) %>);
10
+ <% end %>
11
+ });
12
+ </script>
13
+ <h2>Turbo Filters</h2>
14
+ <%= form_tag({ :controller => turbo_filter_query.instance_values["filters_for_class"].to_s.pluralize.downcase, :action => 'index' },
15
+ :method => :get, :id => 'turbo_filter_query_form') do %>
16
+ <table style="width:100%">
17
+ <tr>
18
+ <td>
19
+ <table id="filters-table"></table>
20
+ </td>
21
+ <td class="add-filter">
22
+ <%= label_tag('add_filter_select', t(:label_filter_add)) %>
23
+ <%= select_tag 'add_filter_select', filters_options_for_select(turbo_filter_query), :name => nil %>
24
+ </td>
25
+ </tr>
26
+ </table>
27
+ <%= hidden_field_tag 'f[]', '' %>
28
+ <%= link_to_function t(:apply), 'submit_turbo_filter_query_form("turbo_filter_query_form")', :class => 'btn btn-primary' %>
29
+ <%= link_to t(:clear), { :set_filter => 1 }, "data-no-turbolink" => true, :class => 'btn' %>
30
+ <% end -%>
31
+ <hr>
@@ -0,0 +1,32 @@
1
+ en:
2
+ label_day_plural: Days
3
+ label_filter_add: Add Filter
4
+ label_filter_plural: Filters
5
+ label_equals: is
6
+ label_not_equals: is not
7
+ label_in_less_than: in less than
8
+ label_in_more_than: in more than
9
+ label_in_the_next_days: in the next
10
+ label_in_the_past_days: in the past
11
+ label_greater_or_equal: '>='
12
+ label_less_or_equal: '<='
13
+ label_between: between
14
+ label_in: in
15
+ label_today: today
16
+ label_all_time: all time
17
+ label_yesterday: yesterday
18
+ label_this_week: this week
19
+ label_last_week: last week
20
+ label_last_n_weeks: "last %{count} weeks"
21
+ label_last_n_days: "last %{count} days"
22
+ label_this_month: this month
23
+ label_last_month: last month
24
+ label_this_year: this year
25
+ label_date_range: Date range
26
+ label_less_than_ago: less than days ago
27
+ label_more_than_ago: more than days ago
28
+ label_ago: days ago
29
+ label_contains: contains
30
+ label_not_contains: doesn't contain
31
+ label_any: any
32
+ label_none: none
@@ -0,0 +1,4 @@
1
+ module TurboFilter
2
+ class Engine < ::Rails::Engine
3
+ end
4
+ end
@@ -0,0 +1,36 @@
1
+ module TurboFilter
2
+ module TurboFilterController
3
+
4
+ # Retrieve query from session or build a new query
5
+ def retrieve_turbo_filter_query(filters_for_class)
6
+ if params[:set_filter] || session[:query].nil?
7
+ # Give it a name, required to be valid
8
+ @turbo_filter_query = TurboFilter::TurboFilterQuery.new(filters_for_class)
9
+ build_turbo_filter_query_from_params
10
+ session[:query] = {:filters => @turbo_filter_query.filters}
11
+ else
12
+ # retrieve from session
13
+ @turbo_filter_query ||= TurboFilter::TurboFilterQuery.new(filters_for_class, session[:query][:filters])
14
+ build_turbo_filter_query_from_params
15
+ end
16
+ end
17
+
18
+ def retrieve_turbo_filter_query_from_session
19
+ if session[:query]
20
+ @turbo_filter_query = TurboFilter::TurboFilterQuery.new(filters_for_class, session[:query][:filters])
21
+ end
22
+ end
23
+
24
+ def build_turbo_filter_query_from_params
25
+ if params[:f]
26
+ filters = if session[:query]
27
+ session[:query][:filters] = session[:query][:filters].select { |k,v| params[:f].reject(&:blank?).include?(k) }
28
+ else
29
+ {}
30
+ end
31
+ @turbo_filter_query.filters = filters
32
+ @turbo_filter_query.add_filters(params[:f], params[:operators] || params[:op], params[:values] || params[:v])
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ module TurboFilter
2
+ module TurboFilterHelper
3
+
4
+ def turbo_filters
5
+ render :partial => 'turbo_filters/filters', :layout => false, :locals => {:turbo_filter_query => @turbo_filter_query}
6
+ end
7
+
8
+ def filters_options_for_select(query)
9
+ options_for_select(filters_options(query))
10
+ end
11
+
12
+ def filters_options(query)
13
+ options = [[]]
14
+ options += query.available_filters.map do |field, field_options|
15
+ [field_options[:name], field]
16
+ end
17
+ end
18
+
19
+ # Helper to render JSON in views
20
+ def raw_json(arg)
21
+ arg.to_json.to_s.gsub('/', '\/').html_safe
22
+ end
23
+
24
+ def link_to_function(name, function, html_options={})
25
+ content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,353 @@
1
+ module TurboFilter
2
+ class TurboFilterQuery
3
+
4
+ class_attribute :filters
5
+ class_attribute :operators
6
+ self.operators = {
7
+ "=" => :label_equals,
8
+ "!" => :label_not_equals,
9
+ "!*" => :label_none,
10
+ "*" => :label_any,
11
+ ">=" => :label_greater_or_equal,
12
+ "<=" => :label_less_or_equal,
13
+ "><" => :label_between,
14
+ "<t+" => :label_in_less_than,
15
+ ">t+" => :label_in_more_than,
16
+ "><t+"=> :label_in_the_next_days,
17
+ "t+" => :label_in,
18
+ "t" => :label_today,
19
+ "ld" => :label_yesterday,
20
+ "w" => :label_this_week,
21
+ "lw" => :label_last_week,
22
+ "l2w" => [:label_last_n_weeks, {:count => 2}],
23
+ "m" => :label_this_month,
24
+ "lm" => :label_last_month,
25
+ "y" => :label_this_year,
26
+ ">t-" => :label_less_than_ago,
27
+ "<t-" => :label_more_than_ago,
28
+ "><t-"=> :label_in_the_past_days,
29
+ "t-" => :label_ago,
30
+ "~" => :label_contains,
31
+ "!~" => :label_not_contains
32
+ }
33
+
34
+ class_attribute :operators_by_filter_type
35
+ self.operators_by_filter_type = {
36
+ :list => [ "=", "!" ],
37
+ :list_optional => [ "=", "!", "!*", "*" ],
38
+ :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "><t+", "t+", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", ">t-", "<t-", "><t-", "t-", "!*", "*" ],
39
+ :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "><t-", "t-", "t", "ld", "w", "lw", "l2w", "m", "lm", "y", "!*", "*" ],
40
+ :string => [ "=", "~", "!", "!~", "!*", "*" ],
41
+ :text => [ "~", "!~", "!*", "*" ],
42
+ :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
43
+ :float => [ "=", ">=", "<=", "><", "!*", "*" ]
44
+ }
45
+
46
+ # Returns a hash of localized labels for all filter operators
47
+ def self.operators_labels
48
+ operators.inject({}) {|h, operator| h[operator.first] = I18n.t(*operator.last); h}
49
+ end
50
+
51
+ def initialize(filters_for_class=nil,filters={})
52
+ @filters_for_class = filters_for_class
53
+ self.filters = filters
54
+ end
55
+
56
+ # Adds available filters
57
+ def initialize_available_filters
58
+ if @filters_for_class.is_a?(Class) && (@filters_for_class.superclass == ActiveRecord::Base)
59
+ associations = @filters_for_class.reflect_on_all_associations(:belongs_to)
60
+ association_foreign_keys = associations.map(&:foreign_key)
61
+ @filters_for_class.columns.each do |col|
62
+ case col.type
63
+ when :string
64
+ add_available_filter col.name, :type => :text
65
+ when :date
66
+ add_available_filter col.name, :type => :date
67
+ when :datetime
68
+ add_available_filter col.name, :type => :date_past
69
+ when :float
70
+ add_available_filter col.name, :type => :float
71
+ when :integer
72
+ if association_foreign_keys.include?(col.name)
73
+ association_class = associations.select { |a| a.foreign_key == col.name }.first.class_name.constantize
74
+ association_values = association_class.all.collect{|s| [s.to_s, s.id.to_s] }
75
+ add_available_filter col.name, :type => :list, :values => association_values
76
+ else
77
+ add_available_filter col.name, :type => :integer
78
+ end
79
+ when :boolean
80
+ add_available_filter col.name, :type => :list, :values => [["Yes","1"],["No", "0"]]
81
+ end # case
82
+ end # do
83
+ end
84
+ end
85
+ protected :initialize_available_filters
86
+
87
+ # Adds an available filter
88
+ def add_available_filter(field, options)
89
+ @available_filters ||= ActiveSupport::OrderedHash.new
90
+ @available_filters[field] = options
91
+ @available_filters
92
+ end
93
+
94
+ # Removes an available filter
95
+ def delete_available_filter(field)
96
+ if @available_filters
97
+ @available_filters.delete(field)
98
+ end
99
+ end
100
+
101
+ # Return a hash of available filters
102
+ def available_filters
103
+ unless @available_filters
104
+ initialize_available_filters
105
+ @available_filters.to_a.each do |field, options|
106
+ options[:name] ||= I18n.t("field_#{field}".gsub(/_id$/, ''))
107
+ end
108
+ end
109
+ @available_filters
110
+ end
111
+
112
+ def add_filter(field, operator, values=nil)
113
+ # values must be an array
114
+ return unless values.nil? || values.is_a?(Array)
115
+ # check if field is defined as an available filter
116
+ if available_filters.has_key? field
117
+ filter_options = available_filters[field]
118
+ filters[field] = {:operator => operator, :values => (values || [''])}
119
+ end
120
+ end
121
+
122
+ def add_short_filter(field, expression)
123
+ return unless expression && available_filters.has_key?(field)
124
+ field_type = available_filters[field][:type]
125
+ operators_by_filter_type[field_type].sort.reverse.detect do |operator|
126
+ next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
127
+ values = $1
128
+ add_filter field, operator, values.present? ? values.split('|') : ['']
129
+ end || add_filter(field, '=', expression.split('|'))
130
+ end
131
+
132
+ # Add multiple filters using +add_filter+
133
+ def add_filters(fields, operators, values)
134
+ if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
135
+ fields.each do |field|
136
+ add_filter(field, operators[field], values && values[field])
137
+ end
138
+ end
139
+ end
140
+
141
+ def has_filter?(field)
142
+ filters and filters[field]
143
+ end
144
+
145
+ def type_for(field)
146
+ available_filters[field][:type] if available_filters.has_key?(field)
147
+ end
148
+
149
+ def operator_for(field)
150
+ has_filter?(field) ? filters[field][:operator] : nil
151
+ end
152
+
153
+ def values_for(field)
154
+ has_filter?(field) ? filters[field][:values] : nil
155
+ end
156
+
157
+ def value_for(field, index=0)
158
+ (values_for(field) || [])[index]
159
+ end
160
+
161
+ def label_for(field)
162
+ label = available_filters[field][:name] if available_filters.has_key?(field)
163
+ label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
164
+ end
165
+
166
+ # Returns a representation of the available filters for JSON serialization
167
+ def available_filters_as_json
168
+ json = {}
169
+ available_filters.to_a.each do |field, options|
170
+ json[field] = options.slice(:type, :name, :values).stringify_keys
171
+ end
172
+ json
173
+ end
174
+
175
+ def statement
176
+ # filters clauses
177
+ filters_clauses = []
178
+ filters.each_key do |field|
179
+ v = values_for(field).clone
180
+ next unless v and !v.empty?
181
+ operator = operator_for(field)
182
+
183
+ filters_clauses << '(' + sql_for_field(field, operator, v, @filters_for_class.table_name, field) + ')'
184
+ end if filters
185
+
186
+ filters_clauses.reject!(&:blank?)
187
+
188
+ filters_clauses.any? ? filters_clauses.join(' AND ') : nil
189
+ end
190
+
191
+
192
+ # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
193
+ def sql_for_field(field, operator, value, db_table, db_field)
194
+ sql = ''
195
+ case operator
196
+ when "="
197
+ if value.any?
198
+ case type_for(field)
199
+ when :date, :date_past
200
+ sql = date_clause(db_table, db_field, parse_date(value.first), parse_date(value.first))
201
+ when :integer
202
+ sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
203
+ when :float
204
+ sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
205
+ else
206
+ sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{@filters_for_class.connection.quote_string(val)}'"}.join(",") + ")"
207
+ end
208
+ else
209
+ # IN an empty set
210
+ sql = "1=0"
211
+ end
212
+ when "!"
213
+ if value.any?
214
+ sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{@filters_for_class.connection.quote_string(val)}'"}.join(",") + "))"
215
+ else
216
+ # NOT IN an empty set
217
+ sql = "1=1"
218
+ end
219
+ when "!*"
220
+ sql = "#{db_table}.#{db_field} IS NULL"
221
+ when "*"
222
+ sql = "#{db_table}.#{db_field} IS NOT NULL"
223
+ when ">="
224
+ if [:date, :date_past].include?(type_for(field))
225
+ sql = date_clause(db_table, db_field, parse_date(value.first), nil)
226
+ else
227
+ sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
228
+ end
229
+ when "<="
230
+ if [:date, :date_past].include?(type_for(field))
231
+ sql = date_clause(db_table, db_field, nil, parse_date(value.first))
232
+ else
233
+ sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
234
+ end
235
+ when "><"
236
+ if [:date, :date_past].include?(type_for(field))
237
+ sql = date_clause(db_table, db_field, parse_date(value[0]), parse_date(value[1]))
238
+ else
239
+ sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
240
+ end
241
+ when "><t-"
242
+ # between today - n days and today
243
+ sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
244
+ when ">t-"
245
+ # >= today - n days
246
+ sql = relative_date_clause(db_table, db_field, - value.first.to_i, nil)
247
+ when "<t-"
248
+ # <= today - n days
249
+ sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
250
+ when "t-"
251
+ # = n days in past
252
+ sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
253
+ when "><t+"
254
+ # between today and today + n days
255
+ sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
256
+ when ">t+"
257
+ # >= today + n days
258
+ sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
259
+ when "<t+"
260
+ # <= today + n days
261
+ sql = relative_date_clause(db_table, db_field, nil, value.first.to_i)
262
+ when "t+"
263
+ # = today + n days
264
+ sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
265
+ when "t"
266
+ # = today
267
+ sql = relative_date_clause(db_table, db_field, 0, 0)
268
+ when "ld"
269
+ # = yesterday
270
+ sql = relative_date_clause(db_table, db_field, -1, -1)
271
+ when "w"
272
+ # = this week
273
+ first_day_of_week = l(:general_first_day_of_week).to_i
274
+ day_of_week = Date.today.cwday
275
+ days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
276
+ sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
277
+ when "lw"
278
+ # = last week
279
+ first_day_of_week = l(:general_first_day_of_week).to_i
280
+ day_of_week = Date.today.cwday
281
+ days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
282
+ sql = relative_date_clause(db_table, db_field, - days_ago - 7, - days_ago - 1)
283
+ when "l2w"
284
+ # = last 2 weeks
285
+ first_day_of_week = l(:general_first_day_of_week).to_i
286
+ day_of_week = Date.today.cwday
287
+ days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
288
+ sql = relative_date_clause(db_table, db_field, - days_ago - 14, - days_ago - 1)
289
+ when "m"
290
+ # = this month
291
+ date = Date.today
292
+ sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
293
+ when "lm"
294
+ # = last month
295
+ date = Date.today.prev_month
296
+ sql = date_clause(db_table, db_field, date.beginning_of_month, date.end_of_month)
297
+ when "y"
298
+ # = this year
299
+ date = Date.today
300
+ sql = date_clause(db_table, db_field, date.beginning_of_year, date.end_of_year)
301
+ when "~"
302
+ sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{@filters_for_class.connection.quote_string(value.first.to_s.downcase)}%'"
303
+ when "!~"
304
+ sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{@filters_for_class.connection.quote_string(value.first.to_s.downcase)}%'"
305
+ else
306
+ raise "Unknown query operator #{operator}"
307
+ end
308
+
309
+ return sql
310
+ end
311
+
312
+ # Returns a SQL clause for a date or datetime field.
313
+ def date_clause(table, field, from, to)
314
+ s = []
315
+ if from
316
+ if from.is_a?(Date)
317
+ from = Time.local(from.year, from.month, from.day).yesterday.end_of_day
318
+ else
319
+ from = from - 1 # second
320
+ end
321
+ if @filters_for_class.default_timezone == :utc
322
+ from = from.utc
323
+ end
324
+ s << ("#{table}.#{field} > '%s'" % [@filters_for_class.connection.quoted_date(from)])
325
+ end
326
+ if to
327
+ if to.is_a?(Date)
328
+ to = Time.local(to.year, to.month, to.day).end_of_day
329
+ end
330
+ if @filters_for_class.default_timezone == :utc
331
+ to = to.utc
332
+ end
333
+ s << ("#{table}.#{field} <= '%s'" % [@filters_for_class.connection.quoted_date(to)])
334
+ end
335
+ s.join(' AND ')
336
+ end
337
+
338
+ # Returns a SQL clause for a date or datetime field using relative dates.
339
+ def relative_date_clause(table, field, days_from, days_to)
340
+ date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
341
+ end
342
+
343
+ # Returns a Date or Time from the given filter value
344
+ def parse_date(arg)
345
+ if arg.to_s =~ /\A\d{4}-\d{2}-\d{2}T/
346
+ Time.parse(arg) rescue nil
347
+ else
348
+ Date.parse(arg) rescue nil
349
+ end
350
+ end
351
+
352
+ end
353
+ end
@@ -1,3 +1,3 @@
1
1
  module TurboFilter
2
- VERSION = "0.0.1"
2
+ VERSION = "1.0.1"
3
3
  end
data/lib/turbo_filter.rb CHANGED
@@ -1,5 +1,13 @@
1
- require "turbo_filter/version"
1
+ require 'turbo_filter/engine'
2
+ require 'turbo_filter/version'
3
+ require 'turbo_filter/turbo_filter_controller'
4
+ require 'turbo_filter/turbo_filter_helper'
5
+ require 'turbo_filter/turbo_filter_query'
6
+
7
+ ActiveSupport.on_load(:action_view) do
8
+ ::ActionView::Base.send :include, TurboFilter::TurboFilterHelper
9
+ end
10
+ ::ActionController::Base.send :include, TurboFilter::TurboFilterController
2
11
 
3
12
  module TurboFilter
4
- # Your code goes here...
5
13
  end
metadata CHANGED
@@ -1,20 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo_filter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
5
- prerelease:
4
+ version: 1.0.1
6
5
  platform: ruby
7
6
  authors:
8
7
  - Sandeep Kumar
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2015-01-19 00:00:00.000000000 Z
11
+ date: 2016-01-18 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: bundler
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
17
  - - ~>
20
18
  - !ruby/object:Gem::Version
@@ -22,7 +20,6 @@ dependencies:
22
20
  type: :development
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
24
  - - ~>
28
25
  - !ruby/object:Gem::Version
@@ -30,7 +27,6 @@ dependencies:
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: rake
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
31
  - - ! '>='
36
32
  - !ruby/object:Gem::Version
@@ -38,7 +34,6 @@ dependencies:
38
34
  type: :development
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
38
  - - ! '>='
44
39
  - !ruby/object:Gem::Version
@@ -55,33 +50,39 @@ files:
55
50
  - LICENSE.txt
56
51
  - README.md
57
52
  - Rakefile
53
+ - app/assets/javascripts/turbo_filter.js
54
+ - app/views/turbo_filters/_filters.html.erb
55
+ - config/locales/en.yml
58
56
  - lib/turbo_filter.rb
57
+ - lib/turbo_filter/engine.rb
58
+ - lib/turbo_filter/turbo_filter_controller.rb
59
+ - lib/turbo_filter/turbo_filter_helper.rb
60
+ - lib/turbo_filter/turbo_filter_query.rb
59
61
  - lib/turbo_filter/version.rb
60
62
  - turbo_filter.gemspec
61
63
  homepage: ''
62
64
  licenses:
63
65
  - MIT
66
+ metadata: {}
64
67
  post_install_message:
65
68
  rdoc_options: []
66
69
  require_paths:
67
70
  - lib
68
71
  required_ruby_version: !ruby/object:Gem::Requirement
69
- none: false
70
72
  requirements:
71
73
  - - ! '>='
72
74
  - !ruby/object:Gem::Version
73
75
  version: '0'
74
76
  required_rubygems_version: !ruby/object:Gem::Requirement
75
- none: false
76
77
  requirements:
77
78
  - - ! '>='
78
79
  - !ruby/object:Gem::Version
79
80
  version: '0'
80
81
  requirements: []
81
82
  rubyforge_project:
82
- rubygems_version: 1.8.23.2
83
+ rubygems_version: 2.4.8
83
84
  signing_key:
84
- specification_version: 3
85
+ specification_version: 4
85
86
  summary: Filter records
86
87
  test_files: []
87
88
  has_rdoc: