ransack_ffcrm 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/.gitignore +4 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +40 -0
  4. data/LICENSE +20 -0
  5. data/README.md +137 -0
  6. data/Rakefile +19 -0
  7. data/lib/ransack/adapters/active_record/3.0/compat.rb +166 -0
  8. data/lib/ransack/adapters/active_record/3.0/context.rb +161 -0
  9. data/lib/ransack/adapters/active_record/3.1/context.rb +166 -0
  10. data/lib/ransack/adapters/active_record/base.rb +33 -0
  11. data/lib/ransack/adapters/active_record/context.rb +41 -0
  12. data/lib/ransack/adapters/active_record.rb +12 -0
  13. data/lib/ransack/configuration.rb +35 -0
  14. data/lib/ransack/constants.rb +23 -0
  15. data/lib/ransack/context.rb +124 -0
  16. data/lib/ransack/helpers/form_builder.rb +203 -0
  17. data/lib/ransack/helpers/form_helper.rb +75 -0
  18. data/lib/ransack/helpers.rb +2 -0
  19. data/lib/ransack/locale/en.yml +70 -0
  20. data/lib/ransack/naming.rb +53 -0
  21. data/lib/ransack/nodes/attribute.rb +49 -0
  22. data/lib/ransack/nodes/bindable.rb +30 -0
  23. data/lib/ransack/nodes/condition.rb +212 -0
  24. data/lib/ransack/nodes/grouping.rb +183 -0
  25. data/lib/ransack/nodes/node.rb +34 -0
  26. data/lib/ransack/nodes/sort.rb +41 -0
  27. data/lib/ransack/nodes/value.rb +108 -0
  28. data/lib/ransack/nodes.rb +7 -0
  29. data/lib/ransack/predicate.rb +70 -0
  30. data/lib/ransack/ransacker.rb +24 -0
  31. data/lib/ransack/search.rb +123 -0
  32. data/lib/ransack/translate.rb +92 -0
  33. data/lib/ransack/version.rb +3 -0
  34. data/lib/ransack/visitor.rb +68 -0
  35. data/lib/ransack.rb +27 -0
  36. data/ransack_ffcrm.gemspec +30 -0
  37. data/spec/blueprints/articles.rb +5 -0
  38. data/spec/blueprints/comments.rb +5 -0
  39. data/spec/blueprints/notes.rb +3 -0
  40. data/spec/blueprints/people.rb +4 -0
  41. data/spec/blueprints/tags.rb +3 -0
  42. data/spec/console.rb +21 -0
  43. data/spec/helpers/ransack_helper.rb +2 -0
  44. data/spec/ransack/adapters/active_record/base_spec.rb +67 -0
  45. data/spec/ransack/adapters/active_record/context_spec.rb +45 -0
  46. data/spec/ransack/configuration_spec.rb +31 -0
  47. data/spec/ransack/helpers/form_builder_spec.rb +137 -0
  48. data/spec/ransack/helpers/form_helper_spec.rb +38 -0
  49. data/spec/ransack/nodes/condition_spec.rb +15 -0
  50. data/spec/ransack/nodes/grouping_spec.rb +13 -0
  51. data/spec/ransack/predicate_spec.rb +55 -0
  52. data/spec/ransack/search_spec.rb +225 -0
  53. data/spec/spec_helper.rb +47 -0
  54. data/spec/support/en.yml +5 -0
  55. data/spec/support/schema.rb +111 -0
  56. metadata +229 -0
