searchlogic 1.5.3
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +228 -0
- data/MIT-LICENSE +20 -0
- data/Manifest +123 -0
- data/README.rdoc +383 -0
- data/Rakefile +15 -0
- data/TODO.rdoc +6 -0
- data/examples/README.rdoc +4 -0
- data/init.rb +1 -0
- data/lib/searchlogic.rb +89 -0
- data/lib/searchlogic/active_record/associations.rb +52 -0
- data/lib/searchlogic/active_record/base.rb +218 -0
- data/lib/searchlogic/active_record/connection_adapters/mysql_adapter.rb +172 -0
- data/lib/searchlogic/active_record/connection_adapters/postgresql_adapter.rb +168 -0
- data/lib/searchlogic/active_record/connection_adapters/sqlite_adapter.rb +75 -0
- data/lib/searchlogic/condition/base.rb +159 -0
- data/lib/searchlogic/condition/begins_with.rb +17 -0
- data/lib/searchlogic/condition/blank.rb +21 -0
- data/lib/searchlogic/condition/child_of.rb +11 -0
- data/lib/searchlogic/condition/descendant_of.rb +24 -0
- data/lib/searchlogic/condition/ends_with.rb +17 -0
- data/lib/searchlogic/condition/equals.rb +27 -0
- data/lib/searchlogic/condition/greater_than.rb +15 -0
- data/lib/searchlogic/condition/greater_than_or_equal_to.rb +15 -0
- data/lib/searchlogic/condition/inclusive_descendant_of.rb +11 -0
- data/lib/searchlogic/condition/keywords.rb +47 -0
- data/lib/searchlogic/condition/less_than.rb +15 -0
- data/lib/searchlogic/condition/less_than_or_equal_to.rb +15 -0
- data/lib/searchlogic/condition/like.rb +15 -0
- data/lib/searchlogic/condition/nil.rb +21 -0
- data/lib/searchlogic/condition/not_begin_with.rb +20 -0
- data/lib/searchlogic/condition/not_blank.rb +19 -0
- data/lib/searchlogic/condition/not_end_with.rb +20 -0
- data/lib/searchlogic/condition/not_equal.rb +26 -0
- data/lib/searchlogic/condition/not_have_keywords.rb +20 -0
- data/lib/searchlogic/condition/not_like.rb +20 -0
- data/lib/searchlogic/condition/not_nil.rb +19 -0
- data/lib/searchlogic/condition/sibling_of.rb +14 -0
- data/lib/searchlogic/condition/tree.rb +17 -0
- data/lib/searchlogic/conditions/base.rb +484 -0
- data/lib/searchlogic/conditions/protection.rb +36 -0
- data/lib/searchlogic/config.rb +31 -0
- data/lib/searchlogic/config/helpers.rb +289 -0
- data/lib/searchlogic/config/search.rb +53 -0
- data/lib/searchlogic/core_ext/hash.rb +75 -0
- data/lib/searchlogic/helpers/control_types/link.rb +310 -0
- data/lib/searchlogic/helpers/control_types/links.rb +241 -0
- data/lib/searchlogic/helpers/control_types/remote_link.rb +87 -0
- data/lib/searchlogic/helpers/control_types/remote_links.rb +72 -0
- data/lib/searchlogic/helpers/control_types/remote_select.rb +36 -0
- data/lib/searchlogic/helpers/control_types/select.rb +82 -0
- data/lib/searchlogic/helpers/form.rb +208 -0
- data/lib/searchlogic/helpers/utilities.rb +197 -0
- data/lib/searchlogic/modifiers/absolute.rb +15 -0
- data/lib/searchlogic/modifiers/acos.rb +11 -0
- data/lib/searchlogic/modifiers/asin.rb +11 -0
- data/lib/searchlogic/modifiers/atan.rb +11 -0
- data/lib/searchlogic/modifiers/base.rb +27 -0
- data/lib/searchlogic/modifiers/ceil.rb +15 -0
- data/lib/searchlogic/modifiers/char_length.rb +15 -0
- data/lib/searchlogic/modifiers/cos.rb +15 -0
- data/lib/searchlogic/modifiers/cot.rb +15 -0
- data/lib/searchlogic/modifiers/day_of_month.rb +15 -0
- data/lib/searchlogic/modifiers/day_of_week.rb +15 -0
- data/lib/searchlogic/modifiers/day_of_year.rb +15 -0
- data/lib/searchlogic/modifiers/degrees.rb +11 -0
- data/lib/searchlogic/modifiers/exp.rb +15 -0
- data/lib/searchlogic/modifiers/floor.rb +15 -0
- data/lib/searchlogic/modifiers/hex.rb +11 -0
- data/lib/searchlogic/modifiers/hour.rb +11 -0
- data/lib/searchlogic/modifiers/log.rb +15 -0
- data/lib/searchlogic/modifiers/log10.rb +11 -0
- data/lib/searchlogic/modifiers/log2.rb +11 -0
- data/lib/searchlogic/modifiers/lower.rb +15 -0
- data/lib/searchlogic/modifiers/ltrim.rb +15 -0
- data/lib/searchlogic/modifiers/md5.rb +11 -0
- data/lib/searchlogic/modifiers/microseconds.rb +11 -0
- data/lib/searchlogic/modifiers/milliseconds.rb +11 -0
- data/lib/searchlogic/modifiers/minute.rb +15 -0
- data/lib/searchlogic/modifiers/month.rb +15 -0
- data/lib/searchlogic/modifiers/octal.rb +15 -0
- data/lib/searchlogic/modifiers/radians.rb +11 -0
- data/lib/searchlogic/modifiers/round.rb +11 -0
- data/lib/searchlogic/modifiers/rtrim.rb +15 -0
- data/lib/searchlogic/modifiers/second.rb +15 -0
- data/lib/searchlogic/modifiers/sign.rb +11 -0
- data/lib/searchlogic/modifiers/sin.rb +11 -0
- data/lib/searchlogic/modifiers/square_root.rb +15 -0
- data/lib/searchlogic/modifiers/tan.rb +15 -0
- data/lib/searchlogic/modifiers/trim.rb +15 -0
- data/lib/searchlogic/modifiers/upper.rb +15 -0
- data/lib/searchlogic/modifiers/week.rb +11 -0
- data/lib/searchlogic/modifiers/year.rb +11 -0
- data/lib/searchlogic/search/base.rb +148 -0
- data/lib/searchlogic/search/conditions.rb +53 -0
- data/lib/searchlogic/search/ordering.rb +244 -0
- data/lib/searchlogic/search/pagination.rb +121 -0
- data/lib/searchlogic/search/protection.rb +89 -0
- data/lib/searchlogic/search/searching.rb +31 -0
- data/lib/searchlogic/shared/utilities.rb +50 -0
- data/lib/searchlogic/shared/virtual_classes.rb +39 -0
- data/lib/searchlogic/version.rb +79 -0
- data/searchlogic.gemspec +39 -0
- data/test/fixtures/accounts.yml +15 -0
- data/test/fixtures/cats.yml +3 -0
- data/test/fixtures/dogs.yml +3 -0
- data/test/fixtures/orders.yml +14 -0
- data/test/fixtures/user_groups.yml +13 -0
- data/test/fixtures/users.yml +36 -0
- data/test/test_active_record_associations.rb +81 -0
- data/test/test_active_record_base.rb +93 -0
- data/test/test_condition_base.rb +52 -0
- data/test/test_condition_types.rb +143 -0
- data/test/test_conditions_base.rb +242 -0
- data/test/test_conditions_protection.rb +16 -0
- data/test/test_config.rb +23 -0
- data/test/test_helper.rb +134 -0
- data/test/test_search_base.rb +227 -0
- data/test/test_search_conditions.rb +19 -0
- data/test/test_search_ordering.rb +165 -0
- data/test/test_search_pagination.rb +72 -0
- data/test/test_search_protection.rb +24 -0
- data/test_libs/acts_as_tree.rb +98 -0
- data/test_libs/ordered_hash.rb +9 -0
- data/test_libs/rexml_fix.rb +14 -0
- metadata +317 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class DescendantOf < Tree
|
4
|
+
def to_conditions(value)
|
5
|
+
# Wish I knew how to do this in SQL
|
6
|
+
root = (value.is_a?(klass) ? value : klass.find(value)) rescue return
|
7
|
+
strs = []
|
8
|
+
subs = []
|
9
|
+
all_children_ids(root).each do |child_id|
|
10
|
+
strs << "#{quoted_table_name}.#{quote_column_name(klass.primary_key)} = ?"
|
11
|
+
subs << child_id
|
12
|
+
end
|
13
|
+
[strs.join(" OR "), *subs]
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def all_children_ids(record)
|
18
|
+
ids = record.children.collect { |child| child.send(klass.primary_key) }
|
19
|
+
record.children.each { |child| ids += all_children_ids(child) }
|
20
|
+
ids
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class EndsWith < Base
|
4
|
+
self.join_arrays_with_or = true
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def condition_names_for_column
|
8
|
+
super + ["ew", "ends", "end"]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_conditions(value)
|
13
|
+
["#{column_sql} LIKE ?", "%#{value}"]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class Equals < Base
|
4
|
+
self.handle_array_value = true
|
5
|
+
self.ignore_meaningless_value = false
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def condition_names_for_column
|
9
|
+
super + ["", "is"]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_conditions(value)
|
14
|
+
# Let ActiveRecord handle this
|
15
|
+
args = []
|
16
|
+
case value
|
17
|
+
when Range
|
18
|
+
args = [value.first, value.last]
|
19
|
+
else
|
20
|
+
args << value
|
21
|
+
end
|
22
|
+
|
23
|
+
["#{column_sql} #{klass.send(:attribute_condition, value)}", *args]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class GreaterThanOrEqualTo < Base
|
4
|
+
class << self
|
5
|
+
def condition_names_for_column
|
6
|
+
super + ["gte", "at_least", "least"]
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_conditions(value)
|
11
|
+
["#{column_sql} >= ?", value]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class InclusiveDescendantOf < Tree
|
4
|
+
def to_conditions(value)
|
5
|
+
condition = DescendantOf.new(klass, options)
|
6
|
+
condition.value = value
|
7
|
+
merge_conditions(["#{quoted_table_name}.#{quote_column_name(klass.primary_key)} = ?", (value.is_a?(klass) ? value.send(klass.primary_key) : value)], condition.sanitize, :any => true)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class Keywords < Base
|
4
|
+
# Because be default it joins with AND, so padding an array just gives you more options. Joining with and is no different than combining all of the words.
|
5
|
+
self.join_arrays_with_or = true
|
6
|
+
|
7
|
+
BLACKLISTED_WORDS = ('a'..'z').to_a + ["about", "an", "are", "as", "at", "be", "by", "com", "de", "en", "for", "from", "how", "in", "is", "it", "la", "of", "on", "or", "that", "the", "the", "this", "to", "und", "was", "what", "when", "where", "who", "will", "with", "www"] # from ranks.nl
|
8
|
+
FOREIGN_CHARACTERS = 'àáâãäåßéèêëìíîïñòóôõöùúûüýÿ'
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def condition_names_for_column
|
12
|
+
super + ["kwords", "kw"]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_conditions(value)
|
17
|
+
strs = []
|
18
|
+
subs = []
|
19
|
+
|
20
|
+
search_parts = value.gsub(/,/, " ").split(/ /)
|
21
|
+
replace_non_alnum_characters!(search_parts)
|
22
|
+
search_parts.uniq!
|
23
|
+
remove_blacklisted_words!(search_parts)
|
24
|
+
return if search_parts.blank?
|
25
|
+
|
26
|
+
search_parts.each do |search_part|
|
27
|
+
strs << "#{column_sql} #{like_condition_name} ?"
|
28
|
+
subs << "%#{search_part}%"
|
29
|
+
end
|
30
|
+
|
31
|
+
[strs.join(" AND "), *subs]
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def replace_non_alnum_characters!(search_parts)
|
36
|
+
search_parts.each do |word|
|
37
|
+
word.downcase!
|
38
|
+
word.gsub!(/[^[:alnum:]#{FOREIGN_CHARACTERS}]/, '')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove_blacklisted_words!(search_parts)
|
43
|
+
search_parts.delete_if { |word| word.blank? || BLACKLISTED_WORDS.include?(word.downcase) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class LessThanOrEqualTo < Base
|
4
|
+
class << self
|
5
|
+
def condition_names_for_column
|
6
|
+
super + ["lte", "at_most", "most"]
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_conditions(value)
|
11
|
+
["#{column_sql} <= ?", value]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class Like < Base
|
4
|
+
class << self
|
5
|
+
def condition_names_for_column
|
6
|
+
super + ["contains", "has"]
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_conditions(value)
|
11
|
+
["#{column_sql} #{like_condition_name} ?", "%#{value}%"]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class Nil < Base
|
4
|
+
self.value_type = :boolean
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def condition_names_for_column
|
8
|
+
super + ["is_nil", "is_null", "null"]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_conditions(value)
|
13
|
+
if value == true
|
14
|
+
"#{column_sql} IS NULL"
|
15
|
+
elsif value == false
|
16
|
+
"#{column_sql} IS NOT NULL"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class NotBeginWith < Base
|
4
|
+
class << self
|
5
|
+
def condition_names_for_column
|
6
|
+
super + ["not_bw", "not_sw", "not_start_with", "not_start", "beginning_is_not", "beginning_not"]
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_conditions(value)
|
11
|
+
begin_with = BeginWith.new(klass, options)
|
12
|
+
begin_with.value = value
|
13
|
+
conditions = being_with.sanitize
|
14
|
+
return conditions if conditions.blank?
|
15
|
+
conditions.first.gsub!(" LIKE ", " NOT LIKE ")
|
16
|
+
conditions
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class NotBlank < Base
|
4
|
+
self.value_type = :boolean
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def condition_names_for_column
|
8
|
+
super + ["is_not_blank"]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_conditions(value)
|
13
|
+
blank = Blank.new(klass, options)
|
14
|
+
blank.value = !value
|
15
|
+
blank.sanitize
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class NotEndWith < Base
|
4
|
+
class << self
|
5
|
+
def condition_names_for_column
|
6
|
+
super + ["not_ew", "not_end", "end_is_not", "end_not"]
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_conditions(value)
|
11
|
+
ends_with = EndsWith.new(klass, options)
|
12
|
+
ends_with.value = value
|
13
|
+
conditions = ends_with.sanitize
|
14
|
+
return conditions if conditions.blank?
|
15
|
+
conditions.first.gsub!(" LIKE ", " NOT LIKE ")
|
16
|
+
conditions
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class NotEqual < Base
|
4
|
+
self.handle_array_value = true
|
5
|
+
self.ignore_meaningless_value = false
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def condition_names_for_column
|
9
|
+
super + ["does_not_equal", "not_equal", "is_not", "not"]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_conditions(value)
|
14
|
+
# Delegate to equals and then change
|
15
|
+
condition = Equals.new(klass, options)
|
16
|
+
condition.value = value
|
17
|
+
conditions_array = condition.sanitize
|
18
|
+
conditions_array.first.gsub!(/ IS /, " IS NOT ")
|
19
|
+
conditions_array.first.gsub!(/ BETWEEN /, " NOT BETWEEN ")
|
20
|
+
conditions_array.first.gsub!(/ IN /, " NOT IN ")
|
21
|
+
conditions_array.first.gsub!(/=/, "!=")
|
22
|
+
conditions_array
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class NotHaveKeywords < Base
|
4
|
+
class << self
|
5
|
+
def condition_names_for_column
|
6
|
+
super + ["not_have_keywords", "not_keywords", "not_have_kw", "not_kw", "not_have_kwwords", "not_kwwords"]
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_conditions(value)
|
11
|
+
keywords = Keywords.new(klass, options)
|
12
|
+
keywords.value = value
|
13
|
+
conditions = keywords.sanitize
|
14
|
+
return conditions if conditions.blank?
|
15
|
+
conditions.first.gsub!(" #{like_condition_name} ", " NOT #{like_condition_name} ")
|
16
|
+
conditions
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class NotLike < Base
|
4
|
+
class << self
|
5
|
+
def condition_names_for_column
|
6
|
+
super + ["not_contain", "not_have"]
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_conditions(value)
|
11
|
+
like = Like.new(klass, options)
|
12
|
+
like.value = value
|
13
|
+
conditions = like.sanitize
|
14
|
+
return conditions if conditions.blank?
|
15
|
+
conditions.first.gsub!(" #{like_condition_name} ", " NOT #{like_condition_name} ")
|
16
|
+
conditions
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class NotNil < Base
|
4
|
+
self.value_type = :boolean
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def condition_names_for_column
|
8
|
+
super + ["is_not_nil", "is_not_null", "not_null"]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_conditions(value)
|
13
|
+
is_nil = Nil.new(klass, options)
|
14
|
+
is_nil.value = !value
|
15
|
+
is_nil.sanitize
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class SiblingOf < Tree
|
4
|
+
def to_conditions(value)
|
5
|
+
parent_association = klass.reflect_on_association(:parent)
|
6
|
+
foreign_key_name = (parent_association && parent_association.options[:foreign_key]) || "parent_id"
|
7
|
+
parent_id = (value.is_a?(klass) ? value : klass.find(value)).send(foreign_key_name)
|
8
|
+
condition = ChildOf.new(klass, options)
|
9
|
+
condition.value = parent_id
|
10
|
+
merge_conditions(["#{quoted_table_name}.#{quote_column_name(klass.primary_key)} != ?", (value.is_a?(klass) ? value.send(klass.primary_key) : value)], condition.sanitize)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Condition
|
3
|
+
class Tree < Base # :nodoc:
|
4
|
+
self.join_arrays_with_or = true
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def condition_names_for_column
|
8
|
+
[]
|
9
|
+
end
|
10
|
+
|
11
|
+
def condition_names_for_model(model)
|
12
|
+
[condition_type_name]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,484 @@
|
|
1
|
+
module Searchlogic
|
2
|
+
module Conditions # :nodoc:
|
3
|
+
# = Conditions
|
4
|
+
#
|
5
|
+
# Represents a collection of conditions and performs various tasks on that collection. For information on each condition see Searchlogic::Condition.
|
6
|
+
# Each condition has its own file and class and the source for each condition is pretty self explanatory.
|
7
|
+
class Base
|
8
|
+
include Shared::Utilities
|
9
|
+
include Shared::VirtualClasses
|
10
|
+
|
11
|
+
attr_accessor :any, :relationship_name
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_accessor :added_klass_conditions, :added_column_equals_conditions, :added_associations
|
15
|
+
|
16
|
+
def column_details # :nodoc:
|
17
|
+
return @column_details if @column_details
|
18
|
+
|
19
|
+
@column_details = []
|
20
|
+
|
21
|
+
klass.columns.each do |column|
|
22
|
+
column_detail = {:column => column}
|
23
|
+
column_detail[:aliases] = case column.type
|
24
|
+
when :datetime, :time, :timestamp
|
25
|
+
[column.name.gsub(/_at$/, "")]
|
26
|
+
when :date
|
27
|
+
[column.name.gsub(/_at$/, "")]
|
28
|
+
else
|
29
|
+
[]
|
30
|
+
end
|
31
|
+
|
32
|
+
@column_details << column_detail
|
33
|
+
end
|
34
|
+
|
35
|
+
@column_details
|
36
|
+
end
|
37
|
+
|
38
|
+
# Registers a condition as an available condition for a column or a class. MySQL supports a "sounds like" function. I want to use it, so let's add it.
|
39
|
+
#
|
40
|
+
# === Example
|
41
|
+
#
|
42
|
+
# # config/initializers/searchlogic.rb
|
43
|
+
# # Actual function for MySQL databases only
|
44
|
+
# class SoundsLike < Searchlogic::Condition::Base
|
45
|
+
# # The name of the conditions. By default its the name of the class, if you want alternate or alias conditions just add them on.
|
46
|
+
# # If you don't want to add aliases you don't even need to define this method
|
47
|
+
# def self.name_for_column(column)
|
48
|
+
# super
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# # You can return an array or a string. NOT a hash, because all of these conditions
|
52
|
+
# # need to eventually get merged together. The array or string can be anything you would put in
|
53
|
+
# # the :conditions option for ActiveRecord::Base.find(). Also notice the column_sql variable. This is essentail
|
54
|
+
# # for applying modifiers and should be used in your conditions wherever you want the column.
|
55
|
+
# def to_conditions(value)
|
56
|
+
# ["#{column_sql} SOUNDS LIKE ?", value]
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# Searchlogic::Seearch::Conditions.register_condition(SoundsLike)
|
61
|
+
def register_condition(condition_class)
|
62
|
+
raise(ArgumentError, "You can only register conditions that extend Searchlogic::Condition::Base") unless condition_class.ancestors.include?(Searchlogic::Condition::Base)
|
63
|
+
conditions << condition_class unless conditions.include?(condition_class)
|
64
|
+
end
|
65
|
+
|
66
|
+
# A list of available condition type classes
|
67
|
+
def conditions
|
68
|
+
@@conditions ||= []
|
69
|
+
end
|
70
|
+
|
71
|
+
# Registers a modifier as an available modifier for each column.
|
72
|
+
#
|
73
|
+
# === Example
|
74
|
+
#
|
75
|
+
# # config/initializers/searchlogic.rb
|
76
|
+
# class Ceil < Searchlogic::Modifiers::Base
|
77
|
+
# # The name of the modifier. By default its the name of the class, if you want alternate or alias modifiers just add them on.
|
78
|
+
# # If you don't want to add aliases you don't even need to define this method
|
79
|
+
# def self.modifier_names
|
80
|
+
# super + ["round_up"]
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# # The name of the method in the connection adapters (see below). By default its the name of your class suffixed with "_sql".
|
84
|
+
# # So in this example it would be "ceil_sql". Unless you want to change that you don't need to define this method.
|
85
|
+
# def self.adapter_method_name
|
86
|
+
# super
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# # This is the type of value returned from the modifier. This is neccessary for typcasting values for the modifier when
|
90
|
+
# # applied to a column
|
91
|
+
# def self.return_type
|
92
|
+
# :integer
|
93
|
+
# end
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# Searchlogic::Seearch::Conditions.register_modifiers(Ceil)
|
97
|
+
#
|
98
|
+
# Now here's the fun part, applying this modifier to each connection adapter. Some databases call modifiers differently. If they all apply them the same you can
|
99
|
+
# add in the function to ActiveRecord::ConnectionAdapters::AbstractAdapter, otherwise you need to add them to each
|
100
|
+
# individually: ActiveRecord::ConnectionAdapters::MysqlAdapter, ActiveRecord::ConnectionAdapters::PostgreSQLAdapter, ActiveRecord::ConnectionAdapters::SQLiteAdapter
|
101
|
+
#
|
102
|
+
# Do this by includine a model with your method. The name of your method, by default, is: #{modifier_name}_sql. So in the example above it would be "ceil_sql"
|
103
|
+
#
|
104
|
+
# module CeilAdapterMethod
|
105
|
+
# def ceil_sql(column_name)
|
106
|
+
# "CEIL(#{column_name})"
|
107
|
+
# end
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
# ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:include, CeilAdapterMethod)
|
111
|
+
# # ... include for the rest of the adapters
|
112
|
+
def register_modifier(modifier_class)
|
113
|
+
raise(ArgumentError, "You can only register conditions that extend Searchlogic::Modifiers::Base") unless modifier_class.ancestors.include?(Searchlogic::Modifiers::Base)
|
114
|
+
modifiers << modifier_class unless modifiers.include?(modifier_class)
|
115
|
+
end
|
116
|
+
|
117
|
+
# A list of available modifier classes
|
118
|
+
def modifiers
|
119
|
+
@@modifiers ||= []
|
120
|
+
end
|
121
|
+
|
122
|
+
# A list of all associations created, used for caching and performance
|
123
|
+
def association_names
|
124
|
+
@association_names ||= []
|
125
|
+
end
|
126
|
+
|
127
|
+
# A list of all conditions available, users for caching and performance
|
128
|
+
def condition_names
|
129
|
+
@condition_names ||= []
|
130
|
+
end
|
131
|
+
|
132
|
+
def needed?(model_class, conditions) # :nodoc:
|
133
|
+
return false if conditions.blank?
|
134
|
+
|
135
|
+
if conditions.is_a?(Hash)
|
136
|
+
return true if conditions[:any]
|
137
|
+
stringified_conditions = conditions.stringify_keys
|
138
|
+
stringified_conditions.keys.each { |condition| return false if condition.include?(".") } # setting conditions on associations, which is just another way of writing SQL, and we ignore SQL
|
139
|
+
|
140
|
+
column_names = model_class.column_names
|
141
|
+
stringified_conditions.keys.each do |condition|
|
142
|
+
return true unless column_names.include?(condition)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
false
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def initialize(init_conditions = {})
|
151
|
+
add_associations!
|
152
|
+
add_column_equals_conditions!
|
153
|
+
self.conditions = init_conditions
|
154
|
+
end
|
155
|
+
|
156
|
+
# Determines if we should join the conditions with "AND" or "OR".
|
157
|
+
#
|
158
|
+
# === Examples
|
159
|
+
#
|
160
|
+
# search.conditions.any = true # will join all conditions with "or", you can also set this to "true", "1", or "yes"
|
161
|
+
# search.conditions.any = false # will join all conditions with "and"
|
162
|
+
def any=(value)
|
163
|
+
associations.each { |name, association| association.any = value }
|
164
|
+
@any = value
|
165
|
+
end
|
166
|
+
|
167
|
+
def any # :nodoc:
|
168
|
+
any?
|
169
|
+
end
|
170
|
+
|
171
|
+
# Convenience method for determining if we should join the conditions with "AND" or "OR".
|
172
|
+
def any?
|
173
|
+
@any == true || @any == "true" || @any == "1" || @any == "yes"
|
174
|
+
end
|
175
|
+
|
176
|
+
# A list of joins to use when searching, includes relationships
|
177
|
+
def auto_joins
|
178
|
+
j = []
|
179
|
+
associations.each do |name, association|
|
180
|
+
next if association.conditions.blank?
|
181
|
+
association_joins = association.auto_joins
|
182
|
+
j << (association_joins.blank? ? name : {name => association_joins})
|
183
|
+
end
|
184
|
+
j.blank? ? nil : (j.size == 1 ? j.first : j)
|
185
|
+
end
|
186
|
+
|
187
|
+
def inspect
|
188
|
+
"#<#{klass}Conditions#{conditions.blank? ? "" : " #{conditions.inspect}"}>"
|
189
|
+
end
|
190
|
+
|
191
|
+
# Sanitizes the conditions down into conditions that ActiveRecord::Base.find can understand.
|
192
|
+
def sanitize
|
193
|
+
return @conditions if @conditions
|
194
|
+
merge_conditions(*(objects.collect { |name, object| object.sanitize } << {:any => any}))
|
195
|
+
end
|
196
|
+
|
197
|
+
# Allows you to set the conditions via a hash.
|
198
|
+
def conditions=(value)
|
199
|
+
case value
|
200
|
+
when Hash
|
201
|
+
assert_valid_conditions(value)
|
202
|
+
remove_conditions_from_protected_assignement(value).each do |condition, condition_value|
|
203
|
+
|
204
|
+
# delete all blanks from mass assignments, forms submit blanks, blanks are meaningless
|
205
|
+
# equals condition thinks everything is meaningful, and arrays can be pased
|
206
|
+
new_condition_value = nil
|
207
|
+
case condition_value
|
208
|
+
when Array
|
209
|
+
new_condition_value = []
|
210
|
+
condition_value.each { |v| new_condition_value << v unless v == "" }
|
211
|
+
next if new_condition_value.size == 0
|
212
|
+
new_condition_value = new_condition_value.first if new_condition_value.size == 1
|
213
|
+
else
|
214
|
+
next if condition_value == ""
|
215
|
+
new_condition_value = condition_value
|
216
|
+
end
|
217
|
+
|
218
|
+
send("#{condition}=", new_condition_value)
|
219
|
+
end
|
220
|
+
else
|
221
|
+
reset_objects!
|
222
|
+
@conditions = value
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# All of the active conditions (conditions that have been set)
|
227
|
+
def conditions
|
228
|
+
return @conditions if @conditions
|
229
|
+
return if objects.blank?
|
230
|
+
|
231
|
+
conditions_hash = {}
|
232
|
+
objects.each do |name, object|
|
233
|
+
if object.class < Searchlogic::Conditions::Base
|
234
|
+
relationship_conditions = object.conditions
|
235
|
+
next if relationship_conditions.blank?
|
236
|
+
conditions_hash[name] = relationship_conditions
|
237
|
+
else
|
238
|
+
next if object.value_is_meaningless?
|
239
|
+
conditions_hash[name] = object.value
|
240
|
+
end
|
241
|
+
end
|
242
|
+
conditions_hash
|
243
|
+
end
|
244
|
+
|
245
|
+
private
|
246
|
+
def add_associations!
|
247
|
+
return true if self.class.added_associations
|
248
|
+
|
249
|
+
klass.reflect_on_all_associations.each do |association|
|
250
|
+
self.class.association_names << association.name.to_s
|
251
|
+
|
252
|
+
self.class.class_eval <<-"end_eval", __FILE__, __LINE__
|
253
|
+
def #{association.name}
|
254
|
+
if objects[:#{association.name}].nil?
|
255
|
+
objects[:#{association.name}] = Searchlogic::Conditions::Base.create_virtual_class(#{association.class_name}).new
|
256
|
+
objects[:#{association.name}].relationship_name = "#{association.name}"
|
257
|
+
objects[:#{association.name}].protect = protect
|
258
|
+
end
|
259
|
+
objects[:#{association.name}]
|
260
|
+
end
|
261
|
+
|
262
|
+
def #{association.name}=(conditions); @conditions = nil; #{association.name}.conditions = conditions; end
|
263
|
+
def reset_#{association.name}!; objects.delete(:#{association.name}); end
|
264
|
+
end_eval
|
265
|
+
end
|
266
|
+
|
267
|
+
self.class.added_associations = true
|
268
|
+
end
|
269
|
+
|
270
|
+
def add_column_equals_conditions!
|
271
|
+
return true if self.class.added_column_equals_conditions
|
272
|
+
klass.column_names.each { |name| setup_condition(name) }
|
273
|
+
self.class.added_column_equals_conditions = true
|
274
|
+
end
|
275
|
+
|
276
|
+
def extract_column_and_condition_from_method_name(name)
|
277
|
+
name_parts = name.gsub("=", "").split("_")
|
278
|
+
|
279
|
+
condition_parts = []
|
280
|
+
column = nil
|
281
|
+
while column.nil? && name_parts.size > 0
|
282
|
+
possible_column_name = name_parts.join("_")
|
283
|
+
|
284
|
+
self.class.column_details.each do |column_detail|
|
285
|
+
if column_detail[:column].name == possible_column_name || column_detail[:aliases].include?(possible_column_name)
|
286
|
+
column = column_detail
|
287
|
+
break
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
condition_parts << name_parts.pop if !column
|
292
|
+
end
|
293
|
+
|
294
|
+
return if column.nil?
|
295
|
+
|
296
|
+
condition_name = condition_parts.reverse.join("_")
|
297
|
+
condition = nil
|
298
|
+
|
299
|
+
# Find the real condition
|
300
|
+
self.class.conditions.each do |condition_klass|
|
301
|
+
if condition_klass.condition_names_for_column.include?(condition_name)
|
302
|
+
condition = condition_klass
|
303
|
+
break
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
[column, condition]
|
308
|
+
end
|
309
|
+
|
310
|
+
def breakdown_method_name(name)
|
311
|
+
column_detail, condition_klass = extract_column_and_condition_from_method_name(name)
|
312
|
+
if !column_detail.nil? && !condition_klass.nil?
|
313
|
+
# There were no modifiers
|
314
|
+
return [[], column_detail, condition_klass]
|
315
|
+
else
|
316
|
+
# There might be modifiers
|
317
|
+
name_parts = name.split("_of_")
|
318
|
+
column_detail, condition_klass = extract_column_and_condition_from_method_name(name_parts.pop)
|
319
|
+
if !column_detail.nil? && !condition_klass.nil?
|
320
|
+
# There were modifiers, lets get their real names
|
321
|
+
modifier_klasses = []
|
322
|
+
name_parts.each do |modifier_name|
|
323
|
+
size_before = modifier_klasses.size
|
324
|
+
self.class.modifiers.each do |modifier_klass|
|
325
|
+
if modifier_klass.modifier_names.include?(modifier_name)
|
326
|
+
modifier_klasses << modifier_klass
|
327
|
+
break
|
328
|
+
end
|
329
|
+
end
|
330
|
+
return if modifier_klasses.size == size_before # there was an invalid modifer, return nil for everything and let it act as a nomethoderror
|
331
|
+
end
|
332
|
+
|
333
|
+
return [modifier_klasses, column_detail, condition_klass]
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
nil
|
338
|
+
end
|
339
|
+
|
340
|
+
def build_method_name(modifier_klasses, column_name, condition_name)
|
341
|
+
modifier_name_parts = []
|
342
|
+
modifier_klasses.each { |modifier_klass| modifier_name_parts << modifier_klass.modifier_names.first }
|
343
|
+
method_name_parts = []
|
344
|
+
method_name_parts << modifier_name_parts.join("_of_") + "_of" unless modifier_name_parts.blank?
|
345
|
+
method_name_parts << column_name
|
346
|
+
method_name_parts << condition_name unless condition_name.blank?
|
347
|
+
method_name_parts.join("_")
|
348
|
+
end
|
349
|
+
|
350
|
+
def method_missing(name, *args, &block)
|
351
|
+
if setup_condition(name)
|
352
|
+
send(name, *args, &block)
|
353
|
+
else
|
354
|
+
super
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def setup_condition(name)
|
359
|
+
modifier_klasses, column_detail, condition_klass = breakdown_method_name(name.to_s)
|
360
|
+
if !column_detail.nil? && !condition_klass.nil?
|
361
|
+
method_name = build_method_name(modifier_klasses, column_detail[:column].name, condition_klass.condition_names_for_column.first)
|
362
|
+
|
363
|
+
if !added_condition?(method_name)
|
364
|
+
column_type = column_sql = nil
|
365
|
+
if !modifier_klasses.blank?
|
366
|
+
# Find the column type
|
367
|
+
column_type = modifier_klasses.first.return_type
|
368
|
+
|
369
|
+
# Build the column sql
|
370
|
+
column_sql = "{table}.{column}"
|
371
|
+
modifier_klasses.each do |modifier_klass|
|
372
|
+
next unless klass.connection.respond_to?(modifier_klass.adapter_method_name)
|
373
|
+
column_sql = klass.connection.send(modifier_klass.adapter_method_name, column_sql)
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
add_condition!(condition_klass, method_name, :column => column_detail[:column], :column_type => column_type, :column_sql_format => column_sql)
|
378
|
+
|
379
|
+
([column_detail[:column].name] + column_detail[:aliases]).each do |column_name|
|
380
|
+
condition_klass.condition_names_for_column.each do |condition_name|
|
381
|
+
alias_method_name = build_method_name(modifier_klasses, column_name, condition_name)
|
382
|
+
add_condition_alias!(alias_method_name, method_name) unless added_condition?(alias_method_name)
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
alias_method_name = name.to_s.gsub("=", "")
|
388
|
+
add_condition_alias!(alias_method_name, method_name) unless added_condition?(alias_method_name)
|
389
|
+
|
390
|
+
return true
|
391
|
+
end
|
392
|
+
|
393
|
+
false
|
394
|
+
end
|
395
|
+
|
396
|
+
def add_condition!(condition, name, options = {})
|
397
|
+
self.class.condition_names << name
|
398
|
+
options[:column] = options[:column].name if options[:column].class < ::ActiveRecord::ConnectionAdapters::Column
|
399
|
+
|
400
|
+
self.class.class_eval <<-"end_eval", __FILE__, __LINE__
|
401
|
+
def #{name}_object
|
402
|
+
if objects[:#{name}].nil?
|
403
|
+
options = {}
|
404
|
+
objects[:#{name}] = #{condition.name}.new(klass, #{options.inspect})
|
405
|
+
end
|
406
|
+
objects[:#{name}]
|
407
|
+
end
|
408
|
+
|
409
|
+
def #{name}; #{name}_object.value; end
|
410
|
+
|
411
|
+
def #{name}=(value)
|
412
|
+
@conditions = nil
|
413
|
+
|
414
|
+
#{name}_object.value = value
|
415
|
+
reset_#{name}! if #{name}_object.value_is_meaningless?
|
416
|
+
value
|
417
|
+
end
|
418
|
+
|
419
|
+
def reset_#{name}!; objects.delete(:#{name}); end
|
420
|
+
end_eval
|
421
|
+
end
|
422
|
+
|
423
|
+
def added_condition?(name)
|
424
|
+
respond_to?("#{name}_object") && respond_to?(name) && respond_to?("#{name}=") && respond_to?("reset_#{name}!")
|
425
|
+
end
|
426
|
+
|
427
|
+
def add_condition_alias!(alias_name, name)
|
428
|
+
self.class.condition_names << alias_name
|
429
|
+
|
430
|
+
self.class.class_eval do
|
431
|
+
alias_method "#{alias_name}_object", "#{name}_object"
|
432
|
+
alias_method alias_name, name
|
433
|
+
alias_method "#{alias_name}=", "#{name}="
|
434
|
+
alias_method "reset_#{alias_name}!", "reset_#{name}!"
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
def assert_valid_conditions(conditions)
|
439
|
+
conditions.each do |condition, value|
|
440
|
+
next if (self.class.condition_names + self.class.association_names + ["any"]).include?(condition.to_s)
|
441
|
+
|
442
|
+
go_to_next = false
|
443
|
+
self.class.column_details.each do |column_detail|
|
444
|
+
if column_detail[:column].name == condition.to_s || column_detail[:aliases].include?(condition.to_s)
|
445
|
+
go_to_next = true
|
446
|
+
break
|
447
|
+
end
|
448
|
+
end
|
449
|
+
next if go_to_next
|
450
|
+
|
451
|
+
next unless respond_to?(condition)
|
452
|
+
|
453
|
+
raise(ArgumentError, "The #{condition} condition is not a valid condition")
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
def associations
|
458
|
+
associations = {}
|
459
|
+
objects.each do |name, object|
|
460
|
+
associations[name] = object if object.class < ::Searchlogic::Conditions::Base
|
461
|
+
end
|
462
|
+
associations
|
463
|
+
end
|
464
|
+
|
465
|
+
def objects
|
466
|
+
@objects ||= {}
|
467
|
+
end
|
468
|
+
|
469
|
+
def reset_objects!
|
470
|
+
objects.each { |name, object| eval("@#{name} = nil") }
|
471
|
+
objects.clear
|
472
|
+
end
|
473
|
+
|
474
|
+
def remove_conditions_from_protected_assignement(conditions)
|
475
|
+
return conditions if klass.accessible_conditions.nil? && klass.protected_conditions.nil?
|
476
|
+
if klass.accessible_conditions
|
477
|
+
conditions.reject { |condition, value| !klass.accessible_conditions.include?(condition.to_s) }
|
478
|
+
elsif klass.protected_conditions
|
479
|
+
conditions.reject { |condition, value| klass.protected_conditions.include?(condition.to_s) }
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
483
|
+
end
|
484
|
+
end
|