skanev-searchlogic 2.1.8.1

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 local_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) && !association_ordering_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_(#{column_names.join("|")})$/
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,151 @@
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
+ value = column_for_type_cast.type_cast(value)
147
+ Time.zone && value.is_a?(Time) ? value.in_time_zone : value
148
+ end
149
+ end
150
+ end
151
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require "searchlogic"
@@ -0,0 +1,77 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{searchlogic}
5
+ s.version = "2.1.8.1"
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-16}
10
+ s.description = %q{Searchlogic provides common named scopes and object based searching for ActiveRecord.}
11
+ s.email = %q{bjohnson@binarylogic.com}
12
+ s.extra_rdoc_files = [
13
+ "LICENSE",
14
+ "README.rdoc"
15
+ ]
16
+ s.files = [
17
+ ".gitignore",
18
+ "CHANGELOG.rdoc",
19
+ "LICENSE",
20
+ "README.rdoc",
21
+ "Rakefile",
22
+ "VERSION.yml",
23
+ "init.rb",
24
+ "lib/searchlogic.rb",
25
+ "lib/searchlogic/active_record_consistency.rb",
26
+ "lib/searchlogic/core_ext/object.rb",
27
+ "lib/searchlogic/core_ext/proc.rb",
28
+ "lib/searchlogic/named_scopes/alias_scope.rb",
29
+ "lib/searchlogic/named_scopes/association_conditions.rb",
30
+ "lib/searchlogic/named_scopes/association_ordering.rb",
31
+ "lib/searchlogic/named_scopes/conditions.rb",
32
+ "lib/searchlogic/named_scopes/ordering.rb",
33
+ "lib/searchlogic/rails_helpers.rb",
34
+ "lib/searchlogic/search.rb",
35
+ "rails/init.rb",
36
+ "searchlogic.gemspec",
37
+ "spec/core_ext/object_spec.rb",
38
+ "spec/core_ext/proc_spec.rb",
39
+ "spec/named_scopes/alias_scope_spec.rb",
40
+ "spec/named_scopes/association_conditions_spec.rb",
41
+ "spec/named_scopes/association_ordering_spec.rb",
42
+ "spec/named_scopes/conditions_spec.rb",
43
+ "spec/named_scopes/ordering_spec.rb",
44
+ "spec/search_spec.rb",
45
+ "spec/spec_helper.rb"
46
+ ]
47
+ s.homepage = %q{http://github.com/skanev/searchlogic}
48
+ s.rdoc_options = ["--charset=UTF-8"]
49
+ s.require_paths = ["lib"]
50
+ s.rubyforge_project = %q{searchlogic}
51
+ s.rubygems_version = %q{1.3.4}
52
+ s.summary = %q{Searchlogic provides common named scopes and object based searching for ActiveRecord.}
53
+ s.test_files = [
54
+ "spec/core_ext/object_spec.rb",
55
+ "spec/core_ext/proc_spec.rb",
56
+ "spec/named_scopes/alias_scope_spec.rb",
57
+ "spec/named_scopes/association_conditions_spec.rb",
58
+ "spec/named_scopes/association_ordering_spec.rb",
59
+ "spec/named_scopes/conditions_spec.rb",
60
+ "spec/named_scopes/ordering_spec.rb",
61
+ "spec/search_spec.rb",
62
+ "spec/spec_helper.rb"
63
+ ]
64
+
65
+ if s.respond_to? :specification_version then
66
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
67
+ s.specification_version = 3
68
+
69
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
70
+ s.add_runtime_dependency(%q<activerecord>, [">= 2.0.0"])
71
+ else
72
+ s.add_dependency(%q<activerecord>, [">= 2.0.0"])
73
+ end
74
+ else
75
+ s.add_dependency(%q<activerecord>, [">= 2.0.0"])
76
+ end
77
+ 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,104 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/../spec_helper")
2
+
3
+ describe "Association Conditions" 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 include optional associations" do
54
+ pending # this is a problem with using inner joins and left outer joins
55
+ Company.create
56
+ company = Company.create
57
+ user = company.users.create
58
+ order = user.orders.create(:total => 20, :taxes => 3)
59
+ Company.ascend_by_users_orders_total.all.should == Company.all
60
+ end
61
+
62
+ it "should not create the same join twice" do
63
+ company = Company.create
64
+ user = company.users.create
65
+ order = user.orders.create(:total => 20, :taxes => 3)
66
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all.should == Company.all
67
+ end
68
+
69
+ it "should not create the same join twice when traveling through the duplicate join" do
70
+ Company.users_username_like("bjohnson").users_orders_total_gt(100).all.should == Company.all
71
+ end
72
+
73
+ it "should not create the same join twice when traveling through the duplicate join 2" do
74
+ Company.users_orders_total_gt(100).users_orders_line_items_price_gt(20).all.should == Company.all
75
+ end
76
+
77
+ it "should allow the use of :include when a join was created" do
78
+ company = Company.create
79
+ user = company.users.create
80
+ order = user.orders.create(:total => 20, :taxes => 3)
81
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => :users).should == Company.all
82
+ end
83
+
84
+ it "should allow the use of deep :include when a join was created" do
85
+ company = Company.create
86
+ user = company.users.create
87
+ order = user.orders.create(:total => 20, :taxes => 3)
88
+ Company.users_orders_total_gt(10).users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => {:users => :orders}).should == Company.all
89
+ end
90
+
91
+ it "should allow the use of :include when traveling through the duplicate join" do
92
+ company = Company.create
93
+ user = company.users.create(:username => "bjohnson")
94
+ order = user.orders.create(:total => 20, :taxes => 3)
95
+ Company.users_username_like("bjohnson").users_orders_taxes_lt(5).ascend_by_users_orders_total.all(:include => :users).should == Company.all
96
+ end
97
+
98
+ it "should allow the use of deep :include when traveling through the duplicate join" do
99
+ company = Company.create
100
+ user = company.users.create(:username => "bjohnson")
101
+ order = user.orders.create(:total => 20, :taxes => 3)
102
+ Company.users_orders_taxes_lt(50).ascend_by_users_orders_total.all(:include => {:users => :orders}).should == Company.all
103
+ end
104
+ end