joost-searchlogic 2.1.5.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,69 @@
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
+ # Examples:
10
+ #
11
+ # order @search, :by => :username
12
+ # order @search, :by => :created_at, :as => "Created"
13
+ #
14
+ # This helper accepts the following options:
15
+ #
16
+ # * <tt>:by</tt> - the name of the named scope. This helper will prepend this value with "ascend_by_" and "descend_by_"
17
+ # * <tt>:as</tt> - the text used in the link, defaults to whatever is passed to :by
18
+ # * <tt>:ascend_scope</tt> - what scope to call for ascending the data, defaults to "ascend_by_:by"
19
+ # * <tt>:descend_scope</tt> - what scope to call for descending the data, defaults to "descend_by_:by"
20
+ # * <tt>:params_scope</tt> - the name of the params key to scope the order condition by, defaults to :search
21
+ def order(search, options = {}, html_options = {})
22
+ options[:params_scope] ||= :search
23
+ options[:as] ||= options[:by].to_s.humanize
24
+ options[:ascend_scope] ||= "ascend_by_#{options[:by]}"
25
+ options[:descend_scope] ||= "descend_by_#{options[:by]}"
26
+ ascending = search.order.to_s == options[:ascend_scope]
27
+ new_scope = ascending ? options[:descend_scope] : options[:ascend_scope]
28
+ selected = [options[:ascend_scope], options[:descend_scope]].include?(search.order.to_s)
29
+ if selected
30
+ css_classes = html_options[:class] ? html_options[:class].split(" ") : []
31
+ if ascending
32
+ options[:as] = "&#9650;&nbsp;#{options[:as]}"
33
+ css_classes << "ascending"
34
+ else
35
+ options[:as] = "&#9660;&nbsp;#{options[:as]}"
36
+ css_classes << "descending"
37
+ end
38
+ html_options[:class] = css_classes.join(" ")
39
+ end
40
+ link_to options[:as], url_for(options[:params_scope] => search.conditions.merge( { :order => new_scope } ) ), html_options
41
+ end
42
+
43
+ # Automatically makes the form method :get if a Searchlogic::Search and sets
44
+ # the params scope to :search
45
+ def form_for(*args, &block)
46
+ if search_obj = args.find { |arg| arg.is_a?(Searchlogic::Search) }
47
+ options = args.extract_options!
48
+ options[:html] ||= {}
49
+ options[:html][:method] ||= :get
50
+ options[:url] ||= url_for
51
+ args.unshift(:search) if args.first == search_obj
52
+ args << options
53
+ end
54
+ super
55
+ end
56
+
57
+ # Automatically adds an "order" hidden field in your form to preserve how the data
58
+ # is being ordered.
59
+ def fields_for(*args, &block)
60
+ if search_obj = args.find { |arg| arg.is_a?(Searchlogic::Search) }
61
+ args.unshift(:search) if args.first == search_obj
62
+ concat(content_tag("div", hidden_field_tag("#{args.first}[order]", search_obj.order)) + "\n")
63
+ super
64
+ else
65
+ super
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,150 @@
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
+ # Additional method, gets aliases as "search" if that method
21
+ # is available. A lot of other libraries like to use "search"
22
+ # as well, so if you have a conflict like this, you can use
23
+ # this method directly.
24
+ def searchlogic(conditions = {})
25
+ Search.new(self, scope(:find), conditions)
26
+ end
27
+ end
28
+
29
+ # Is an invalid condition is used this error will be raised. Ex:
30
+ #
31
+ # User.search(:unkown => true)
32
+ #
33
+ # Where unknown is not a valid named scope for the User model.
34
+ class UnknownConditionError < StandardError
35
+ def initialize(condition)
36
+ msg = "The #{condition} is not a valid condition. You may only use conditions that map to a named scope"
37
+ super(msg)
38
+ end
39
+ end
40
+
41
+ attr_accessor :klass, :current_scope, :conditions
42
+ undef :id if respond_to?(:id)
43
+
44
+ # Creates a new search object for the given class. Ex:
45
+ #
46
+ # Searchlogic::Search.new(User, {}, {:username_like => "bjohnson"})
47
+ def initialize(klass, current_scope, conditions = {})
48
+ self.klass = klass
49
+ self.current_scope = current_scope
50
+ self.conditions = conditions if conditions.is_a?(Hash)
51
+ end
52
+
53
+ def clone
54
+ self.class.new(klass, current_scope.clone, conditions.clone)
55
+ end
56
+
57
+ # Returns a hash of the current conditions set.
58
+ def conditions
59
+ @conditions ||= {}
60
+ end
61
+
62
+ # Accepts a hash of conditions.
63
+ def conditions=(values)
64
+ values.each do |condition, value|
65
+ value.delete_if { |v| v.blank? } if value.is_a?(Array)
66
+ next if value.blank?
67
+ send("#{condition}=", value)
68
+ end
69
+ end
70
+
71
+ # Delete a condition from the search. Since conditions map to named scopes,
72
+ # if a named scope accepts a parameter there is no way to actually delete
73
+ # the scope if you do not want it anymore. A nil value might be meaningful
74
+ # to that scope.
75
+ def delete(*names)
76
+ names.each { |name| @conditions.delete(name.to_sym) }
77
+ self
78
+ end
79
+
80
+ private
81
+ def method_missing(name, *args, &block)
82
+ if name.to_s =~ /(\w+)=$/
83
+ condition = $1.to_sym
84
+ scope_name = normalize_scope_name($1)
85
+ if scope?(scope_name)
86
+ conditions[condition] = type_cast(args.first, cast_type(scope_name))
87
+ else
88
+ raise UnknownConditionError.new(name)
89
+ end
90
+ elsif scope?(normalize_scope_name(name))
91
+ if args.size > 0
92
+ send("#{name}=", *args)
93
+ self
94
+ else
95
+ conditions[name]
96
+ end
97
+ else
98
+ scope = conditions.inject(klass.scoped(current_scope)) do |scope, condition|
99
+ scope_name, value = condition
100
+ scope_name = normalize_scope_name(scope_name)
101
+ klass.send(scope_name, value) if !klass.respond_to?(scope_name)
102
+ arity = klass.named_scope_arity(scope_name)
103
+
104
+ if !arity || arity == 0
105
+ if value == true
106
+ scope.send(scope_name)
107
+ else
108
+ scope
109
+ end
110
+ else
111
+ scope.send(scope_name, value)
112
+ end
113
+ end
114
+ scope.send(name, *args, &block)
115
+ end
116
+ end
117
+
118
+ def normalize_scope_name(scope_name)
119
+ klass.column_names.include?(scope_name.to_s) ? "#{scope_name}_equals".to_sym : scope_name.to_sym
120
+ end
121
+
122
+ def scope?(scope_name)
123
+ klass.scopes.key?(scope_name) || klass.condition?(scope_name)
124
+ end
125
+
126
+ def cast_type(name)
127
+ 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
128
+ named_scope_options = klass.named_scope_options(name)
129
+ arity = klass.named_scope_arity(name)
130
+ if !arity || arity == 0
131
+ :boolean
132
+ else
133
+ named_scope_options.respond_to?(:searchlogic_arg_type) ? named_scope_options.searchlogic_arg_type : :string
134
+ end
135
+ end
136
+
137
+ def type_cast(value, type)
138
+ case value
139
+ when Array
140
+ value.collect { |v| type_cast(v, type) }
141
+ else
142
+ # Let's leverage ActiveRecord's type casting, so that casting is consistent
143
+ # with the other models.
144
+ column_for_type_cast = ActiveRecord::ConnectionAdapters::Column.new("", nil)
145
+ column_for_type_cast.instance_variable_set(:@type, type)
146
+ column_for_type_cast.type_cast(value)
147
+ end
148
+ end
149
+ end
150
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "searchlogic"
@@ -0,0 +1,73 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{searchlogic}
5
+ s.version = "2.1.5.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-07-12}
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/active_record_consistency.rb",
25
+ "lib/searchlogic/core_ext/object.rb",
26
+ "lib/searchlogic/core_ext/proc.rb",
27
+ "lib/searchlogic/named_scopes/alias_scope.rb",
28
+ "lib/searchlogic/named_scopes/associations.rb",
29
+ "lib/searchlogic/named_scopes/conditions.rb",
30
+ "lib/searchlogic/named_scopes/ordering.rb",
31
+ "lib/searchlogic/rails_helpers.rb",
32
+ "lib/searchlogic/search.rb",
33
+ "rails/init.rb",
34
+ "searchlogic.gemspec",
35
+ "spec/core_ext/object_spec.rb",
36
+ "spec/core_ext/proc_spec.rb",
37
+ "spec/named_scopes/alias_scope_spec.rb",
38
+ "spec/named_scopes/associations_spec.rb",
39
+ "spec/named_scopes/conditions_spec.rb",
40
+ "spec/named_scopes/ordering_spec.rb",
41
+ "spec/search_spec.rb",
42
+ "spec/spec_helper.rb"
43
+ ]
44
+ s.homepage = %q{http://github.com/binarylogic/searchlogic}
45
+ s.rdoc_options = ["--charset=UTF-8"]
46
+ s.require_paths = ["lib"]
47
+ s.rubyforge_project = %q{searchlogic}
48
+ s.rubygems_version = %q{1.3.4}
49
+ s.summary = %q{Searchlogic provides common named scopes and object based searching for ActiveRecord.}
50
+ s.test_files = [
51
+ "spec/core_ext/object_spec.rb",
52
+ "spec/core_ext/proc_spec.rb",
53
+ "spec/named_scopes/alias_scope_spec.rb",
54
+ "spec/named_scopes/associations_spec.rb",
55
+ "spec/named_scopes/conditions_spec.rb",
56
+ "spec/named_scopes/ordering_spec.rb",
57
+ "spec/search_spec.rb",
58
+ "spec/spec_helper.rb"
59
+ ]
60
+
61
+ if s.respond_to? :specification_version then
62
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
63
+ s.specification_version = 3
64
+
65
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
66
+ s.add_runtime_dependency(%q<activerecord>, [">= 2.0.0"])
67
+ else
68
+ s.add_dependency(%q<activerecord>, [">= 2.0.0"])
69
+ end
70
+ else
71
+ s.add_dependency(%q<activerecord>, [">= 2.0.0"])
72
+ end
73
+ 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,15 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ describe "AliasScope" do
4
+ it "should allow alias scopes" do
5
+ User.create(:username => "bjohnson")
6
+ User.create(:username => "thunt")
7
+ User.username_has("bjohnson").all.should == User.find_all_by_username("bjohnson")
8
+ end
9
+
10
+ it "should allow alias scopes from the search object" do
11
+ search = User.search
12
+ search.username_has = "bjohnson"
13
+ search.username_has.should == "bjohnson"
14
+ end
15
+ end
@@ -0,0 +1,120 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ describe "Associations" do
4
+ it "should create a named scope" do
5
+ Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => :users)
6
+ end
7
+
8
+ it "should create a deep named scope" do
9
+ Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => {:users => :orders})
10
+ end
11
+
12
+ it "should not allowed named scopes on non existent association columns" do
13
+ lambda { User.users_whatever_like("bjohnson") }.should raise_error(NoMethodError)
14
+ end
15
+
16
+ it "should not allowed named scopes on non existent deep association columns" do
17
+ lambda { User.users_orders_whatever_like("bjohnson") }.should raise_error(NoMethodError)
18
+ end
19
+
20
+ it "should allow named scopes to be called multiple times and reflect the value passed" do
21
+ Company.users_username_like("bjohnson").proxy_options.should == User.username_like("bjohnson").proxy_options.merge(:joins => :users)
22
+ Company.users_username_like("thunt").proxy_options.should == User.username_like("thunt").proxy_options.merge(:joins => :users)
23
+ end
24
+
25
+ it "should allow deep named scopes to be called multiple times and reflect the value passed" do
26
+ Company.users_orders_total_greater_than(10).proxy_options.should == Order.total_greater_than(10).proxy_options.merge(:joins => {:users => :orders})
27
+ Company.users_orders_total_greater_than(20).proxy_options.should == Order.total_greater_than(20).proxy_options.merge(:joins => {:users => :orders})
28
+ end
29
+
30
+ it "should have an arity of 1 if the underlying scope has an arity of 1" do
31
+ Company.users_orders_total_greater_than(10)
32
+ Company.named_scope_arity("users_orders_total_greater_than").should == Order.named_scope_arity("total_greater_than")
33
+ end
34
+
35
+ it "should have an arity of nil if the underlying scope has an arity of nil" do
36
+ Company.users_orders_total_null
37
+ Company.named_scope_arity("users_orders_total_null").should == Order.named_scope_arity("total_null")
38
+ end
39
+
40
+ it "should have an arity of -1 if the underlying scope has an arity of -1" do
41
+ Company.users_id_equals_any
42
+ Company.named_scope_arity("users_id_equals_any").should == User.named_scope_arity("id_equals_any")
43
+ end
44
+
45
+ it "should allow aliases" do
46
+ Company.users_username_contains("bjohnson").proxy_options.should == User.username_contains("bjohnson").proxy_options.merge(:joins => :users)
47
+ end
48
+
49
+ it "should allow deep aliases" do
50
+ Company.users_orders_total_gt(10).proxy_options.should == Order.total_gt(10).proxy_options.merge(:joins => {:users => :orders})
51
+ end
52
+
53
+ it "should allow ascending" do
54
+ Company.ascend_by_users_username.proxy_options.should == User.ascend_by_username.proxy_options.merge(:joins => :users)
55
+ end
56
+
57
+ it "should allow descending" do
58
+ Company.descend_by_users_username.proxy_options.should == User.descend_by_username.proxy_options.merge(:joins => :users)
59
+ end
60
+
61
+ it "should allow deep ascending" do
62
+ Company.ascend_by_users_orders_total.proxy_options.should == Order.ascend_by_total.proxy_options.merge(:joins => {:users => :orders})
63
+ end
64
+
65
+ it "should allow deep descending" do
66
+ Company.descend_by_users_orders_total.proxy_options.should == Order.descend_by_total.proxy_options.merge(:joins => {:users => :orders})
67
+ end
68
+
69
+ it "should include optional associations" do
70
+ pending # this is a problem with using inner joins and left outer joins
71
+ Company.create
72
+ company = Company.create
73
+ user = company.users.create
74
+ order = user.orders.create(:total => 20, :taxes => 3)
75
+ Company.ascend_by_users_orders_total.all.should == Company.all
76
+ end
77
+
78
+ it "should not create the same join twice" do
79
+ company = Company.create
80
+ user = company.users.create
81
+ order = user.orders.create(:total => 20, :taxes => 3)
82
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all.should == Company.all
83
+ end
84
+
85
+ it "should not create the same join twice when traveling through the duplicate join" do
86
+ Company.users_username_like("bjohnson").users_orders_total_gt(100).all.should == Company.all
87
+ end
88
+
89
+ it "should not create the same join twice when traveling through the duplicate join 2" do
90
+ Company.users_orders_total_gt(100).users_orders_line_items_price_gt(20).all.should == Company.all
91
+ end
92
+
93
+ it "should allow the use of :include when a join was created" do
94
+ company = Company.create
95
+ user = company.users.create
96
+ order = user.orders.create(:total => 20, :taxes => 3)
97
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => :users).should == Company.all
98
+ end
99
+
100
+ it "should allow the use of deep :include when a join was created" do
101
+ company = Company.create
102
+ user = company.users.create
103
+ order = user.orders.create(:total => 20, :taxes => 3)
104
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => {:users => :orders}).should == Company.all
105
+ end
106
+
107
+ it "should allow the use of :include when traveling through the duplicate join" do
108
+ company = Company.create
109
+ user = company.users.create(:username => "bjohnson")
110
+ order = user.orders.create(:total => 20, :taxes => 3)
111
+ Company.users_username_like("bjohnson").users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => :users).should == Company.all
112
+ end
113
+
114
+ it "should allow the use of deep :include when traveling through the duplicate join" do
115
+ company = Company.create
116
+ user = company.users.create(:username => "bjohnson")
117
+ order = user.orders.create(:total => 20, :taxes => 3)
118
+ Company.users_orders_taxes_lt(50).ascend_by_users_orders_total.all(:include => {:users => :orders}).should == Company.all
119
+ end
120
+ end