@@ -0,0 +1,41 @@
1
+ module Ransack
2
+ module Nodes
3
+ class Sort < Node
4
+ include Bindable
5
+
6
+ attr_reader :name, :dir
7
+ i18n_word :asc, :desc
8
+
9
+ class << self
10
+ def extract(context, str)
11
+ attr, direction = str.split(/\s+/,2)
12
+ self.new(context).build(:name => attr, :dir => direction)
13
+ end
14
+ end
15
+
16
+ def build(params)
17
+ params.with_indifferent_access.each do |key, value|
18
+ if key.match(/^(name|dir)$/)
19
+ self.send("#{key}=", value)
20
+ end
21
+ end
22
+
23
+ self
24
+ end
25
+
26
+ def valid?
27
+ bound? && attr
28
+ end
29
+
30
+ def name=(name)
31
+ @name = name
32
+ context.bind(self, name) unless name.blank?
33
+ end
34
+
35
+ def dir=(dir)
36
+ @dir = %w(asc desc).include?(dir) ? dir : 'asc'
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,108 @@
1
+ module Ransack
2
+ module Nodes
3
+ class Value < Node
4
+ attr_accessor :value
5
+ delegate :present?, :blank?, :to => :value
6
+
7
+ def initialize(context, value = nil)
8
+ super(context)
9
+ @value = value
10
+ end
11
+
12
+ def persisted?
13
+ false
14
+ end
15
+
16
+ def eql?(other)
17
+ self.class == other.class &&
18
+ self.value == other.value
19
+ end
20
+ alias :== :eql?
21
+
22
+ def hash
23
+ value.hash
24
+ end
25
+
26
+ def cast(type)
27
+ case type
28
+ when :date
29
+ cast_to_date(value)
30
+ when :datetime, :timestamp, :time
31
+ cast_to_time(value)
32
+ when :boolean
33
+ cast_to_boolean(value)
34
+ when :integer
35
+ cast_to_integer(value)
36
+ when :float
37
+ cast_to_float(value)
38
+ when :decimal
39
+ cast_to_decimal(value)
40
+ else
41
+ cast_to_string(value)
42
+ end
43
+ end
44
+
45
+ def cast_to_date(val)
46
+ if val.is_a?(String)
47
+ Chronic.parse(val).in_time_zone.to_date rescue nil
48
+ elsif val.respond_to?(:to_date)
49
+ val.to_date rescue nil
50
+ else
51
+ y, m, d = *[val].flatten
52
+ m ||= 1
53
+ d ||= 1
54
+ Date.new(y,m,d) rescue nil
55
+ end
56
+ end
57
+
58
+ def cast_to_time(val)
59
+ if val.is_a?(Array)
60
+ Time.zone.local(*val) rescue nil
61
+ else
62
+ unless val.acts_like?(:time)
63
+ val = val.is_a?(String) ? Chronic.parse(val) : val.to_time rescue nil
64
+ end
65
+ val.in_time_zone rescue nil
66
+ end
67
+ end
68
+
69
+ def cast_to_boolean(val)
70
+ if Constants::TRUE_VALUES.include?(val)
71
+ true
72
+ elsif Constants::FALSE_VALUES.include?(val)
73
+ false
74
+ else
75
+ nil
76
+ end
77
+ end
78
+
79
+ def cast_to_string(val)
80
+ val.respond_to?(:to_s) ? val.to_s : String.new(val)
81
+ end
82
+
83
+ def cast_to_integer(val)
84
+ val.blank? ? nil : val.to_i
85
+ end
86
+
87
+ def cast_to_float(val)
88
+ val.blank? ? nil : val.to_f
89
+ end
90
+
91
+ def cast_to_decimal(val)
92
+ if val.blank?
93
+ nil
94
+ elsif val.class == BigDecimal
95
+ val
96
+ elsif val.respond_to?(:to_d)
97
+ val.to_d
98
+ else
99
+ val.to_s.to_d
100
+ end
101
+ end
102
+
103
+ def array_of_arrays?(val)
104
+ Array === val && Array === val.first
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,7 @@
1
+ require 'ransack/nodes/bindable'
2
+ require 'ransack/nodes/node'
3
+ require 'ransack/nodes/attribute'
4
+ require 'ransack/nodes/value'
5
+ require 'ransack/nodes/condition'
6
+ require 'ransack/nodes/sort'
7
+ require 'ransack/nodes/grouping'
@@ -0,0 +1,70 @@
1
+ module Ransack
2
+ class Predicate
3
+ attr_reader :name, :arel_predicate, :type, :formatter, :validator, :compound, :wants_array
4
+
5
+ class << self
6
+
7
+ def names
8
+ Ransack.predicates.keys
9
+ end
10
+
11
+ def names_by_decreasing_length
12
+ names.sort {|a,b| b.length <=> a.length}
13
+ end
14
+
15
+ def named(name)
16
+ Ransack.predicates[name.to_s]
17
+ end
18
+
19
+ def detect_and_strip_from_string!(str)
20
+ names_by_decreasing_length.detect {|p| str.sub!(/_#{p}$/, '')}
21
+ end
22
+
23
+ def detect_from_string(str)
24
+ names_by_decreasing_length.detect {|p| str.match(/_#{p}$/)}
25
+ end
26
+
27
+ def name_from_attribute_name(attribute_name)
28
+ names_by_decreasing_length.detect {|p| attribute_name.to_s.match(/_#{p}$/)}
29
+ end
30
+
31
+ def for_attribute_name(attribute_name)
32
+ self.named(detect_from_string(attribute_name.to_s))
33
+ end
34
+
35
+ end
36
+
37
+ def initialize(opts = {})
38
+ @name = opts[:name]
39
+ @arel_predicate = opts[:arel_predicate]
40
+ @type = opts[:type]
41
+ @formatter = opts[:formatter]
42
+ @validator = opts[:validator] || lambda { |v| v.respond_to?(:empty?) ? !v.empty? : !v.nil? }
43
+ @compound = opts[:compound]
44
+ @wants_array = @compound || ['in', 'not_in'].include?(@arel_predicate)
45
+ end
46
+
47
+ def eql?(other)
48
+ self.class == other.class &&
49
+ self.name == other.name
50
+ end
51
+ alias :== :eql?
52
+
53
+ def hash
54
+ name.hash
55
+ end
56
+
57
+ def format(val)
58
+ if formatter
59
+ formatter.call(val)
60
+ else
61
+ val
62
+ end
63
+ end
64
+
65
+ def validate(vals, type = @type)
66
+ vals.select {|v| validator.call(type ? v.cast(type) : v.value)}.any?
67
+ end
68
+
69
+ end
70
+ end
@@ -0,0 +1,24 @@
1
+ module Ransack
2
+ class Ransacker
3
+
4
+ attr_reader :name, :type, :formatter, :args
5
+
6
+ delegate :call, :to => :@callable
7
+
8
+ def initialize(klass, name, opts = {}, &block)
9
+ @klass, @name = klass, name
10
+
11
+ @type = opts[:type] || :string
12
+ @args = opts[:args] || [:parent]
13
+ @formatter = opts[:formatter]
14
+ @callable = opts[:callable] || block ||
15
+ (@klass.method(name) if @klass.respond_to?(name)) ||
16
+ proc {|parent| parent.table[name]}
17
+ end
18
+
19
+ def attr_from(bindable)
20
+ call(*args.map {|arg| bindable.send(arg)})
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,123 @@
1
+ require 'ransack/nodes'
2
+ require 'ransack/context'
3
+ require 'ransack/naming'
4
+
5
+ module Ransack
6
+ class Search
7
+ include Naming
8
+
9
+ attr_reader :base, :context
10
+
11
+ delegate :object, :klass, :to => :context
12
+ delegate :new_grouping, :new_condition,
13
+ :build_grouping, :build_condition,
14
+ :translate, :to => :base
15
+
16
+ def initialize(object, params = {}, options = {})
17
+ params ||= {}
18
+ @context = Context.for(object, options)
19
+ @context.auth_object = options[:auth_object]
20
+ @base = Nodes::Grouping.new(@context, 'and')
21
+ build(params.with_indifferent_access)
22
+ end
23
+
24
+ def result(opts = {})
25
+ @context.evaluate(self, opts)
26
+ end
27
+
28
+ def inspect
29
+ # Replace the inspect method on object's singleton class to call to_s() instead of to_a().
30
+ object.class_eval { alias :inspect :to_s }
31
+ super
32
+ end
33
+
34
+ def build(params)
35
+ collapse_multiparameter_attributes!(params).each do |key, value|
36
+ case key
37
+ when 's', 'sorts'
38
+ send("#{key}=", value)
39
+ else
40
+ base.send("#{key}=", value) if base.attribute_method?(key)
41
+ end
42
+ end
43
+ self
44
+ end
45
+
46
+ def sorts=(args)
47
+ case args
48
+ when Array
49
+ args.each do |sort|
50
+ sort = Nodes::Sort.extract(@context, sort)
51
+ self.sorts << sort
52
+ end
53
+ when Hash
54
+ args.each do |index, attrs|
55
+ sort = Nodes::Sort.new(@context).build(attrs)
56
+ self.sorts << sort
57
+ end
58
+ when String
59
+ self.sorts = [args]
60
+ else
61
+ raise ArgumentError, "Invalid argument (#{args.class}) supplied to sorts="
62
+ end
63
+ end
64
+ alias :s= :sorts=
65
+
66
+ def sorts
67
+ @sorts ||= []
68
+ end
69
+ alias :s :sorts
70
+
71
+ def build_sort(opts = {})
72
+ new_sort(opts).tap do |sort|
73
+ self.sorts << sort
74
+ end
75
+ end
76
+
77
+ def new_sort(opts = {})
78
+ Nodes::Sort.new(@context).build(opts)
79
+ end
80
+
81
+ def respond_to?(method_id)
82
+ super or begin
83
+ method_name = method_id.to_s
84
+ writer = method_name.sub!(/\=$/, '')
85
+ base.attribute_method?(method_name) ? true : false
86
+ end
87
+ end
88
+
89
+ def method_missing(method_id, *args)
90
+ method_name = method_id.to_s
91
+ writer = method_name.sub!(/\=$/, '')
92
+ if base.attribute_method?(method_name)
93
+ base.send(method_id, *args)
94
+ else
95
+ super
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def collapse_multiparameter_attributes!(attrs)
102
+ attrs.keys.each do |k|
103
+ if k.include?("(")
104
+ real_attribute, position = k.split(/\(|\)/)
105
+ cast = %w(a s i).include?(position.last) ? position.last : nil
106
+ position = position.to_i - 1
107
+ value = attrs.delete(k)
108
+ attrs[real_attribute] ||= []
109
+ attrs[real_attribute][position] = if cast
110
+ (value.blank? && cast == 'i') ? nil : value.send("to_#{cast}")
111
+ else
112
+ value
113
+ end
114
+ elsif Hash === attrs[k]
115
+ collapse_multiparameter_attributes!(attrs[k])
116
+ end
117
+ end
118
+
119
+ attrs
120
+ end
121
+
122
+ end
123
+ end
@@ -0,0 +1,92 @@
1
+ I18n.load_path += Dir[File.join(File.dirname(__FILE__), 'locale', '*.yml')]
2
+
3
+ module Ransack
4
+ module Translate
5
+ def self.word(key, options = {})
6
+ I18n.translate(:"ransack.#{key}", :default => key.to_s)
7
+ end
8
+
9
+ def self.predicate(key, options = {})
10
+ I18n.translate(:"ransack.predicates.#{key}", :default => key.to_s)
11
+ end
12
+
13
+ def self.attribute(key, options = {})
14
+ unless context = options.delete(:context)
15
+ raise ArgumentError, "A context is required to translate attributes"
16
+ end
17
+
18
+ original_name = key.to_s
19
+ base_class = context.klass
20
+ base_ancestors = base_class.ancestors.select { |x| x.respond_to?(:model_name) }
21
+ predicate = Predicate.detect_from_string(original_name)
22
+ attributes_str = original_name.sub(/_#{predicate}$/, '')
23
+ attribute_names = attributes_str.split(/_and_|_or_/)
24
+ combinator = attributes_str.match(/_and_/) ? :and : :or
25
+ defaults = base_ancestors.map do |klass|
26
+ :"ransack.attributes.#{klass.model_name.underscore}.#{original_name}"
27
+ end
28
+
29
+ translated_names = attribute_names.map do |attr|
30
+ attribute_name(context, attr, options[:include_associations])
31
+ end
32
+
33
+ interpolations = {}
34
+ interpolations[:attributes] = translated_names.join(" #{Translate.word(combinator)} ")
35
+
36
+ if predicate
37
+ defaults << "%{attributes} %{predicate}"
38
+ interpolations[:predicate] = Translate.predicate(predicate)
39
+ else
40
+ defaults << "%{attributes}"
41
+ end
42
+
43
+ defaults << options.delete(:default) if options[:default]
44
+ options.reverse_merge! :count => 1, :default => defaults
45
+ I18n.translate(defaults.shift, options.merge(interpolations))
46
+ end
47
+
48
+ def self.association(key, options = {})
49
+ unless context = options.delete(:context)
50
+ raise ArgumentError, "A context is required to translate associations"
51
+ end
52
+
53
+ defaults = key.blank? ? [:"#{context.klass.i18n_scope}.models.#{context.klass.model_name.underscore}"] : [:"ransack.associations.#{context.klass.model_name.underscore}.#{key}"]
54
+ defaults << context.traverse(key).model_name.human
55
+ options = {:count => 1, :default => defaults}
56
+ I18n.translate(defaults.shift, options)
57
+ end
58
+
59
+ private
60
+
61
+ def self.attribute_name(context, name, include_associations = nil)
62
+ assoc_path = context.association_path(name)
63
+ associated_class = context.traverse(assoc_path) if assoc_path.present?
64
+ attr_name = name.sub(/^#{assoc_path}_/, '')
65
+ interpolations = {}
66
+ interpolations[:attr_fallback_name] = I18n.translate(
67
+ (associated_class ?
68
+ :"ransack.attributes.#{associated_class.model_name.underscore}.#{attr_name}" :
69
+ :"ransack.attributes.#{context.klass.model_name.underscore}.#{attr_name}"
70
+ ),
71
+ :default => [
72
+ (associated_class ?
73
+ :"#{associated_class.i18n_scope}.attributes.#{associated_class.model_name.underscore}.#{attr_name}" :
74
+ :"#{context.klass.i18n_scope}.attributes.#{context.klass.model_name.underscore}.#{attr_name}"
75
+ ),
76
+ attr_name.humanize
77
+ ]
78
+ )
79
+ defaults = [
80
+ :"ransack.attributes.#{context.klass.model_name.underscore}.#{name}"
81
+ ]
82
+ if include_associations && associated_class
83
+ defaults << '%{association_name} %{attr_fallback_name}'
84
+ interpolations[:association_name] = association(assoc_path, :context => context)
85
+ else
86
+ defaults << '%{attr_fallback_name}'
87
+ end
88
+ options = {:count => 1, :default => defaults}
89
+ I18n.translate(defaults.shift, options.merge(interpolations))
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,3 @@
1
+ module Ransack
2
+ VERSION = "0.6.0"
3
+ end
@@ -0,0 +1,68 @@
1
+ module Ransack
2
+ class Visitor
3
+
4
+ def accept(object)
5
+ visit(object)
6
+ end
7
+
8
+ def can_accept?(object)
9
+ respond_to? DISPATCH[object.class]
10
+ end
11
+
12
+ def visit_Array(object)
13
+ object.map {|o| accept(o)}.compact
14
+ end
15
+
16
+ def visit_Ransack_Nodes_Condition(object)
17
+ object.arel_predicate if object.valid?
18
+ end
19
+
20
+ def visit_Ransack_Nodes_Grouping(object)
21
+ object.combinator == 'or' ? visit_or(object) : visit_and(object)
22
+ end
23
+
24
+ def visit_and(object)
25
+ nodes = object.values.map {|o| accept(o)}.compact
26
+ return nil unless nodes.size > 0
27
+
28
+ if nodes.size > 1
29
+ Arel::Nodes::Grouping.new(Arel::Nodes::And.new(nodes))
30
+ else
31
+ nodes.first
32
+ end
33
+ end
34
+
35
+ def visit_or(object)
36
+ nodes = object.values.map {|o| accept(o)}.compact
37
+ return nil unless nodes.size > 0
38
+
39
+ if nodes.size > 1
40
+ nodes.inject(&:or)
41
+ else
42
+ nodes.first
43
+ end
44
+ end
45
+
46
+ def visit_Ransack_Nodes_Sort(object)
47
+ object.attr.send(object.dir) if object.valid?
48
+ end
49
+
50
+ def quoted?(object)
51
+ case object
52
+ when Arel::Nodes::SqlLiteral, Bignum, Fixnum
53
+ false
54
+ else
55
+ true
56
+ end
57
+ end
58
+
59
+ def visit(object)
60
+ send(DISPATCH[object.class], object)
61
+ end
62
+
63
+ DISPATCH = Hash.new do |hash, klass|
64
+ hash[klass] = "visit_#{klass.name.gsub('::', '_')}"
65
+ end
66
+
67
+ end
68
+ end
data/lib/ransack.rb ADDED
@@ -0,0 +1,27 @@
1
+ require 'ransack/configuration'
2
+
3
+ module Ransack
4
+ extend Configuration
5
+
6
+ class UntraversableAssociationError < StandardError; end;
7
+ end
8
+
9
+ Ransack.configure do |config|
10
+ Ransack::Constants::AREL_PREDICATES.each do |name|
11
+ config.add_predicate name, :arel_predicate => name
12
+ end
13
+
14
+ Ransack::Constants::DERIVED_PREDICATES.each do |args|
15
+ config.add_predicate *args
16
+ end
17
+ end
18
+
19
+ require 'ransack/translate'
20
+ require 'ransack/search'
21
+ require 'ransack/ransacker'
22
+ require 'ransack/adapters/active_record'
23
+ require 'ransack/helpers'
24
+ require 'action_controller'
25
+ require 'chronic'
26
+
27
+ ActionController::Base.helper Ransack::Helpers::FormHelper
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "ransack/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "ransack_ffcrm"
7
+ s.version = Ransack::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Ernie Miller"]
10
+ s.email = ["ernie@metautonomo.us"]
11
+ s.homepage = "http://metautonomo.us/projects/ransack"
12
+ s.summary = %q{Object-based searching for ActiveRecord (currently).}
13
+ s.description = %q{Ransack is the successor to the MetaSearch gem. It improves and expands upon MetaSearch's functionality, but does not have a 100%-compatible API.}
14
+
15
+ s.rubyforge_project = "ransack"
16
+
17
+ s.add_dependency 'activerecord', '~> 3.0'
18
+ s.add_dependency 'actionpack', '~> 3.0'
19
+ s.add_dependency 'polyamorous', '~> 0.5.0'
20
+ s.add_dependency 'chronic', '~> 0.6.7'
21
+ s.add_development_dependency 'rspec', '~> 2.8.0'
22
+ s.add_development_dependency 'machinist', '~> 1.0.6'
23
+ s.add_development_dependency 'faker', '~> 0.9.5'
24
+ s.add_development_dependency 'sqlite3', '~> 1.3.3'
25
+
26
+ s.files = `git ls-files`.split("\n")
27
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
28
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
29
+ s.require_paths = ["lib"]
30
+ end
@@ -0,0 +1,5 @@
1
+ Article.blueprint do
2
+ person
3
+ title
4
+ body
5
+ end
@@ -0,0 +1,5 @@
1
+ Comment.blueprint do
2
+ article
3
+ person
4
+ body
5
+ end
@@ -0,0 +1,3 @@
1
+ Note.blueprint do
2
+ note
3
+ end
@@ -0,0 +1,4 @@
1
+ Person.blueprint do
2
+ name
3
+ salary
4
+ end
@@ -0,0 +1,3 @@
1
+ Tag.blueprint do
2
+ name { Sham.tag_name }
3
+ end
data/spec/console.rb ADDED
@@ -0,0 +1,21 @@
1
+ Bundler.setup
2
+ require 'machinist/active_record'
3
+ require 'sham'
4
+ require 'faker'
5
+ require 'ransack'
6
+
7
+ Dir[File.expand_path('../../spec/{helpers,support,blueprints}/*.rb', __FILE__)].each do |f|
8
+ require f
9
+ end
10
+
11
+ Sham.define do
12
+ name { Faker::Name.name }
13
+ title { Faker::Lorem.sentence }
14
+ body { Faker::Lorem.paragraph }
15
+ salary {|index| 30000 + (index * 1000)}
16
+ tag_name { Faker::Lorem.words(3).join(' ') }
17
+ note { Faker::Lorem.words(7).join(' ') }
18
+ end
19
+
20
+ Schema.create
21
+
@@ -0,0 +1,2 @@
1
+ module RansackHelper
2
+ end