newellista-searchlogic 2.0.2

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.
@@ -0,0 +1,53 @@
1
+ module Searchlogic
2
+ module NamedScopes
3
+ # Handles dynamically creating named scopes for orderin by columns.
4
+ module Ordering
5
+ def condition?(name) # :nodoc:
6
+ super || order_condition?(name)
7
+ end
8
+
9
+ def primary_condition_name(name) # :nodoc
10
+ if result = super
11
+ result
12
+ elsif order_condition?(name)
13
+ name.to_sym
14
+ else
15
+ nil
16
+ end
17
+ end
18
+
19
+ def order_condition?(name) # :nodoc:
20
+ !order_condition_details(name).nil?
21
+ end
22
+
23
+ private
24
+ def method_missing(name, *args, &block)
25
+ if name == :order
26
+ named_scope name, lambda { |scope_name|
27
+ return {} if !order_condition?(scope_name)
28
+ send(scope_name).proxy_options
29
+ }
30
+ send(name, *args)
31
+ elsif details = order_condition_details(name)
32
+ create_order_conditions(details[:column])
33
+ send(name, *args)
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def order_condition_details(name)
40
+ if name.to_s =~ /^(ascend|descend)_by_(\w+)$/
41
+ {:order_as => $1, :column => $2}
42
+ elsif name.to_s =~ /^order$/
43
+ {}
44
+ end
45
+ end
46
+
47
+ def create_order_conditions(column)
48
+ named_scope("ascend_by_#{column}".to_sym, {:order => "#{table_name}.#{column} ASC"})
49
+ named_scope("descend_by_#{column}".to_sym, {:order => "#{table_name}.#{column} DESC"})
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,63 @@
1
+ module Searchlogic
2
+ module RailsHelpers
3
+ # Creates a link that alternates between acending and descending. It basically
4
+ # alternates between calling 2 named scopes: "ascend_by_*" and "descend_by_*"
5
+ #
6
+ # By default Searchlogic gives you these named scopes for all of your columns, but
7
+ # if you wanted to create your own, it will work with those too.
8
+ #
9
+ # This helper accepts the following options:
10
+ #
11
+ # * <tt>:by</tt> - the name of the named scope. This helper will prepend this value with "ascend_by_" and "descend_by_"
12
+ # * <tt>:as</tt> - the text used in the link, defaults to whatever is passed to :by
13
+ # * <tt>:ascend_scope</tt> - what scope to call for ascending the data, defaults to "ascend_by_:by"
14
+ # * <tt>:descend_scope</tt> - what scope to call for descending the data, defaults to "descend_by_:by"
15
+ # * <tt>:params_scope</tt> - the name of the params key to scope the order condition by, defaults to :search
16
+ def order(search, options = {}, html_options = {})
17
+ options[:params_scope] ||= :search
18
+ options[:as] ||= options[:by].to_s.humanize
19
+ options[:ascend_scope] ||= "ascend_by_#{options[:by]}"
20
+ options[:descend_scope] ||= "descend_by_#{options[:by]}"
21
+ ascending = search.order.to_s == options[:ascend_scope]
22
+ new_scope = ascending ? options[:descend_scope] : options[:ascend_scope]
23
+ selected = [options[:ascend_scope], options[:descend_scope]].include?(search.order.to_s)
24
+ if selected
25
+ css_classes = html_options[:class] ? html_options[:class].split(" ") : []
26
+ if ascending
27
+ options[:as] = "&#9650;&nbsp;#{options[:as]}"
28
+ css_classes << "ascending"
29
+ else
30
+ options[:as] = "&#9660;&nbsp;#{options[:as]}"
31
+ css_classes << "descending"
32
+ end
33
+ html_options[:class] = css_classes.join(" ")
34
+ end
35
+ link_to options[:as], url_for(options[:params_scope] => search[:conditions].merge( { :order => new_scope } ) ), html_options
36
+ end
37
+
38
+ # Automatically makes the form method :get if a Searchlogic::Search and sets
39
+ # the params scope to :search
40
+ def form_for(*args, &block)
41
+ if search_obj = args.find { |arg| arg.is_a?(Searchlogic::Search) }
42
+ options = args.extract_options!
43
+ options[:html] ||= {}
44
+ options[:html][:method] ||= :get
45
+ args.unshift(:search) if args.first == search_obj
46
+ args << options
47
+ end
48
+ super
49
+ end
50
+
51
+ # Automatically adds an "order" hidden field in your form to preserve how the data
52
+ # is being ordered.
53
+ def fields_for(*args, &block)
54
+ if search_obj = args.find { |arg| arg.is_a?(Searchlogic::Search) }
55
+ args.unshift(:search) if args.first == search_obj
56
+ concat(hidden_field_tag("#{args.first}[order]", search_obj.order) + "\n")
57
+ super
58
+ else
59
+ super
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,146 @@
1
+ module Searchlogic
2
+ # A class that acts like a model, creates attr_accessors for named_scopes, and then
3
+ # chains together everything when an "action" method is called. It basically makes
4
+ # implementing search forms in your application effortless:
5
+ #
6
+ # search = User.search
7
+ # search.username_like = "bjohnson"
8
+ # search.all
9
+ #
10
+ # Is equivalent to:
11
+ #
12
+ # User.search(:username_like => "bjohnson").all
13
+ #
14
+ # Is equivalent to:
15
+ #
16
+ # User.username_like("bjohnson").all
17
+ class Search
18
+ # Responsible for adding a "search" method into your models.
19
+ module Implementation
20
+ # Returns a new Search object for the given model.
21
+ def search(conditions = {})
22
+ Search.new(self, scope(:find), conditions)
23
+ end
24
+ end
25
+
26
+ # Is an invalid condition is used this error will be raised. Ex:
27
+ #
28
+ # User.search(:unkown => true)
29
+ #
30
+ # Where unknown is not a valid named scope for the User model.
31
+ class UnknownConditionError < StandardError
32
+ def initialize(condition)
33
+ msg = "The #{condition} is not a valid condition. You may only use conditions that map to a named scope"
34
+ super(msg)
35
+ end
36
+ end
37
+
38
+ attr_accessor :klass, :current_scope, :conditions
39
+
40
+ # Creates a new search object for the given class. Ex:
41
+ #
42
+ # Searchlogic::Search.new(User, {}, {:username_like => "bjohnson"})
43
+ def initialize(klass, current_scope, conditions = {})
44
+ self.klass = klass
45
+ self.current_scope = current_scope
46
+ self.conditions = conditions if conditions.is_a?(Hash)
47
+ end
48
+
49
+ def clone
50
+ self.class.new(klass, current_scope.clone, conditions.clone)
51
+ end
52
+
53
+ # Returns a hash of the current conditions set.
54
+ def conditions
55
+ @conditions ||= {}
56
+ end
57
+
58
+ # Accepts a hash of conditions.
59
+ def conditions=(values)
60
+ values.each do |condition, value|
61
+ value.delete_if { |v| v.blank? } if value.is_a?(Array)
62
+ next if value.blank?
63
+ send("#{condition}=", value)
64
+ end
65
+ end
66
+
67
+ # Delete a condition from the search. Since conditions map to named scopes,
68
+ # if a named scope accepts a parameter there is no way to actually delete
69
+ # the scope if you do not want it anymore. A nil value might be meaningful
70
+ # to that scope.
71
+ def delete(*names)
72
+ names.each { |name| @conditions.delete(name.to_sym) }
73
+ self
74
+ end
75
+
76
+ private
77
+ def method_missing(name, *args, &block)
78
+ if name.to_s =~ /(\w+)=$/
79
+ condition = $1.to_sym
80
+ scope_name = normalize_scope_name($1)
81
+ if scope?(scope_name)
82
+ conditions[condition] = type_cast(args.first, cast_type(scope_name))
83
+ else
84
+ raise UnknownConditionError.new(name)
85
+ end
86
+ elsif scope?(normalize_scope_name(name))
87
+ if args.size > 0
88
+ send("#{name}=", *args)
89
+ self
90
+ else
91
+ conditions[name]
92
+ end
93
+ else
94
+ scope = conditions.inject(klass.scoped(current_scope)) do |scope, condition|
95
+ scope_name, value = condition
96
+ scope_name = normalize_scope_name(scope_name)
97
+ klass.send(scope_name, value) if !klass.respond_to?(scope_name)
98
+ arity = klass.named_scope_arity(scope_name)
99
+
100
+ if !arity || arity == 0
101
+ if value == true
102
+ scope.send(scope_name)
103
+ else
104
+ scope
105
+ end
106
+ else
107
+ scope.send(scope_name, value)
108
+ end
109
+ end
110
+ scope.send(name, *args, &block)
111
+ end
112
+ end
113
+
114
+ def normalize_scope_name(scope_name)
115
+ klass.column_names.include?(scope_name.to_s) ? "#{scope_name}_equals".to_sym : scope_name.to_sym
116
+ end
117
+
118
+ def scope?(scope_name)
119
+ klass.scopes.key?(scope_name) || klass.condition?(scope_name)
120
+ end
121
+
122
+ def cast_type(name)
123
+ klass.send(name, nil) if !klass.respond_to?(name) # We need to set up the named scope if it doesn't exist, so we can get a value for named_ssope_options
124
+ named_scope_options = klass.named_scope_options(name)
125
+ arity = klass.named_scope_arity(name)
126
+ if !arity || arity == 0
127
+ :boolean
128
+ else
129
+ named_scope_options.respond_to?(:searchlogic_arg_type) ? named_scope_options.searchlogic_arg_type : :string
130
+ end
131
+ end
132
+
133
+ def type_cast(value, type)
134
+ case value
135
+ when Array
136
+ value.collect { |v| type_cast(v, type) }
137
+ else
138
+ # Let's leverage ActiveRecord's type casting, so that casting is consistent
139
+ # with the other models.
140
+ column_for_type_cast = ActiveRecord::ConnectionAdapters::Column.new("", nil)
141
+ column_for_type_cast.instance_variable_set(:@type, type)
142
+ column_for_type_cast.type_cast(value)
143
+ end
144
+ end
145
+ end
146
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "searchlogic"
@@ -0,0 +1,65 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{searchlogic}
5
+ s.version = "2.0.2"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Ben Johnson of Binary Logic"]
9
+ s.date = %q{2009-06-20}
10
+ s.email = %q{bjohnson@binarylogic.com}
11
+ s.extra_rdoc_files = [
12
+ "LICENSE",
13
+ "README.rdoc"
14
+ ]
15
+ s.files = [
16
+ ".gitignore",
17
+ "CHANGELOG.rdoc",
18
+ "LICENSE",
19
+ "README.rdoc",
20
+ "Rakefile",
21
+ "VERSION.yml",
22
+ "init.rb",
23
+ "lib/searchlogic.rb",
24
+ "lib/searchlogic/core_ext/object.rb",
25
+ "lib/searchlogic/core_ext/proc.rb",
26
+ "lib/searchlogic/named_scopes/associations.rb",
27
+ "lib/searchlogic/named_scopes/conditions.rb",
28
+ "lib/searchlogic/named_scopes/ordering.rb",
29
+ "lib/searchlogic/rails_helpers.rb",
30
+ "lib/searchlogic/search.rb",
31
+ "rails/init.rb",
32
+ "searchlogic.gemspec",
33
+ "spec/core_ext/object_spec.rb",
34
+ "spec/core_ext/proc_spec.rb",
35
+ "spec/named_scopes/associations_spec.rb",
36
+ "spec/named_scopes/conditions_spec.rb",
37
+ "spec/named_scopes/ordering_spec.rb",
38
+ "spec/search_spec.rb",
39
+ "spec/spec_helper.rb"
40
+ ]
41
+ s.homepage = %q{http://github.com/newellista/searchlogic}
42
+ s.rdoc_options = ["--charset=UTF-8"]
43
+ s.require_paths = ["lib"]
44
+ s.rubygems_version = %q{1.3.4}
45
+ s.summary = %q{Searchlogic provides common named scopes and object based searching for ActiveRecord.}
46
+ s.test_files = [
47
+ "spec/core_ext/object_spec.rb",
48
+ "spec/core_ext/proc_spec.rb",
49
+ "spec/named_scopes/associations_spec.rb",
50
+ "spec/named_scopes/conditions_spec.rb",
51
+ "spec/named_scopes/ordering_spec.rb",
52
+ "spec/search_spec.rb",
53
+ "spec/spec_helper.rb"
54
+ ]
55
+
56
+ if s.respond_to? :specification_version then
57
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
58
+ s.specification_version = 3
59
+
60
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
61
+ else
62
+ end
63
+ else
64
+ end
65
+ end
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ describe "Object" do
4
+ it "should accept and pass the argument to the searchlogic_arg_type" do
5
+ searchlogic_lambda(:integer) {}.searchlogic_arg_type.should == :integer
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ describe "Proc" do
4
+ it "should have a searchlogic_arg_type accessor" do
5
+ p = Proc.new {}
6
+ p.searchlogic_arg_type = :integer
7
+ p.searchlogic_arg_type.should == :integer
8
+ end
9
+ end
@@ -0,0 +1,125 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ describe "Associations" do
4
+ before(:each) do
5
+ @users_join_sql = ["INNER JOIN \"users\" ON users.company_id = companies.id"]
6
+ @orders_join_sql = ["INNER JOIN \"users\" ON users.company_id = companies.id", "INNER JOIN \"orders\" ON orders.user_id = users.id"]
7
+ end
8
+
9
+ it "should create a named scope" do
10
+ Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => :users)
11
+ end
12
+
13
+ it "should create a deep named scope" do
14
+ Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => {:users => :orders})
15
+ end
16
+
17
+ it "should not allowed named scopes on non existent association columns" do
18
+ lambda { User.users_whatever_like("bjohnson") }.should raise_error(NoMethodError)
19
+ end
20
+
21
+ it "should not allowed named scopes on non existent deep association columns" do
22
+ lambda { User.users_orders_whatever_like("bjohnson") }.should raise_error(NoMethodError)
23
+ end
24
+
25
+ it "should allow named scopes to be called multiple times and reflect the value passed" do
26
+ Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => :users)
27
+ Company.users_username_like("thunt").proxy_options.should == User.username_like("thunt").proxy_options.merge(:joins => :users)
28
+ end
29
+
30
+ it "should allow deep named scopes to be called multiple times and reflect the value passed" do
31
+ Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => {:users => :orders})
32
+ Company.users_orders_total_greater_than(20).proxy_options.should == Order.total_greater_than(20).proxy_options.merge(:joins => {:users => :orders})
33
+ end
34
+
35
+ it "should have an arity of 1 if the underlying scope has an arity of 1" do
36
+ Company.users_orders_total_greater_than(10)
37
+ Company.named_scope_arity("users_orders_total_greater_than").should == Order.named_scope_arity("total_greater_than")
38
+ end
39
+
40
+ it "should have an arity of nil if the underlying scope has an arity of nil" do
41
+ Company.users_orders_total_null
42
+ Company.named_scope_arity("users_orders_total_null").should == Order.named_scope_arity("total_null")
43
+ end
44
+
45
+ it "should have an arity of -1 if the underlying scope has an arity of -1" do
46
+ Company.users_id_equals_any
47
+ Company.named_scope_arity("users_id_equals_any").should == User.named_scope_arity("id_equals_any")
48
+ end
49
+
50
+ it "should allow aliases" do
51
+ Company.users_username_contains("bjohnson").proxy_options.should == User.username_contains("bjohnson").proxy_options.merge(:joins => :users)
52
+ end
53
+
54
+ it "should allow deep aliases" do
55
+ Company.users_orders_total_gt(10).proxy_options.should == Order.total_gt(10).proxy_options.merge(:joins => {:users => :orders})
56
+ end
57
+
58
+ it "should allow ascending" do
59
+ Company.ascend_by_users_username.proxy_options.should == User.ascend_by_username.proxy_options.merge(:joins => :users)
60
+ end
61
+
62
+ it "should allow descending" do
63
+ Company.descend_by_users_username.proxy_options.should == User.descend_by_username.proxy_options.merge(:joins => :users)
64
+ end
65
+
66
+ it "should allow deep ascending" do
67
+ Company.ascend_by_users_orders_total.proxy_options.should == Order.ascend_by_total.proxy_options.merge(:joins => {:users => :orders})
68
+ end
69
+
70
+ it "should allow deep descending" do
71
+ Company.descend_by_users_orders_total.proxy_options.should == Order.descend_by_total.proxy_options.merge(:joins => {:users => :orders})
72
+ end
73
+
74
+ it "should include optional associations" do
75
+ pending # this is a problem with using inner joins and left outer joins
76
+ Company.create
77
+ company = Company.create
78
+ user = company.users.create
79
+ order = user.orders.create(:total => 20, :taxes => 3)
80
+ Company.ascend_by_users_orders_total.all.should == Company.all
81
+ end
82
+
83
+ it "should not create the same join twice" do
84
+ company = Company.create
85
+ user = company.users.create
86
+ order = user.orders.create(:total => 20, :taxes => 3)
87
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all.should == Company.all
88
+ end
89
+
90
+ it "should not create the same join twice when traveling through the duplicate join" do
91
+ Company.users_username_like("bjohnson").users_orders_total_gt(100).all.should == Company.all
92
+ end
93
+
94
+ it "should not create the same join twice when traveling through the duplicate join 2" do
95
+ Company.users_orders_total_gt(100).users_orders_line_items_price_gt(20).all.should == Company.all
96
+ end
97
+
98
+ it "should allow the use of :include when a join was created" do
99
+ company = Company.create
100
+ user = company.users.create
101
+ order = user.orders.create(:total => 20, :taxes => 3)
102
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => :users).should == Company.all
103
+ end
104
+
105
+ it "should allow the use of deep :include when a join was created" do
106
+ company = Company.create
107
+ user = company.users.create
108
+ order = user.orders.create(:total => 20, :taxes => 3)
109
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => {:users => :orders}).should == Company.all
110
+ end
111
+
112
+ it "should allow the use of :include when traveling through the duplicate join" do
113
+ company = Company.create
114
+ user = company.users.create(:username => "bjohnson")
115
+ order = user.orders.create(:total => 20, :taxes => 3)
116
+ Company.users_username_like("bjohnson").users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => :users).should == Company.all
117
+ end
118
+
119
+ it "should allow the use of deep :include when traveling through the duplicate join" do
120
+ company = Company.create
121
+ user = company.users.create(:username => "bjohnson")
122
+ order = user.orders.create(:total => 20, :taxes => 3)
123
+ Company.users_orders_taxes_lt(50).ascend_by_users_orders_total.all(:include => {:users => :orders}).should == Company.all
124
+ end
125
+ end