with_filters 0.1.0

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.
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