searchlogic 1.5.3

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.
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