turbo_filter 0.0.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: