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
@@ -0,0 +1,23 @@
1
+ module WithFilters
2
+ module Filter
3
+ # @private
4
+ class Choice
5
+ attr_reader :field_name, :label, :value, :attrs
6
+
7
+ def initialize(field_name, label, value, options = {})
8
+ @field_name = "#{field_name}[]"
9
+ @label = label
10
+ @value = value
11
+
12
+ options[:id] ||= "#{field_name}_#{value}".gsub(']', '').gsub(/[^-a-zA-Z0-9:.]/, '_')
13
+
14
+ @selected = !!options.delete(:selected)
15
+ @attrs = options
16
+ end
17
+
18
+ def selected?
19
+ @selected
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,28 @@
1
+ module WithFilters
2
+ module Filter
3
+ # @private
4
+ class Collection < Array
5
+ def initialize(field_name, choices, options = {})
6
+ choices = choices.to_a if choices.is_a?(Range)
7
+
8
+ selected = Array.wrap(options[:selected]).map(&:to_s)
9
+
10
+ choices.map do |element|
11
+ text, value, choice_options = if element.is_a?(Array)
12
+ html_attrs = element.detect {|e| Hash === e} || {}
13
+ element = element.reject {|e| Hash === e}
14
+ [element.first.to_s, element.last, html_attrs]
15
+ elsif !element.is_a?(String) && element.respond_to?(:first) && element.respond_to?(:last)
16
+ [element.first.to_s, element.last, {}]
17
+ else
18
+ [element.to_s, element, {}]
19
+ end
20
+
21
+ choice_options[:selected] = 'selected' if selected.include?(value.to_s)
22
+
23
+ self.push(Choice.new(field_name, text, value, choice_options))
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,59 @@
1
+ module WithFilters
2
+ # @private
3
+ module Filter
4
+ TYPES = {
5
+ :'datetime-local' => Text,
6
+ text: Text,
7
+ email: Text,
8
+ url: Text,
9
+ tel: Text,
10
+ number: Text,
11
+ range: Text,
12
+ date: Text,
13
+ month: Text,
14
+ week: Text,
15
+ time: Text,
16
+ datetime: Text,
17
+ search: Text,
18
+ color: Text,
19
+ hidden: Text,
20
+ checkbox: CheckBox,
21
+ radio: Radio,
22
+ select: Select
23
+ }
24
+
25
+ RANGED_TYPES = {
26
+ :'datetime-local' => TextRange,
27
+ text: TextRange,
28
+ email: TextRange,
29
+ url: TextRange,
30
+ tel: TextRange,
31
+ number: TextRange,
32
+ range: TextRange,
33
+ date: TextRange,
34
+ month: TextRange,
35
+ week: TextRange,
36
+ time: TextRange,
37
+ datetime: TextRange,
38
+ search: TextRange,
39
+ color: TextRange,
40
+ select: SelectRange
41
+ }
42
+
43
+ def self.create(name, namespace, value, options = {})
44
+ as = options.delete(:as)
45
+
46
+ options[:type] = as.to_s
47
+
48
+ TYPES[as].new(name, namespace, value, options)
49
+ end
50
+
51
+ def self.create_range(name, namespace, value, options = {})
52
+ as = options.delete(:as)
53
+
54
+ options[:type] = as.to_s
55
+
56
+ RANGED_TYPES[as].new(name, namespace, value, options)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,7 @@
1
+ module WithFilters
2
+ module Filter
3
+ # @private
4
+ class Radio < Base
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ module WithFilters
2
+ module Filter
3
+ # @private
4
+ class Select < Base
5
+ def initialize(name, namespace, value, options = {})
6
+ collection = options.delete(:collection) if options[:collection].is_a?(String)
7
+ if collection
8
+ Array.wrap(value).each do |v|
9
+ matched = collection.sub!(/(<option[^>]*value\s*=\s*['"]?#{v}[^>]*)/, '\1 selected="selected"')
10
+ unless matched
11
+ collection.sub!(/>#{v}</, " selected=\"selected\">#{v}<")
12
+ end
13
+ end
14
+ end
15
+
16
+ super
17
+
18
+ @collection = collection if collection
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,15 @@
1
+ module WithFilters
2
+ module Filter
3
+ # @private
4
+ class SelectStart < BaseStart
5
+ end
6
+
7
+ # @private
8
+ class SelectStop < BaseStop
9
+ end
10
+
11
+ # @private
12
+ class SelectRange < BaseRange
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,30 @@
1
+ module WithFilters
2
+ module Filter
3
+ # @private
4
+ class Text < Base
5
+ def initialize(name, namespace, value, options = {})
6
+ super
7
+
8
+ if @attrs[:type] != :text
9
+ new_partial = @to_partial_path.split(File::SEPARATOR)
10
+ new_partial[-1] = "_#{new_partial[-1]}_as_#{@attrs[:type]}.*"
11
+
12
+ @to_partial_path << "_as_#{@attrs[:type]}" if Dir.glob(File.join(Rails.root, 'app', 'views', *new_partial)).any?
13
+ end
14
+ end
15
+
16
+ def create_partial_path
17
+ partial_path = nil
18
+ if @theme and @attrs[:type] != :text
19
+ themed_partial_path = self.class.name.underscore.split(File::SEPARATOR).insert(1, @theme)
20
+ themed_partial_path[themed_partial_path.length - 1] << "_as_#{@attrs[:type]}"
21
+ if Dir.glob(File.join(Rails.root, 'app', 'views', *themed_partial_path).sub(/([^#{File::SEPARATOR}]+?)$/, '_\1.*')).any?
22
+ partial_path = themed_partial_path.join(File::SEPARATOR)
23
+ end
24
+ end
25
+
26
+ partial_path || super
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ module WithFilters
2
+ module Filter
3
+ # @private
4
+ class TextStart < BaseStart
5
+ end
6
+
7
+ # @private
8
+ class TextStop < BaseStop
9
+ end
10
+
11
+ # @private
12
+ class TextRange < BaseRange
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,93 @@
1
+ module WithFilters
2
+ class FilterForm
3
+ attr_reader :attrs, :to_partial_path, :filters, :param_namespace, :hidden_filters, :actions
4
+
5
+ # @see ActionViewExtension#filter_form_for
6
+ #
7
+ # @since 0.1.0
8
+ def initialize(records, values = {}, options = {})
9
+ @records = records
10
+ @values = values
11
+
12
+ @theme = options.delete(:theme)
13
+ @attrs = options.reverse_merge(novalidate: 'novalidate', method: 'get')
14
+
15
+ @to_partial_path = self.class.name.underscore
16
+ @param_namespace = @records.with_filters_data[:param_namespace]
17
+ @hidden_filters = []
18
+ @filters = []
19
+ @actions = []
20
+ end
21
+
22
+ # @see input
23
+ #
24
+ # @since 0.1.0
25
+ def hidden(name, options = {})
26
+ options.merge!(as: :hidden)
27
+
28
+ input(name, options)
29
+ end
30
+
31
+ # @param [Symbol] name
32
+ # @param [Hash] options
33
+ #
34
+ # @since 0.1.0
35
+ def input(name, options = {})
36
+ options[:as] = find_as(name, options[:collection]) unless options[:as]
37
+ options.merge!(theme: @theme)
38
+ as = options[:as]
39
+
40
+ filter = WithFilters::Filter.create(name, self.param_namespace, @values[name], options)
41
+
42
+ (as == :hidden ? @hidden_filters : @filters).push(filter)
43
+ end
44
+
45
+ # @param [Symbol] name
46
+ # @param [Hash] options
47
+ #
48
+ # @since 0.1.0
49
+ def input_range(name, options = {})
50
+ options[:as] = find_as(name, options[:collection]) unless options[:as]
51
+ options.merge!(theme: @theme)
52
+
53
+ @filters.push(WithFilters::Filter.create_range(name, self.param_namespace, @values[name] || {}, options))
54
+ end
55
+
56
+ # @param [Symbol] type
57
+ # @param [Hash] options
58
+ #
59
+ # @since 0.1.0
60
+ def action(type, options = {})
61
+ @actions.push(WithFilters::Action.new(type, options))
62
+ end
63
+
64
+ private
65
+
66
+ # Converts a database column type to an input type.
67
+ #
68
+ # @param [Symbol] name
69
+ # @param [Boolean] has_collection Indicates whether or not there is a collection
70
+ # associated with the input data.
71
+ #
72
+ # @since 0.1.0
73
+ def find_as(name, has_collection)
74
+ return :select if has_collection
75
+
76
+ case @records.with_filters_data[:column_types][name]
77
+ when :integer, :float, :decimal then :number
78
+ when :date then :date
79
+ when :time then :time
80
+ when :datetime, :timestamp then :datetime
81
+ when :boolean then :checkbox
82
+ when :string
83
+ case name
84
+ when /email/ then :email
85
+ when /url/ then :url
86
+ when /phone/ then :tel
87
+ else :text
88
+ end
89
+ else :text
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,10 @@
1
+ module WithFilters
2
+ module ValuePrep
3
+ # @private
4
+ class BooleanPrep < DefaultPrep
5
+ def prepare_value(value)
6
+ (super == 'on')
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module WithFilters
2
+ module ValuePrep
3
+ # @private
4
+ class DatePrep < DefaultPrep
5
+ def prepare_value(value)
6
+ super.to_date
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,51 @@
1
+ module WithFilters
2
+ module ValuePrep
3
+ # @private
4
+ class DateTimePrep < DefaultPrep
5
+ def prepare_value(value)
6
+ value = super
7
+ date_info = Date._parse(value)
8
+
9
+ if date_info[:sec_fraction]
10
+ to_parsed_s(value, {}, date_info[:sec_fraction].to_f)
11
+ else
12
+ {start: prepare_start_value(value), stop: prepare_stop_value(value)}
13
+ end
14
+ end
15
+
16
+ def prepare_start_value(value)
17
+ date_info = Date._parse(value)
18
+
19
+ if date_info[:sec_fraction]
20
+ to_parsed_s(value, {}, date_info[:sec_fraction])
21
+ else
22
+ to_parsed_s(value)
23
+ end
24
+ end
25
+
26
+ def prepare_stop_value(value)
27
+ date_info = Date._parse(value)
28
+
29
+ if date_info[:sec_fraction]
30
+ to_parsed_s(value, {}, date_info[:sec_fraction])
31
+ elsif date_info[:sec]
32
+ to_parsed_s(value, seconds: 1)
33
+ elsif date_info[:min]
34
+ to_parsed_s(value, minutes: 1)
35
+ elsif date_info[:hour]
36
+ to_parsed_s(value, hours: 1)
37
+ elsif date_info[:mday]
38
+ to_parsed_s(value, days: 1)
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def to_parsed_s(value, advance_options = {}, sec_decimal = nil)
45
+ parsed_s = Time.zone.parse(value).advance(advance_options).to_s(:db)
46
+ parsed_s << ('%.6f' % sec_decimal)[1..-1] if sec_decimal
47
+ parsed_s
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,88 @@
1
+ module WithFilters
2
+ module ValuePrep
3
+ # @private
4
+ class DefaultPrep
5
+ # @param [Array, Hash, String] value The value to filter on.
6
+ # @param [Hash] options
7
+ # @option options [Symbol] :match Determines the way the filter is matched.
8
+ # Accepts `:exact`, `:contains`, `:begins_with` and `:ends_with`.
9
+ #
10
+ # @since 0.1.0
11
+ def initialize(value, options = {})
12
+ @value = value
13
+ @options = options
14
+ end
15
+
16
+ # Returns the value so that it is ready to be filtered with.
17
+ #
18
+ # @return [Array, Hash, String]
19
+ #
20
+ # @since 0.1.0
21
+ def value
22
+ @prepared_value ||= if @value.is_a?(Hash)
23
+ {start: prepare_start_value(@value[:start]), stop: prepare_stop_value(@value[:stop])}
24
+ else
25
+ temp = Array.wrap(@value).map do |value|
26
+ add_match(prepare_value(value))
27
+ end
28
+ temp.length == 1 ? temp.first : temp
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # Prepares a value for use in the filter.
35
+ #
36
+ # @param [String] value The value to filter on.
37
+ #
38
+ # @return [String]
39
+ #
40
+ # @since 0.1.0
41
+ def prepare_value(value)
42
+ value.respond_to?(:strip) ? value.strip : value
43
+ end
44
+
45
+ # Prepares the start value for a ranged filter.
46
+ #
47
+ # @param [String] value The value to filter on.
48
+ #
49
+ # @return [String]
50
+ #
51
+ # @since 0.1.0
52
+ def prepare_start_value(value)
53
+ prepare_value(value)
54
+ end
55
+
56
+ # Prepares the stop value for a ranged filter.
57
+ #
58
+ # @param [String] value The value to filter on.
59
+ #
60
+ # @return [String]
61
+ #
62
+ # @since 0.1.0
63
+ def prepare_stop_value(value)
64
+ prepare_value(value)
65
+ end
66
+
67
+ # Add string matchers based on the `:match` option passed to #initialize.
68
+ #
69
+ # @param [String] value The value to filter on.
70
+ #
71
+ # @return [String]
72
+ #
73
+ # @since 0.1.0
74
+ def add_match(value)
75
+ case @options[:match]
76
+ when :contains
77
+ "%#{value}%"
78
+ when :begins_with
79
+ "#{value}%"
80
+ when :ends_with
81
+ "%#{value}"
82
+ else
83
+ value
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,28 @@
1
+ module WithFilters
2
+ # @private
3
+ module ValuePrep
4
+ # A mapping of Rails column types to value preparation class.
5
+ #
6
+ # since 0.1.0
7
+ TYPE_MAP = {
8
+ boolean: BooleanPrep,
9
+ date: DatePrep,
10
+ datetime: DateTimePrep,
11
+ timestamp: DateTimePrep
12
+ }
13
+
14
+ # A factory returning a class that prepares filter values based on a Rails
15
+ # column type.
16
+ #
17
+ # @param [Symbol] column_type A Rails column type.
18
+ # @param [String] value Value to be passed to the value preparation class.
19
+ # @param [Hash] options Options to be passed to the value preparation class.
20
+ #
21
+ # @return [WithFilters::DefaultPrep, Inherited from WithFilters::DefaultPrep]
22
+ #
23
+ # since 0.1.0
24
+ def self.prepare(column_type, value, options = {})
25
+ (TYPE_MAP[column_type].try(:new, value, options) || DefaultPrep.new(value, options)).value
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ module WithFilters
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,32 @@
1
+ require 'with_filters/engine'
2
+ require 'with_filters/hash_extraction'
3
+ require 'with_filters/active_record_extension'
4
+ require 'with_filters/active_record_model_extension'
5
+ require 'with_filters/value_prep/default_prep'
6
+ require 'with_filters/value_prep/boolean_prep'
7
+ require 'with_filters/value_prep/date_prep'
8
+ require 'with_filters/value_prep/date_time_prep'
9
+ require 'with_filters/value_prep/value_prep'
10
+ require 'with_filters/action_view_extension'
11
+ require 'with_filters/models/action'
12
+ require 'with_filters/models/filter/choice'
13
+ require 'with_filters/models/filter/collection'
14
+ require 'with_filters/models/filter/base'
15
+ require 'with_filters/models/filter/base_range'
16
+ require 'with_filters/models/filter/text'
17
+ require 'with_filters/models/filter/text_range'
18
+ require 'with_filters/models/filter/radio'
19
+ require 'with_filters/models/filter/select'
20
+ require 'with_filters/models/filter/select_range'
21
+ require 'with_filters/models/filter/check_box'
22
+ require 'with_filters/models/filter/filter'
23
+ require 'with_filters/models/filter_form'
24
+ require 'with_filters/version'
25
+
26
+ ActiveSupport.on_load(:active_record) do
27
+ include WithFilters::ActiveRecordExtension
28
+ end
29
+
30
+ ActiveSupport.on_load(:action_view) do
31
+ include WithFilters::ActionViewExtension
32
+ end