searchlogic 1.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. data/CHANGELOG.rdoc +228 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Manifest +123 -0
  4. data/README.rdoc +383 -0
  5. data/Rakefile +15 -0
  6. data/TODO.rdoc +6 -0
  7. data/examples/README.rdoc +4 -0
  8. data/init.rb +1 -0
  9. data/lib/searchlogic.rb +89 -0
  10. data/lib/searchlogic/active_record/associations.rb +52 -0
  11. data/lib/searchlogic/active_record/base.rb +218 -0
  12. data/lib/searchlogic/active_record/connection_adapters/mysql_adapter.rb +172 -0
  13. data/lib/searchlogic/active_record/connection_adapters/postgresql_adapter.rb +168 -0
  14. data/lib/searchlogic/active_record/connection_adapters/sqlite_adapter.rb +75 -0
  15. data/lib/searchlogic/condition/base.rb +159 -0
  16. data/lib/searchlogic/condition/begins_with.rb +17 -0
  17. data/lib/searchlogic/condition/blank.rb +21 -0
  18. data/lib/searchlogic/condition/child_of.rb +11 -0
  19. data/lib/searchlogic/condition/descendant_of.rb +24 -0
  20. data/lib/searchlogic/condition/ends_with.rb +17 -0
  21. data/lib/searchlogic/condition/equals.rb +27 -0
  22. data/lib/searchlogic/condition/greater_than.rb +15 -0
  23. data/lib/searchlogic/condition/greater_than_or_equal_to.rb +15 -0
  24. data/lib/searchlogic/condition/inclusive_descendant_of.rb +11 -0
  25. data/lib/searchlogic/condition/keywords.rb +47 -0
  26. data/lib/searchlogic/condition/less_than.rb +15 -0
  27. data/lib/searchlogic/condition/less_than_or_equal_to.rb +15 -0
  28. data/lib/searchlogic/condition/like.rb +15 -0
  29. data/lib/searchlogic/condition/nil.rb +21 -0
  30. data/lib/searchlogic/condition/not_begin_with.rb +20 -0
  31. data/lib/searchlogic/condition/not_blank.rb +19 -0
  32. data/lib/searchlogic/condition/not_end_with.rb +20 -0
  33. data/lib/searchlogic/condition/not_equal.rb +26 -0
  34. data/lib/searchlogic/condition/not_have_keywords.rb +20 -0
  35. data/lib/searchlogic/condition/not_like.rb +20 -0
  36. data/lib/searchlogic/condition/not_nil.rb +19 -0
  37. data/lib/searchlogic/condition/sibling_of.rb +14 -0
  38. data/lib/searchlogic/condition/tree.rb +17 -0
  39. data/lib/searchlogic/conditions/base.rb +484 -0
  40. data/lib/searchlogic/conditions/protection.rb +36 -0
  41. data/lib/searchlogic/config.rb +31 -0
  42. data/lib/searchlogic/config/helpers.rb +289 -0
  43. data/lib/searchlogic/config/search.rb +53 -0
  44. data/lib/searchlogic/core_ext/hash.rb +75 -0
  45. data/lib/searchlogic/helpers/control_types/link.rb +310 -0
  46. data/lib/searchlogic/helpers/control_types/links.rb +241 -0
  47. data/lib/searchlogic/helpers/control_types/remote_link.rb +87 -0
  48. data/lib/searchlogic/helpers/control_types/remote_links.rb +72 -0
  49. data/lib/searchlogic/helpers/control_types/remote_select.rb +36 -0
  50. data/lib/searchlogic/helpers/control_types/select.rb +82 -0
  51. data/lib/searchlogic/helpers/form.rb +208 -0
  52. data/lib/searchlogic/helpers/utilities.rb +197 -0
  53. data/lib/searchlogic/modifiers/absolute.rb +15 -0
  54. data/lib/searchlogic/modifiers/acos.rb +11 -0
  55. data/lib/searchlogic/modifiers/asin.rb +11 -0
  56. data/lib/searchlogic/modifiers/atan.rb +11 -0
  57. data/lib/searchlogic/modifiers/base.rb +27 -0
  58. data/lib/searchlogic/modifiers/ceil.rb +15 -0
  59. data/lib/searchlogic/modifiers/char_length.rb +15 -0
  60. data/lib/searchlogic/modifiers/cos.rb +15 -0
  61. data/lib/searchlogic/modifiers/cot.rb +15 -0
  62. data/lib/searchlogic/modifiers/day_of_month.rb +15 -0
  63. data/lib/searchlogic/modifiers/day_of_week.rb +15 -0
  64. data/lib/searchlogic/modifiers/day_of_year.rb +15 -0
  65. data/lib/searchlogic/modifiers/degrees.rb +11 -0
  66. data/lib/searchlogic/modifiers/exp.rb +15 -0
  67. data/lib/searchlogic/modifiers/floor.rb +15 -0
  68. data/lib/searchlogic/modifiers/hex.rb +11 -0
  69. data/lib/searchlogic/modifiers/hour.rb +11 -0
  70. data/lib/searchlogic/modifiers/log.rb +15 -0
  71. data/lib/searchlogic/modifiers/log10.rb +11 -0
  72. data/lib/searchlogic/modifiers/log2.rb +11 -0
  73. data/lib/searchlogic/modifiers/lower.rb +15 -0
  74. data/lib/searchlogic/modifiers/ltrim.rb +15 -0
  75. data/lib/searchlogic/modifiers/md5.rb +11 -0
  76. data/lib/searchlogic/modifiers/microseconds.rb +11 -0
  77. data/lib/searchlogic/modifiers/milliseconds.rb +11 -0
  78. data/lib/searchlogic/modifiers/minute.rb +15 -0
  79. data/lib/searchlogic/modifiers/month.rb +15 -0
  80. data/lib/searchlogic/modifiers/octal.rb +15 -0
  81. data/lib/searchlogic/modifiers/radians.rb +11 -0
  82. data/lib/searchlogic/modifiers/round.rb +11 -0
  83. data/lib/searchlogic/modifiers/rtrim.rb +15 -0
  84. data/lib/searchlogic/modifiers/second.rb +15 -0
  85. data/lib/searchlogic/modifiers/sign.rb +11 -0
  86. data/lib/searchlogic/modifiers/sin.rb +11 -0
  87. data/lib/searchlogic/modifiers/square_root.rb +15 -0
  88. data/lib/searchlogic/modifiers/tan.rb +15 -0
  89. data/lib/searchlogic/modifiers/trim.rb +15 -0
  90. data/lib/searchlogic/modifiers/upper.rb +15 -0
  91. data/lib/searchlogic/modifiers/week.rb +11 -0
  92. data/lib/searchlogic/modifiers/year.rb +11 -0
  93. data/lib/searchlogic/search/base.rb +148 -0
  94. data/lib/searchlogic/search/conditions.rb +53 -0
  95. data/lib/searchlogic/search/ordering.rb +244 -0
  96. data/lib/searchlogic/search/pagination.rb +121 -0
  97. data/lib/searchlogic/search/protection.rb +89 -0
  98. data/lib/searchlogic/search/searching.rb +31 -0
  99. data/lib/searchlogic/shared/utilities.rb +50 -0
  100. data/lib/searchlogic/shared/virtual_classes.rb +39 -0
  101. data/lib/searchlogic/version.rb +79 -0
  102. data/searchlogic.gemspec +39 -0
  103. data/test/fixtures/accounts.yml +15 -0
  104. data/test/fixtures/cats.yml +3 -0
  105. data/test/fixtures/dogs.yml +3 -0
  106. data/test/fixtures/orders.yml +14 -0
  107. data/test/fixtures/user_groups.yml +13 -0
  108. data/test/fixtures/users.yml +36 -0
  109. data/test/test_active_record_associations.rb +81 -0
  110. data/test/test_active_record_base.rb +93 -0
  111. data/test/test_condition_base.rb +52 -0
  112. data/test/test_condition_types.rb +143 -0
  113. data/test/test_conditions_base.rb +242 -0
  114. data/test/test_conditions_protection.rb +16 -0
  115. data/test/test_config.rb +23 -0
  116. data/test/test_helper.rb +134 -0
  117. data/test/test_search_base.rb +227 -0
  118. data/test/test_search_conditions.rb +19 -0
  119. data/test/test_search_ordering.rb +165 -0
  120. data/test/test_search_pagination.rb +72 -0
  121. data/test/test_search_protection.rb +24 -0
  122. data/test_libs/acts_as_tree.rb +98 -0
  123. data/test_libs/ordered_hash.rb +9 -0
  124. data/test_libs/rexml_fix.rb +14 -0
  125. 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 GreaterThan < Base
4
+ class << self
5
+ def condition_names_for_column
6
+ super + ["gt", "after"]
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 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 LessThan < Base
4
+ class << self
5
+ def condition_names_for_column
6
+ super + ["lt", "before"]
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 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