with_filters 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (109) hide show
  1. data/.gitignore +8 -0
  2. data/.yardopts +1 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +22 -0
  5. data/README.md +63 -0
  6. data/Rakefile +1 -0
  7. data/app/views/with_filters/_filter_form.html.erb +14 -0
  8. data/app/views/with_filters/filter/_check_box.html.erb +2 -0
  9. data/app/views/with_filters/filter/_check_boxes.html.erb +5 -0
  10. data/app/views/with_filters/filter/_radio.html.erb +5 -0
  11. data/app/views/with_filters/filter/_select.html.erb +2 -0
  12. data/app/views/with_filters/filter/_select_range.html.erb +4 -0
  13. data/app/views/with_filters/filter/_text.html.erb +2 -0
  14. data/app/views/with_filters/filter/_text_range.html.erb +4 -0
  15. data/changelog.md +2 -0
  16. data/lib/generators/with_filters/theme/theme_generator.rb +43 -0
  17. data/lib/with_filters/action_view_extension.rb +110 -0
  18. data/lib/with_filters/active_record_extension.rb +26 -0
  19. data/lib/with_filters/active_record_model_extension.rb +163 -0
  20. data/lib/with_filters/engine.rb +5 -0
  21. data/lib/with_filters/hash_extraction.rb +31 -0
  22. data/lib/with_filters/models/action.rb +14 -0
  23. data/lib/with_filters/models/filter/base.rb +36 -0
  24. data/lib/with_filters/models/filter/base_range.rb +42 -0
  25. data/lib/with_filters/models/filter/check_box.rb +20 -0
  26. data/lib/with_filters/models/filter/choice.rb +23 -0
  27. data/lib/with_filters/models/filter/collection.rb +28 -0
  28. data/lib/with_filters/models/filter/filter.rb +59 -0
  29. data/lib/with_filters/models/filter/radio.rb +7 -0
  30. data/lib/with_filters/models/filter/select.rb +22 -0
  31. data/lib/with_filters/models/filter/select_range.rb +15 -0
  32. data/lib/with_filters/models/filter/text.rb +30 -0
  33. data/lib/with_filters/models/filter/text_range.rb +15 -0
  34. data/lib/with_filters/models/filter_form.rb +93 -0
  35. data/lib/with_filters/value_prep/boolean_prep.rb +10 -0
  36. data/lib/with_filters/value_prep/date_prep.rb +10 -0
  37. data/lib/with_filters/value_prep/date_time_prep.rb +51 -0
  38. data/lib/with_filters/value_prep/default_prep.rb +88 -0
  39. data/lib/with_filters/value_prep/value_prep.rb +28 -0
  40. data/lib/with_filters/version.rb +3 -0
  41. data/lib/with_filters.rb +32 -0
  42. data/spec/active_record_model_extension_spec.rb +435 -0
  43. data/spec/dummy/README.rdoc +261 -0
  44. data/spec/dummy/Rakefile +7 -0
  45. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  46. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  47. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  48. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  49. data/spec/dummy/app/mailers/.gitkeep +0 -0
  50. data/spec/dummy/app/models/.gitkeep +0 -0
  51. data/spec/dummy/app/models/date_time_tester.rb +2 -0
  52. data/spec/dummy/app/models/field_format_tester.rb +2 -0
  53. data/spec/dummy/app/models/nobel_prize.rb +3 -0
  54. data/spec/dummy/app/models/nobel_prize_winner.rb +3 -0
  55. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  56. data/spec/dummy/config/application.rb +56 -0
  57. data/spec/dummy/config/boot.rb +10 -0
  58. data/spec/dummy/config/database.yml +25 -0
  59. data/spec/dummy/config/environment.rb +5 -0
  60. data/spec/dummy/config/environments/development.rb +37 -0
  61. data/spec/dummy/config/environments/production.rb +67 -0
  62. data/spec/dummy/config/environments/test.rb +37 -0
  63. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  64. data/spec/dummy/config/initializers/inflections.rb +15 -0
  65. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  66. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  67. data/spec/dummy/config/initializers/session_store.rb +8 -0
  68. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  69. data/spec/dummy/config/locales/en.yml +5 -0
  70. data/spec/dummy/config/routes.rb +58 -0
  71. data/spec/dummy/config.ru +4 -0
  72. data/spec/dummy/db/migrate/20111227224959_additional_columns.rb +73 -0
  73. data/spec/dummy/db/migrate/20120127203225_create_nobel_prize_winners.rb +30 -0
  74. data/spec/dummy/db/migrate/20120203212237_create_nobel_prizes.rb +34 -0
  75. data/spec/dummy/db/migrate/20120209051208_modify_updated_at.rb +13 -0
  76. data/spec/dummy/db/migrate/20120210163052_change_created_at.rb +17 -0
  77. data/spec/dummy/db/migrate/20120214172946_fix_einstein.rb +9 -0
  78. data/spec/dummy/db/migrate/20120227200013_create_date_time_testers.rb +25 -0
  79. data/spec/dummy/db/migrate/20120309202722_create_field_format_testers.rb +22 -0
  80. data/spec/dummy/db/migrate/20120310195447_update_field_format_testers.rb +15 -0
  81. data/spec/dummy/db/schema.rb +51 -0
  82. data/spec/dummy/db/test.sqlite3 +0 -0
  83. data/spec/dummy/lib/assets/.gitkeep +0 -0
  84. data/spec/dummy/log/.gitkeep +0 -0
  85. data/spec/dummy/public/404.html +26 -0
  86. data/spec/dummy/public/422.html +26 -0
  87. data/spec/dummy/public/500.html +25 -0
  88. data/spec/dummy/public/favicon.ico +0 -0
  89. data/spec/dummy/script/rails +6 -0
  90. data/spec/generators/with_filters_theme_spec.rb +33 -0
  91. data/spec/hash_extraction_spec.rb +51 -0
  92. data/spec/helpers/action_view_extension_spec.rb +345 -0
  93. data/spec/models/action.rb +20 -0
  94. data/spec/models/filter/base_range_spec.rb +32 -0
  95. data/spec/models/filter/base_spec.rb +76 -0
  96. data/spec/models/filter/check_box_spec.rb +36 -0
  97. data/spec/models/filter/choice_spec.rb +24 -0
  98. data/spec/models/filter/collection_spec.rb +72 -0
  99. data/spec/models/filter/filter_spec.rb +35 -0
  100. data/spec/models/filter/select_spec.rb +12 -0
  101. data/spec/models/filter/text_spec.rb +16 -0
  102. data/spec/models/filter_form_spec.rb +212 -0
  103. data/spec/spec_helper.rb +12 -0
  104. data/spec/value_prep/boolean_prep_spec.rb +13 -0
  105. data/spec/value_prep/date_prep_spec.rb +28 -0
  106. data/spec/value_prep/date_time_prep_spec.rb +106 -0
  107. data/spec/value_prep/default_prep_spec.rb +43 -0
  108. data/with_filters.gemspec +27 -0
  109. metadata +280 -0
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ *.swp
6
+ spec/dummy/log/*.log
7
+ .yardoc
8
+ doc
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private --markup markdown
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in with_filters.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Aaron Lasseigne
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # with_filters
2
+
3
+ Add filtering to tables, lists, etc.
4
+
5
+ This project follows [Semantic Versioning](http://semver.org/).
6
+
7
+ ## Installation
8
+
9
+ $ gem install with_filters
10
+
11
+ If you're using Bundler, add this to your Gemfile:
12
+
13
+ gem 'with_filters', '~> 0.1.0'
14
+
15
+ ## Support
16
+
17
+ <table>
18
+ <tr>
19
+ <td><strong>Ruby</strong></td>
20
+ <td>1.9</td>
21
+ </tr>
22
+ <tr>
23
+ <td><strong>Rails</strong></td>
24
+ <td>3.1</td>
25
+ </tr>
26
+ <tr>
27
+ <td><strong>Database Framework</strong></td>
28
+ <td>ActiveRecord</td>
29
+ </tr>
30
+ </table>
31
+
32
+ ## Usage
33
+
34
+ In your controller:
35
+
36
+ @data = Data.with_filters(params, fields: {
37
+ email_address: {match: :contains},
38
+ full_name: ->(value, scope) {
39
+ first_word, second_word = value.strip.split(/\s+/)
40
+
41
+ if second_word
42
+ scope.where(['first_name LIKE ? OR last_name LIKE ?', first_word, first_word])
43
+ else
44
+ scope.where(['first_name LIKE ? AND last_name LIKE ?', first_word, second_word])
45
+ end
46
+ }
47
+ }).order('created_at DESC').page(params[:page] || 1)
48
+
49
+ In your view:
50
+
51
+ <%= filter_form_for(@data) do |f| %>
52
+ <%# pass through variables needed in the form but not associated with filtering %>
53
+ <% if params[:order] %>
54
+ <% f.hidden(:order, name: 'order', value: params[:order]) %>
55
+ <% end %>
56
+
57
+ <% f.input(:id) %>
58
+ <% f.input_range(:created_at, label: 'Account Created On') %> <%# creates a filter to search a range %>
59
+ <% f.input(:full_name) %>
60
+ <% f.input(:email_address) %>
61
+
62
+ <% f.action(:submit, label: 'Filter') %>
63
+ <% end %>
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,14 @@
1
+ <%= form_tag(nil, filter_form.attrs) do %>
2
+ <%= with_filters_hidden(filter_form.hidden_filters) %>
3
+
4
+ <% filter_form.filters.each do |filter| %>
5
+ <%= with_filters_input(filter) %><br/>
6
+ <% end %>
7
+
8
+ <% if filter_form.actions.any? %>
9
+ <br/>
10
+ <% filter_form.actions.each do |action| %>
11
+ <%= with_filters_action_tag(action) %>
12
+ <% end %>
13
+ <% end %>
14
+ <% end %>
@@ -0,0 +1,2 @@
1
+ <%= with_filters_label_tag(filter) %><br/>
2
+ <%= check_box_tag(filter.field_name, filter.value, filter.selected?, filter.attrs) %>
@@ -0,0 +1,5 @@
1
+ <%= with_filters_label(filter) %>
2
+ <% filter.collection.each do |choice| %>
3
+ <%= check_box_tag(choice.field_name, choice.value, choice.selected?, choice.attrs) %>
4
+ <%= label_tag(choice.attrs[:id], choice.label) %><br/>
5
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <%= with_filters_label(filter) %>
2
+ <% filter.collection.each do |choice| %>
3
+ <%= radio_button_tag(choice.field_name, choice.value, choice.selected?, choice.attrs) %>
4
+ <%= label_tag(choice.attrs[:id], choice.label) %><br/>
5
+ <% end %>
@@ -0,0 +1,2 @@
1
+ <%= with_filters_label_tag(filter) %><br/>
2
+ <%= with_filters_select_tag(filter) %>
@@ -0,0 +1,4 @@
1
+ <%= with_filters_label_tag(filter.start) %><br/>
2
+ <%= with_filters_select_tag(filter.start) %><br/>
3
+ <%= with_filters_label_tag(filter.stop) %><br/>
4
+ <%= with_filters_select_tag(filter.stop) %>
@@ -0,0 +1,2 @@
1
+ <%= with_filters_label_tag(filter) %><br/>
2
+ <%= with_filters_text_field_tag(filter) %>
@@ -0,0 +1,4 @@
1
+ <%= with_filters_label_tag(filter.start) %><br/>
2
+ <%= with_filters_text_field_tag(filter.start) %><br/>
3
+ <%= with_filters_label_tag(filter.stop) %><br/>
4
+ <%= with_filters_text_field_tag(filter.stop) %>
data/changelog.md ADDED
@@ -0,0 +1,2 @@
1
+ ## 0.1.0
2
+ * initial release
@@ -0,0 +1,43 @@
1
+ module WithFilters
2
+ class ThemeGenerator < Rails::Generators::NamedBase
3
+ desc('Create a new theme.')
4
+
5
+ VIEW_PATH = 'app/views/with_filters'
6
+
7
+ source_root File.expand_path("../../../../../#{VIEW_PATH}", __FILE__)
8
+
9
+ argument :partial, required: false
10
+
11
+ # A generator to create a theme. Any files missing from the theme will fall
12
+ # back to the original theme. It's recommended that you only generate the
13
+ # files you plan to change.
14
+ #
15
+ # @example Create an entire theme.
16
+ # $ rails generate with_filters:theme foo
17
+ #
18
+ # @example Create a single file.
19
+ # $ rails generate with_filters:theme foo filter_form
20
+ #
21
+ # @since 0.1.0
22
+ def create_theme
23
+ empty_directory(VIEW_PATH)
24
+
25
+ if partial
26
+ if partial.index('/')
27
+ (extra_dirs, partial_name) = partial.match(/^(.*)\/(.*)$/).captures
28
+
29
+ empty_directory("#{VIEW_PATH}/#{file_name}/#{extra_dirs}")
30
+
31
+ copy_file(
32
+ "#{extra_dirs}/_#{partial_name.match(/^text_as_/) ? 'text' : partial_name}.html.erb",
33
+ "#{VIEW_PATH}/#{file_name}/#{extra_dirs}/_#{partial_name}.html.erb"
34
+ )
35
+ else
36
+ copy_file("_#{partial}.html.erb", "#{VIEW_PATH}/#{file_name}/_#{partial}.html.erb")
37
+ end
38
+ else
39
+ directory('', "#{VIEW_PATH}/#{file_name}")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,110 @@
1
+ module WithFilters
2
+ module ActionViewExtension
3
+ include WithFilters::HashExtraction
4
+
5
+ # Create a filter form.
6
+ #
7
+ # @param records [ActiveRecord::Relation, Array] The data being filtered.
8
+ # @param options [Hash]
9
+ # @option options [String] :theme The theme to use when rendering the form.
10
+ # @option options [Varied] remaining ("{'novalidate: 'novalidate', method: 'get'}")
11
+ # All other options are passed as options to the `form_tag` helper.
12
+ #
13
+ # @example
14
+ # <%= filter_form_for(@data, theme: 'foo', class: 'bar') do |f|
15
+ # f.input(:full_name)
16
+ # f.input(:email)
17
+ # end %>
18
+ #
19
+ # @since 0.1.0
20
+ def filter_form_for(records, options = {})
21
+ filter_form = WithFilters::FilterForm.new(records, self.extract_hash_value(params, records.with_filters_data[:param_namespace]) || {}, options)
22
+ yield(filter_form)
23
+
24
+ render(partial: filter_form.to_partial_path, locals: {filter_form: filter_form})
25
+ end
26
+
27
+ # Create hidden inputs.
28
+ #
29
+ # @param [Array<Filter>] hidden_filters
30
+ #
31
+ # @since 0.1.0
32
+ def with_filters_hidden(hidden_filters)
33
+ hidden_filters.map{|hidden_filter|
34
+ hidden_field_tag(hidden_filter.field_name, hidden_filter.value, hidden_filter.attrs)
35
+ }.join("\n").html_safe
36
+ end
37
+
38
+ # Create an input based on the type of `filter` provided.
39
+ #
40
+ # @param [Filter] filter
41
+ #
42
+ # @since 0.1.0
43
+ def with_filters_input(filter)
44
+ render(partial: filter.to_partial_path, locals: {filter: filter})
45
+ end
46
+
47
+ # Create an text like `input` tag based on the `filter`.
48
+ #
49
+ # @param [Filter] filter
50
+ #
51
+ # @since 0.1.0
52
+ def with_filters_text_field_tag(filter)
53
+ text_field_tag(filter.field_name, filter.value, filter.attrs)
54
+ end
55
+
56
+ # Create a `label` tag based on the `filter`.
57
+ #
58
+ # @param [Filter] filter
59
+ #
60
+ # @since 0.1.0
61
+ def with_filters_label_tag(filter)
62
+ label_tag(filter.field_name, filter.label, filter.label_attrs)
63
+ end
64
+
65
+ # Create a `label` tag for individual fields or a `div` tag in its place in
66
+ # the case of fields where the `label` tags are used for individual items.
67
+ #
68
+ # @param [Filter] filter
69
+ #
70
+ # @since 0.1.0
71
+ def with_filters_label(filter)
72
+ if [WithFilters::Filter::Radio, WithFilters::Filter::CheckBox].include?(filter.class) and filter.collection.any?
73
+ content_tag(:div, filter.label, filter.label_attrs)
74
+ else
75
+ with_filters_label_tag(filter)
76
+ end
77
+ end
78
+
79
+ # Create a `select` tag based on the `filter`.
80
+ #
81
+ # @param [Filter] filter
82
+ #
83
+ # @since 0.1.0
84
+ def with_filters_select_tag(filter)
85
+ collection = filter.collection
86
+ unless filter.collection.is_a?(String)
87
+ collection = filter.collection.map do |choice|
88
+ html_attributes = choice.attrs.length > 0 ? ' ' + choice.attrs.map {|k, v| %(#{k.to_s}="#{v}")}.join(' ') : ''
89
+ selected_attribute = choice.selected? ? ' selected="selected"' : ''
90
+
91
+ %(<option value="#{ERB::Util.html_escape(choice.value)}"#{selected_attribute}#{html_attributes}>#{ERB::Util.html_escape(choice.label)}</option>)
92
+ end.join("\n")
93
+ end
94
+
95
+ select_tag(filter.field_name, collection.html_safe, filter.attrs)
96
+ end
97
+
98
+ # @param [WithFilters::Action] action
99
+ #
100
+ # @since 0.1.0
101
+ def with_filters_action_tag(action)
102
+ case action.type
103
+ when :submit
104
+ submit_tag(action.attrs.delete(:value), action.attrs)
105
+ when :reset
106
+ button_tag(action.attrs.delete(:value) || 'Reset', action.attrs)
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,26 @@
1
+ module WithFilters
2
+ # @private
3
+ module ActiveRecordExtension
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class << self
8
+ alias_method_chain :inherited, :with_filters
9
+ end
10
+
11
+ # Attach the ActiveRecord extensions to children of ActiveRecord that were initiated before we loaded WithFilters.
12
+ self.descendants.each do |descendant|
13
+ descendant.send(:include, WithFilters::ActiveRecordModelExtension) if descendant.superclass == ActiveRecord::Base
14
+ end
15
+ end
16
+
17
+ # @private
18
+ module ClassMethods
19
+ # Attaches the ActiveRecord extensions to children of ActiveRecord so we don't pollute ActiveRecord::Base.
20
+ def inherited_with_with_filters(base)
21
+ inherited_without_with_filters(base)
22
+ base.send(:include, WithFilters::ActiveRecordModelExtension) if base.superclass == ActiveRecord::Base
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,163 @@
1
+ module WithFilters
2
+ module ActiveRecordModelExtension
3
+ extend ActiveSupport::Concern
4
+
5
+ # @private
6
+ module AddData
7
+ # @return [Hash<:column_types, :param_namespace>] Properties relating to the current filters.
8
+ #
9
+ # @since 0.1.0
10
+ attr_accessor :with_filters_data
11
+ end
12
+
13
+ # Attaches conditions to a scope.
14
+ #
15
+ # @scope class
16
+ # @overload with_filters(params = nil, options = {})
17
+ # @param [Hash] params A hash of values to filter on.
18
+ # @param [Hash] options
19
+ # @option options [Symbol] :param_namespace ('primary table name') The namespace
20
+ # of the filters inside `params`.
21
+ # @option options [Hash, Proc] :fields When a hash is passed this is a mapping
22
+ # of custom fields to field options. When a proc is passed it is sent `value`
23
+ # and `scope` and is expected to return the scope.
24
+ # @option options [String] :fields[:column] The database column to map the field to.
25
+ # @option options [Symbol] :fields[:match] Determines the way the filter is matched.
26
+ # Accepts `:exact`, `:contains`, `:begins_with` and `:ends_with`.
27
+ #
28
+ # @example Basic
29
+ # with_filters(params)
30
+ #
31
+ # @example With a custom matching and field name.
32
+ # with_filters(params, {fields: {
33
+ # given_name: {column: 'first_name', match: :contains}
34
+ # }})
35
+ #
36
+ # @example With a custom field using a proc.
37
+ # with_filters(params, {fields: {
38
+ # full_name: ->(value, scope) {
39
+ # first_word, second_word = value.strip.split(/\s+/)
40
+ #
41
+ # if second_word
42
+ # scope.where(['first_name LIKE ? OR last_name LIKE ?', first_word, first_word])
43
+ # else
44
+ # scope.where(['first_name LIKE ? AND last_name LIKE ?', first_word, second_word])
45
+ # end
46
+ # }
47
+ # }})
48
+ #
49
+ # @return [ActiveRecord::Relation]
50
+ #
51
+ # @note Switched from `scope` to class method because of a bug in Rails 3.2.1 where
52
+ # `joins_values` aren't available in scopes.
53
+ #
54
+ # @since 0.1.0
55
+ included do
56
+ extend WithFilters::HashExtraction
57
+ def self.with_filters(params = nil, options = {})
58
+ relation = self.scoped.extend(AddData).extending do
59
+ def to_a
60
+ a = super.extend(AddData)
61
+ a.with_filters_data = self.with_filters_data
62
+ a
63
+ end
64
+ end
65
+ param_namespace = options.delete(:param_namespace) || relation.table_name.to_sym
66
+ relation.with_filters_data = {
67
+ param_namespace: param_namespace,
68
+ column_types: find_column_types(relation, options[:fields] || {})
69
+ }
70
+
71
+ scoped_params = params ? self.extract_hash_value(params, param_namespace) || {} : {}
72
+ scoped_params.each do |field, value|
73
+ # skip blank entries
74
+ value.reject!{|v| v.blank?} if value.is_a?(Array)
75
+ if (value.is_a?(String) and value.blank?) or
76
+ (value.is_a?(Array) and value.empty?) or
77
+ (value.is_a?(Hash) and not (value[:start].present? and value[:stop].present?))
78
+ next
79
+ end
80
+
81
+ field_options = {}
82
+ field_options = options[:fields][field] if options[:fields] and options[:fields][field]
83
+
84
+ if field_options.is_a?(Proc)
85
+ relation = field_options.call(value, relation)
86
+ else
87
+ db_column_table_name, db_column_name = (field_options.delete(:column) || field).to_s.split('.')
88
+ if db_column_name.nil?
89
+ db_column_name = db_column_table_name
90
+ db_column_table_name = relation.column_names.include?(db_column_name) ? self.table_name : nil
91
+ end
92
+
93
+ quoted_field = relation.connection.quote_column_name(db_column_name)
94
+ quoted_field = "#{db_column_table_name}.#{quoted_field}" if db_column_table_name
95
+
96
+ value = WithFilters::ValuePrep.prepare(relation.with_filters_data[:column_types][field.to_sym], value, field_options)
97
+
98
+ # attach filter
99
+ relation = case value.class.name.to_sym
100
+ when :Array
101
+ relation.where([Array.new(value.size, "#{quoted_field} LIKE ?").join(' OR '), *value])
102
+ when :Hash
103
+ if ![:datetime, :timestamp].include?(relation.with_filters_data[:column_types][field]) or Date._parse(value[:start])[:sec_fraction]
104
+ relation.where(["#{quoted_field} BETWEEN :start AND :stop", value])
105
+ else
106
+ relation.where(["#{quoted_field} >= :start AND #{quoted_field} < :stop", value])
107
+ end
108
+ when :String, :FalseClass, :TrueClass, :Date, :Time
109
+ relation.where(["#{quoted_field} LIKE ?", value])
110
+ else
111
+ relation
112
+ end
113
+ end
114
+ end
115
+
116
+ relation
117
+ end
118
+ end
119
+
120
+ # @private
121
+ module ClassMethods
122
+ # @param [ActiveRecord::Relation] relation A relation to find the columns of.
123
+ # @param [Hash] field_options
124
+ #
125
+ # @return [Hash<String>] A mapping of fields to their database types.
126
+ #
127
+ # @since 0.1.0
128
+ def find_column_types(relation, field_options)
129
+ field_options = field_options.reject{|k, v| v.is_a?(Proc)}
130
+
131
+ # primary table column types
132
+ column_types = Hash[*relation.columns.map{|column|
133
+ [
134
+ field_options.detect{|field, options|
135
+ [column.name, "#{relation.table_name}.#{column.name}"].include?(options[:column].to_s)
136
+ }.try(:first) || column.name.to_sym,
137
+ column.type
138
+ ]
139
+ }.flatten]
140
+
141
+ # non-primary table columns
142
+ (relation.joins_values + relation.includes_values).uniq.map{|join|
143
+ # convert string joins to table names
144
+ if join.is_a?(String)
145
+ join.scan(/\G(?:(?:,|\bjoin\s)\s*(\w+))/i)
146
+ else
147
+ join
148
+ end
149
+ }.flatten.map do |table_name|
150
+ ActiveRecord::Base::connection.columns(table_name.to_s.tableize).each do |column|
151
+ column_name = field_options.detect{|field, options|
152
+ [column.name, "#{table_name}.#{column.name}"].include?(options[:column].to_s)
153
+ }.try(:first) || column.name.to_sym
154
+
155
+ column_types.reverse_merge!(column_name => column.type)
156
+ end
157
+ end
158
+
159
+ column_types
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,5 @@
1
+ module WithFilters
2
+ # @private
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
@@ -0,0 +1,31 @@
1
+ module WithFilters
2
+ # @private
3
+ module HashExtraction
4
+ # Extracts the value from a hash and takes nesting into account.
5
+ #
6
+ # @param [Hash] hash The hash to search.
7
+ # @param [String, Symbol] key The key or nested key to search for.
8
+ #
9
+ # @return [Object, nil]
10
+ #
11
+ # @example Key is a symbol.
12
+ # extract_hash_key({foo: 'bar'}, :foo) # => 'bar'
13
+ #
14
+ # @example Key is a string with nesting.
15
+ # extract_hash_key({foo: {bar: 'baz'}}, 'foo[:bar]') # => 'baz'
16
+ #
17
+ # @since 0.1.0
18
+ def extract_hash_value(hash, key)
19
+ key = key.to_s
20
+ hash = hash.stringify_keys
21
+
22
+ if hash[key]
23
+ hash[key]
24
+ else
25
+ first_key, remaining_content = key.to_s.match(/^([^\[]+)(.*)$/).captures
26
+
27
+ eval "hash[first_key]#{remaining_content.gsub(/\[/, '["').gsub(/]/, '"]')} rescue nil"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,14 @@
1
+ module WithFilters
2
+ class Action
3
+ attr_accessor :type, :attrs
4
+
5
+ def initialize(type, options = {})
6
+ @type = type
7
+
8
+ options[:type] = @type
9
+ options[:value] = options.delete(:label) if options[:label]
10
+
11
+ @attrs = options
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,36 @@
1
+ module WithFilters
2
+ module Filter
3
+ # @private
4
+ class Base
5
+ attr_reader :to_partial_path, :label, :label_attrs, :field_name, :value, :attrs, :collection
6
+
7
+ def initialize(name, namespace, value, options = {})
8
+ @value = value
9
+
10
+ @theme = options.delete(:theme)
11
+ @label = options.delete(:label) || name.to_s.titleize
12
+ @label_attrs = options.delete(:label_attrs) || {}
13
+ @field_name = options[:field_name] || "#{namespace}[#{name}]"
14
+ @collection = options[:collection] ? Collection.new(@field_name, options.delete(:collection) || [], {selected: value}) : nil
15
+ @attrs = options
16
+
17
+ @to_partial_path = create_partial_path
18
+ end
19
+
20
+ private
21
+
22
+ def create_partial_path
23
+ partial_path = self.class.name.underscore
24
+
25
+ if @theme
26
+ themed_partial_path = partial_path.split(File::SEPARATOR).insert(1, @theme)
27
+ if Dir.glob(File.join(Rails.root, 'app', 'views', *themed_partial_path).sub(/([^#{File::SEPARATOR}]+?)$/, '_\1.*')).any?
28
+ partial_path = themed_partial_path.join(File::SEPARATOR)
29
+ end
30
+ end
31
+
32
+ partial_path
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ module WithFilters
2
+ module Filter
3
+ # @private
4
+ class BaseStart < Base
5
+ def initialize(name, namespace, value, options = {})
6
+ super
7
+
8
+ @field_name << '[start]'
9
+ end
10
+ end
11
+
12
+ # @private
13
+ class BaseStop < Base
14
+ def initialize(name, namespace, value, options = {})
15
+ super
16
+
17
+ @field_name << '[stop]'
18
+ end
19
+ end
20
+
21
+ # @private
22
+ class BaseRange < Base
23
+ attr_reader :start, :stop
24
+
25
+ def initialize(name, namespace, value, options = {})
26
+ start_attrs = options.delete(:start) || {}
27
+ stop_attrs = options.delete(:stop) || {}
28
+
29
+ super
30
+
31
+ start_attrs.reverse_merge!(@attrs)
32
+ start_attrs.reverse_merge!(label: self.label, label_attrs: self.label_attrs, collection: @collection)
33
+ stop_attrs.reverse_merge!(@attrs)
34
+ stop_attrs.reverse_merge!(label: self.label, label_attrs: self.label_attrs, collection: @collection)
35
+
36
+ base_class_name = self.class.to_s.match(/^(.*)Range$/).captures.first
37
+ @start = "#{base_class_name}Start".constantize.new(name, namespace, value[:start], start_attrs)
38
+ @stop = "#{base_class_name}Stop".constantize.new(name, namespace, value[:stop], stop_attrs)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,20 @@
1
+ module WithFilters
2
+ module Filter
3
+ # @private
4
+ class CheckBox < Base
5
+ def initialize(name, namespace, value, options = {})
6
+ super
7
+
8
+ @to_partial_path << 'es' unless self.collection.nil?
9
+ end
10
+
11
+ def selected?
12
+ if self.collection.nil?
13
+ value.to_s == 'on'
14
+ else
15
+ value.present?
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end