searchlogic 1.5.10 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc CHANGED
@@ -1,4 +1,10 @@
1
- == 1.5.10 released 2008-12-10
1
+ == 1.6.0 released 2008-12-8
2
+
3
+ * Converted all tree conditions to nested set conditions for performance reasons. A nested set data structure is now required instead of a tree
4
+ * Added DISTINCT to select if you are not provided a select, searching, and have joins.
5
+ * Included :not_like in the list of conditions to be required upon initialization.
6
+
7
+ == 1.5.10 released 2008-12-8
2
8
 
3
9
  * Create class level conditions upon instantiation to avoid conflicts.
4
10
 
data/Manifest CHANGED
@@ -19,6 +19,7 @@ lib/searchlogic/condition/keywords.rb
19
19
  lib/searchlogic/condition/less_than.rb
20
20
  lib/searchlogic/condition/less_than_or_equal_to.rb
21
21
  lib/searchlogic/condition/like.rb
22
+ lib/searchlogic/condition/nested_set.rb
22
23
  lib/searchlogic/condition/nil.rb
23
24
  lib/searchlogic/condition/not_begin_with.rb
24
25
  lib/searchlogic/condition/not_blank.rb
@@ -28,7 +29,6 @@ lib/searchlogic/condition/not_have_keywords.rb
28
29
  lib/searchlogic/condition/not_like.rb
29
30
  lib/searchlogic/condition/not_nil.rb
30
31
  lib/searchlogic/condition/sibling_of.rb
31
- lib/searchlogic/condition/tree.rb
32
32
  lib/searchlogic/conditions/any_or_all.rb
33
33
  lib/searchlogic/conditions/base.rb
34
34
  lib/searchlogic/conditions/groups.rb
@@ -142,7 +142,10 @@ test/fixtures/animals.yml
142
142
  test/fixtures/orders.yml
143
143
  test/fixtures/user_groups.yml
144
144
  test/fixtures/users.yml
145
- test/libs/acts_as_tree.rb
145
+ test/libs/awesome_nested_set/compatability.rb
146
+ test/libs/awesome_nested_set/helper.rb
147
+ test/libs/awesome_nested_set/named_scope.rb
148
+ test/libs/awesome_nested_set.rb
146
149
  test/libs/rexml_fix.rb
147
150
  test/modifier_tests/day_of_month_test.rb
148
151
  test/search_tests/base_test.rb
data/README.rdoc CHANGED
@@ -347,6 +347,8 @@ Each model comes preloaded with class level conditions as well. The difference i
347
347
  :inclusive_descendant_of Same as above but also includes the root
348
348
  :sibling_of Returns all records that have the same parent
349
349
 
350
+ The above conditions are for a nested set data structure, *not* a tree. Since we need to traverse through the tree a nested set is required for performance reasons. Traversing through a large tree is very bad for performance, a nested set solves this problem.
351
+
350
352
  == Modifiers
351
353
 
352
354
  === What are modifiers?
data/lib/searchlogic.rb CHANGED
@@ -48,13 +48,15 @@ require "searchlogic/conditions/base"
48
48
 
49
49
  # Condition
50
50
  require "searchlogic/condition/base"
51
- require "searchlogic/condition/tree"
52
- SEARCHLOGIC_CONDITIONS = [:begins_with, :blank, :child_of, :descendant_of, :ends_with, :equals, :greater_than, :greater_than_or_equal_to, :inclusive_descendant_of, :like, :nil, :not_begin_with, :not_blank, :not_end_with, :not_equal, :not_have_keywords, :not_nil, :keywords, :less_than, :less_than_or_equal_to, :sibling_of]
51
+ require "searchlogic/condition/nested_set"
52
+ SEARCHLOGIC_CONDITIONS = [:begins_with, :blank, :child_of, :descendant_of, :ends_with, :equals, :greater_than, :greater_than_or_equal_to, :inclusive_descendant_of, :like, :nil, :not_begin_with,
53
+ :not_blank, :not_end_with, :not_equal, :not_have_keywords, :not_like, :not_nil, :keywords, :less_than, :less_than_or_equal_to, :sibling_of]
53
54
  SEARCHLOGIC_CONDITIONS.each { |condition| require "searchlogic/condition/#{condition}" }
54
55
 
55
56
  # Modifiers
56
57
  require "searchlogic/modifiers/base"
57
- SEARCHLOGIC_MODIFIERS = [:absolute, :acos, :asin, :atan, :avg, :ceil, :char_length, :cos, :cot, :count, :day_of_month, :day_of_week, :day_of_year, :degrees, :exp, :floor, :hex, :hour, :log, :log10, :log2, :lower, :ltrim, :md5, :microseconds, :milliseconds, :minute, :month, :octal, :radians, :round, :rtrim, :second, :sign, :sin, :square_root, :sum, :tan, :trim, :upper, :week, :year]
58
+ SEARCHLOGIC_MODIFIERS = [:absolute, :acos, :asin, :atan, :avg, :ceil, :char_length, :cos, :cot, :count, :day_of_month, :day_of_week, :day_of_year, :degrees, :exp, :floor, :hex, :hour, :log, :log10,
59
+ :log2, :lower, :ltrim, :md5, :microseconds, :milliseconds, :minute, :month, :octal, :radians, :round, :rtrim, :second, :sign, :sin, :square_root, :sum, :tan, :trim, :upper, :week, :year]
58
60
  SEARCHLOGIC_MODIFIERS.each { |modifier| require "searchlogic/modifiers/#{modifier}" }
59
61
 
60
62
  # Helpers
@@ -121,8 +121,7 @@ module Searchlogic
121
121
  def meaningless?(v)
122
122
  case v
123
123
  when Array
124
- v.each { |i| return false unless meaningless?(i) }
125
- true
124
+ false
126
125
  else
127
126
  !explicitly_set_value? || (self.class.ignore_meaningless_value? && v != false && v.blank?)
128
127
  end
@@ -1,6 +1,6 @@
1
1
  module Searchlogic
2
2
  module Condition
3
- class ChildOf < Tree
3
+ class ChildOf < NestedSet
4
4
  def to_conditions(value)
5
5
  parent_association = klass.reflect_on_association(:parent)
6
6
  foreign_key_name = (parent_association && parent_association.options[:foreign_key]) || "parent_id"
@@ -1,24 +1,11 @@
1
1
  module Searchlogic
2
2
  module Condition
3
- class DescendantOf < Tree
3
+ class DescendantOf < NestedSet
4
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]
5
+ condition = InclusiveDescendantOf.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)
14
8
  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
9
  end
23
10
  end
24
11
  end
@@ -1,10 +1,9 @@
1
1
  module Searchlogic
2
2
  module Condition
3
- class InclusiveDescendantOf < Tree
3
+ class InclusiveDescendantOf < NestedSet
4
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)
5
+ root = (value.is_a?(klass) ? value : klass.find(value)) rescue return
6
+ ["#{quoted_table_name}.#{quote_column_name(klass.left_column_name)} >= ? AND #{quoted_table_name}.#{quote_column_name(klass.right_column_name)} <= ?", root.left, root.right]
8
7
  end
9
8
  end
10
9
  end
@@ -1,6 +1,6 @@
1
1
  module Searchlogic
2
2
  module Condition
3
- class Tree < Base # :nodoc:
3
+ class NestedSet < Base # :nodoc:
4
4
  self.join_arrays_with_or = true
5
5
 
6
6
  class << self
@@ -8,7 +8,7 @@ module Searchlogic
8
8
  end
9
9
 
10
10
  def to_conditions(value)
11
- begin_with = BeginWith.new(klass, options)
11
+ begin_with = BeginsWith.new(klass, options)
12
12
  begin_with.value = value
13
13
  conditions = being_with.sanitize
14
14
  return conditions if conditions.blank?
@@ -1,6 +1,6 @@
1
1
  module Searchlogic
2
2
  module Condition
3
- class SiblingOf < Tree
3
+ class SiblingOf < NestedSet
4
4
  def to_conditions(value)
5
5
  parent_association = klass.reflect_on_association(:parent)
6
6
  foreign_key_name = (parent_association && parent_association.options[:foreign_key]) || "parent_id"
@@ -91,7 +91,7 @@ module Searchlogic
91
91
  class_level_conditions.each do |condition_class|
92
92
  condition_class.condition_names_for_model.each_with_index do |condition_name, index|
93
93
  if index == 0
94
- add_condition!(condition_class, condition_name)
94
+ add_condition!(condition_class, condition_name, :column => klass.columns_hash[klass.primary_key])
95
95
  else
96
96
  add_condition_alias!(condition_name, condition_class.condition_names_for_model.first)
97
97
  end
@@ -119,6 +119,11 @@ module Searchlogic #:nodoc:
119
119
  find_options
120
120
  end
121
121
 
122
+ def select
123
+ return @select if klass.connection.adapter_name == "PostgreSQL" # Postgres needs ALL of the column names here, including association columns, etc. Which is very strange, so I disable this feature for Postgres all together.
124
+ @select ||= "DISTINCT #{klass.connection.quote_table_name(klass.table_name)}.*" if !joins.blank? && Config.search.remove_duplicates?
125
+ end
126
+
122
127
  def scope
123
128
  @scope ||= {}
124
129
  end
@@ -22,7 +22,6 @@ module Searchlogic
22
22
  end
23
23
  args << options
24
24
  results = klass.#{method}(*args)
25
- results.uniq! if #{SEARCH_METHODS.include?(method)} && results.is_a?(Array) && !joins.blank? && Config.search.remove_duplicates?
26
25
  results
27
26
  end
28
27
  end
@@ -66,8 +66,8 @@ module Searchlogic
66
66
  end
67
67
 
68
68
  MAJOR = 1
69
- MINOR = 5
70
- TINY = 10
69
+ MINOR = 6
70
+ TINY = 0
71
71
 
72
72
  # The current version as a Version instance
73
73
  CURRENT = new(MAJOR, MINOR, TINY)
data/searchlogic.gemspec CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{searchlogic}
5
- s.version = "1.5.10"
5
+ s.version = "1.6.0"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Ben Johnson of Binary Logic"]
9
- s.date = %q{2008-12-07}
9
+ s.date = %q{2008-12-08}
10
10
  s.description = %q{Object based ActiveRecord searching, ordering, pagination, and more!}
11
11
  s.email = %q{bjohnson@binarylogic.com}
12
- s.extra_rdoc_files = ["CHANGELOG.rdoc", "lib/searchlogic/active_record/associations.rb", "lib/searchlogic/active_record/base.rb", "lib/searchlogic/active_record/connection_adapters/mysql_adapter.rb", "lib/searchlogic/active_record/connection_adapters/postgresql_adapter.rb", "lib/searchlogic/active_record/connection_adapters/sqlite_adapter.rb", "lib/searchlogic/condition/base.rb", "lib/searchlogic/condition/begins_with.rb", "lib/searchlogic/condition/blank.rb", "lib/searchlogic/condition/child_of.rb", "lib/searchlogic/condition/descendant_of.rb", "lib/searchlogic/condition/ends_with.rb", "lib/searchlogic/condition/equals.rb", "lib/searchlogic/condition/greater_than.rb", "lib/searchlogic/condition/greater_than_or_equal_to.rb", "lib/searchlogic/condition/inclusive_descendant_of.rb", "lib/searchlogic/condition/keywords.rb", "lib/searchlogic/condition/less_than.rb", "lib/searchlogic/condition/less_than_or_equal_to.rb", "lib/searchlogic/condition/like.rb", "lib/searchlogic/condition/nil.rb", "lib/searchlogic/condition/not_begin_with.rb", "lib/searchlogic/condition/not_blank.rb", "lib/searchlogic/condition/not_end_with.rb", "lib/searchlogic/condition/not_equal.rb", "lib/searchlogic/condition/not_have_keywords.rb", "lib/searchlogic/condition/not_like.rb", "lib/searchlogic/condition/not_nil.rb", "lib/searchlogic/condition/sibling_of.rb", "lib/searchlogic/condition/tree.rb", "lib/searchlogic/conditions/any_or_all.rb", "lib/searchlogic/conditions/base.rb", "lib/searchlogic/conditions/groups.rb", "lib/searchlogic/conditions/magic_methods.rb", "lib/searchlogic/conditions/multiparameter_attributes.rb", "lib/searchlogic/conditions/protection.rb", "lib/searchlogic/config/helpers.rb", "lib/searchlogic/config/search.rb", "lib/searchlogic/config.rb", "lib/searchlogic/core_ext/hash.rb", "lib/searchlogic/core_ext/object.rb", "lib/searchlogic/helpers/control_types/link.rb", "lib/searchlogic/helpers/control_types/links.rb", "lib/searchlogic/helpers/control_types/remote_link.rb", "lib/searchlogic/helpers/control_types/remote_links.rb", "lib/searchlogic/helpers/control_types/remote_select.rb", "lib/searchlogic/helpers/control_types/select.rb", "lib/searchlogic/helpers/form.rb", "lib/searchlogic/helpers/utilities.rb", "lib/searchlogic/modifiers/absolute.rb", "lib/searchlogic/modifiers/acos.rb", "lib/searchlogic/modifiers/asin.rb", "lib/searchlogic/modifiers/atan.rb", "lib/searchlogic/modifiers/avg.rb", "lib/searchlogic/modifiers/base.rb", "lib/searchlogic/modifiers/ceil.rb", "lib/searchlogic/modifiers/char_length.rb", "lib/searchlogic/modifiers/cos.rb", "lib/searchlogic/modifiers/cot.rb", "lib/searchlogic/modifiers/count.rb", "lib/searchlogic/modifiers/day_of_month.rb", "lib/searchlogic/modifiers/day_of_week.rb", "lib/searchlogic/modifiers/day_of_year.rb", "lib/searchlogic/modifiers/degrees.rb", "lib/searchlogic/modifiers/exp.rb", "lib/searchlogic/modifiers/floor.rb", "lib/searchlogic/modifiers/hex.rb", "lib/searchlogic/modifiers/hour.rb", "lib/searchlogic/modifiers/log.rb", "lib/searchlogic/modifiers/log10.rb", "lib/searchlogic/modifiers/log2.rb", "lib/searchlogic/modifiers/lower.rb", "lib/searchlogic/modifiers/ltrim.rb", "lib/searchlogic/modifiers/md5.rb", "lib/searchlogic/modifiers/microseconds.rb", "lib/searchlogic/modifiers/milliseconds.rb", "lib/searchlogic/modifiers/minute.rb", "lib/searchlogic/modifiers/month.rb", "lib/searchlogic/modifiers/octal.rb", "lib/searchlogic/modifiers/radians.rb", "lib/searchlogic/modifiers/round.rb", "lib/searchlogic/modifiers/rtrim.rb", "lib/searchlogic/modifiers/second.rb", "lib/searchlogic/modifiers/sign.rb", "lib/searchlogic/modifiers/sin.rb", "lib/searchlogic/modifiers/square_root.rb", "lib/searchlogic/modifiers/sum.rb", "lib/searchlogic/modifiers/tan.rb", "lib/searchlogic/modifiers/trim.rb", "lib/searchlogic/modifiers/upper.rb", "lib/searchlogic/modifiers/week.rb", "lib/searchlogic/modifiers/year.rb", "lib/searchlogic/search/base.rb", "lib/searchlogic/search/conditions.rb", "lib/searchlogic/search/ordering.rb", "lib/searchlogic/search/pagination.rb", "lib/searchlogic/search/protection.rb", "lib/searchlogic/search/searching.rb", "lib/searchlogic/shared/utilities.rb", "lib/searchlogic/shared/virtual_classes.rb", "lib/searchlogic/version.rb", "lib/searchlogic.rb", "README.rdoc", "TODO.rdoc"]
13
- s.files = ["CHANGELOG.rdoc", "init.rb", "lib/searchlogic/active_record/associations.rb", "lib/searchlogic/active_record/base.rb", "lib/searchlogic/active_record/connection_adapters/mysql_adapter.rb", "lib/searchlogic/active_record/connection_adapters/postgresql_adapter.rb", "lib/searchlogic/active_record/connection_adapters/sqlite_adapter.rb", "lib/searchlogic/condition/base.rb", "lib/searchlogic/condition/begins_with.rb", "lib/searchlogic/condition/blank.rb", "lib/searchlogic/condition/child_of.rb", "lib/searchlogic/condition/descendant_of.rb", "lib/searchlogic/condition/ends_with.rb", "lib/searchlogic/condition/equals.rb", "lib/searchlogic/condition/greater_than.rb", "lib/searchlogic/condition/greater_than_or_equal_to.rb", "lib/searchlogic/condition/inclusive_descendant_of.rb", "lib/searchlogic/condition/keywords.rb", "lib/searchlogic/condition/less_than.rb", "lib/searchlogic/condition/less_than_or_equal_to.rb", "lib/searchlogic/condition/like.rb", "lib/searchlogic/condition/nil.rb", "lib/searchlogic/condition/not_begin_with.rb", "lib/searchlogic/condition/not_blank.rb", "lib/searchlogic/condition/not_end_with.rb", "lib/searchlogic/condition/not_equal.rb", "lib/searchlogic/condition/not_have_keywords.rb", "lib/searchlogic/condition/not_like.rb", "lib/searchlogic/condition/not_nil.rb", "lib/searchlogic/condition/sibling_of.rb", "lib/searchlogic/condition/tree.rb", "lib/searchlogic/conditions/any_or_all.rb", "lib/searchlogic/conditions/base.rb", "lib/searchlogic/conditions/groups.rb", "lib/searchlogic/conditions/magic_methods.rb", "lib/searchlogic/conditions/multiparameter_attributes.rb", "lib/searchlogic/conditions/protection.rb", "lib/searchlogic/config/helpers.rb", "lib/searchlogic/config/search.rb", "lib/searchlogic/config.rb", "lib/searchlogic/core_ext/hash.rb", "lib/searchlogic/core_ext/object.rb", "lib/searchlogic/helpers/control_types/link.rb", "lib/searchlogic/helpers/control_types/links.rb", "lib/searchlogic/helpers/control_types/remote_link.rb", "lib/searchlogic/helpers/control_types/remote_links.rb", "lib/searchlogic/helpers/control_types/remote_select.rb", "lib/searchlogic/helpers/control_types/select.rb", "lib/searchlogic/helpers/form.rb", "lib/searchlogic/helpers/utilities.rb", "lib/searchlogic/modifiers/absolute.rb", "lib/searchlogic/modifiers/acos.rb", "lib/searchlogic/modifiers/asin.rb", "lib/searchlogic/modifiers/atan.rb", "lib/searchlogic/modifiers/avg.rb", "lib/searchlogic/modifiers/base.rb", "lib/searchlogic/modifiers/ceil.rb", "lib/searchlogic/modifiers/char_length.rb", "lib/searchlogic/modifiers/cos.rb", "lib/searchlogic/modifiers/cot.rb", "lib/searchlogic/modifiers/count.rb", "lib/searchlogic/modifiers/day_of_month.rb", "lib/searchlogic/modifiers/day_of_week.rb", "lib/searchlogic/modifiers/day_of_year.rb", "lib/searchlogic/modifiers/degrees.rb", "lib/searchlogic/modifiers/exp.rb", "lib/searchlogic/modifiers/floor.rb", "lib/searchlogic/modifiers/hex.rb", "lib/searchlogic/modifiers/hour.rb", "lib/searchlogic/modifiers/log.rb", "lib/searchlogic/modifiers/log10.rb", "lib/searchlogic/modifiers/log2.rb", "lib/searchlogic/modifiers/lower.rb", "lib/searchlogic/modifiers/ltrim.rb", "lib/searchlogic/modifiers/md5.rb", "lib/searchlogic/modifiers/microseconds.rb", "lib/searchlogic/modifiers/milliseconds.rb", "lib/searchlogic/modifiers/minute.rb", "lib/searchlogic/modifiers/month.rb", "lib/searchlogic/modifiers/octal.rb", "lib/searchlogic/modifiers/radians.rb", "lib/searchlogic/modifiers/round.rb", "lib/searchlogic/modifiers/rtrim.rb", "lib/searchlogic/modifiers/second.rb", "lib/searchlogic/modifiers/sign.rb", "lib/searchlogic/modifiers/sin.rb", "lib/searchlogic/modifiers/square_root.rb", "lib/searchlogic/modifiers/sum.rb", "lib/searchlogic/modifiers/tan.rb", "lib/searchlogic/modifiers/trim.rb", "lib/searchlogic/modifiers/upper.rb", "lib/searchlogic/modifiers/week.rb", "lib/searchlogic/modifiers/year.rb", "lib/searchlogic/search/base.rb", "lib/searchlogic/search/conditions.rb", "lib/searchlogic/search/ordering.rb", "lib/searchlogic/search/pagination.rb", "lib/searchlogic/search/protection.rb", "lib/searchlogic/search/searching.rb", "lib/searchlogic/shared/utilities.rb", "lib/searchlogic/shared/virtual_classes.rb", "lib/searchlogic/version.rb", "lib/searchlogic.rb", "Manifest", "MIT-LICENSE", "Rakefile", "README.rdoc", "test/active_record_tests/associations_test.rb", "test/active_record_tests/base_test.rb", "test/condition_tests/base_test.rb", "test/condition_tests/begins_with_test.rb", "test/condition_tests/blank_test.rb", "test/condition_tests/child_of_test.rb", "test/condition_tests/descendant_of_test.rb", "test/condition_tests/ends_with_test.rb", "test/condition_tests/equals_test.rb", "test/condition_tests/greater_than_or_equal_to_test.rb", "test/condition_tests/greater_than_test.rb", "test/condition_tests/inclusive_descendant_of_test.rb", "test/condition_tests/keywords_test.rb", "test/condition_tests/less_than_or_equal_to_test.rb", "test/condition_tests/less_than_test.rb", "test/condition_tests/like_test.rb", "test/condition_tests/nil_test.rb", "test/condition_tests/not_begin_with_test.rb", "test/condition_tests/not_blank_test.rb", "test/condition_tests/not_end_with_test.rb", "test/condition_tests/not_equal_test.rb", "test/condition_tests/not_have_keywords_test.rb", "test/condition_tests/not_like_test.rb", "test/condition_tests/not_nil_test.rb", "test/condition_tests/sibling_of_test.rb", "test/conditions_tests/any_or_all_test.rb", "test/conditions_tests/base_test.rb", "test/conditions_tests/groups_test.rb", "test/conditions_tests/magic_methods_test.rb", "test/conditions_tests/multiparameter_attributes_test.rb", "test/conditions_tests/protection_test.rb", "test/config_test.rb", "test/fixtures/accounts.yml", "test/fixtures/animals.yml", "test/fixtures/orders.yml", "test/fixtures/user_groups.yml", "test/fixtures/users.yml", "test/libs/acts_as_tree.rb", "test/libs/rexml_fix.rb", "test/modifier_tests/day_of_month_test.rb", "test/search_tests/base_test.rb", "test/search_tests/conditions_test.rb", "test/search_tests/ordering_test.rb", "test/search_tests/pagination_test.rb", "test/search_tests/protection_test.rb", "test/test_helper.rb", "TODO.rdoc", "searchlogic.gemspec"]
12
+ s.extra_rdoc_files = ["CHANGELOG.rdoc", "lib/searchlogic/active_record/associations.rb", "lib/searchlogic/active_record/base.rb", "lib/searchlogic/active_record/connection_adapters/mysql_adapter.rb", "lib/searchlogic/active_record/connection_adapters/postgresql_adapter.rb", "lib/searchlogic/active_record/connection_adapters/sqlite_adapter.rb", "lib/searchlogic/condition/base.rb", "lib/searchlogic/condition/begins_with.rb", "lib/searchlogic/condition/blank.rb", "lib/searchlogic/condition/child_of.rb", "lib/searchlogic/condition/descendant_of.rb", "lib/searchlogic/condition/ends_with.rb", "lib/searchlogic/condition/equals.rb", "lib/searchlogic/condition/greater_than.rb", "lib/searchlogic/condition/greater_than_or_equal_to.rb", "lib/searchlogic/condition/inclusive_descendant_of.rb", "lib/searchlogic/condition/keywords.rb", "lib/searchlogic/condition/less_than.rb", "lib/searchlogic/condition/less_than_or_equal_to.rb", "lib/searchlogic/condition/like.rb", "lib/searchlogic/condition/nested_set.rb", "lib/searchlogic/condition/nil.rb", "lib/searchlogic/condition/not_begin_with.rb", "lib/searchlogic/condition/not_blank.rb", "lib/searchlogic/condition/not_end_with.rb", "lib/searchlogic/condition/not_equal.rb", "lib/searchlogic/condition/not_have_keywords.rb", "lib/searchlogic/condition/not_like.rb", "lib/searchlogic/condition/not_nil.rb", "lib/searchlogic/condition/sibling_of.rb", "lib/searchlogic/conditions/any_or_all.rb", "lib/searchlogic/conditions/base.rb", "lib/searchlogic/conditions/groups.rb", "lib/searchlogic/conditions/magic_methods.rb", "lib/searchlogic/conditions/multiparameter_attributes.rb", "lib/searchlogic/conditions/protection.rb", "lib/searchlogic/config/helpers.rb", "lib/searchlogic/config/search.rb", "lib/searchlogic/config.rb", "lib/searchlogic/core_ext/hash.rb", "lib/searchlogic/core_ext/object.rb", "lib/searchlogic/helpers/control_types/link.rb", "lib/searchlogic/helpers/control_types/links.rb", "lib/searchlogic/helpers/control_types/remote_link.rb", "lib/searchlogic/helpers/control_types/remote_links.rb", "lib/searchlogic/helpers/control_types/remote_select.rb", "lib/searchlogic/helpers/control_types/select.rb", "lib/searchlogic/helpers/form.rb", "lib/searchlogic/helpers/utilities.rb", "lib/searchlogic/modifiers/absolute.rb", "lib/searchlogic/modifiers/acos.rb", "lib/searchlogic/modifiers/asin.rb", "lib/searchlogic/modifiers/atan.rb", "lib/searchlogic/modifiers/avg.rb", "lib/searchlogic/modifiers/base.rb", "lib/searchlogic/modifiers/ceil.rb", "lib/searchlogic/modifiers/char_length.rb", "lib/searchlogic/modifiers/cos.rb", "lib/searchlogic/modifiers/cot.rb", "lib/searchlogic/modifiers/count.rb", "lib/searchlogic/modifiers/day_of_month.rb", "lib/searchlogic/modifiers/day_of_week.rb", "lib/searchlogic/modifiers/day_of_year.rb", "lib/searchlogic/modifiers/degrees.rb", "lib/searchlogic/modifiers/exp.rb", "lib/searchlogic/modifiers/floor.rb", "lib/searchlogic/modifiers/hex.rb", "lib/searchlogic/modifiers/hour.rb", "lib/searchlogic/modifiers/log.rb", "lib/searchlogic/modifiers/log10.rb", "lib/searchlogic/modifiers/log2.rb", "lib/searchlogic/modifiers/lower.rb", "lib/searchlogic/modifiers/ltrim.rb", "lib/searchlogic/modifiers/md5.rb", "lib/searchlogic/modifiers/microseconds.rb", "lib/searchlogic/modifiers/milliseconds.rb", "lib/searchlogic/modifiers/minute.rb", "lib/searchlogic/modifiers/month.rb", "lib/searchlogic/modifiers/octal.rb", "lib/searchlogic/modifiers/radians.rb", "lib/searchlogic/modifiers/round.rb", "lib/searchlogic/modifiers/rtrim.rb", "lib/searchlogic/modifiers/second.rb", "lib/searchlogic/modifiers/sign.rb", "lib/searchlogic/modifiers/sin.rb", "lib/searchlogic/modifiers/square_root.rb", "lib/searchlogic/modifiers/sum.rb", "lib/searchlogic/modifiers/tan.rb", "lib/searchlogic/modifiers/trim.rb", "lib/searchlogic/modifiers/upper.rb", "lib/searchlogic/modifiers/week.rb", "lib/searchlogic/modifiers/year.rb", "lib/searchlogic/search/base.rb", "lib/searchlogic/search/conditions.rb", "lib/searchlogic/search/ordering.rb", "lib/searchlogic/search/pagination.rb", "lib/searchlogic/search/protection.rb", "lib/searchlogic/search/searching.rb", "lib/searchlogic/shared/utilities.rb", "lib/searchlogic/shared/virtual_classes.rb", "lib/searchlogic/version.rb", "lib/searchlogic.rb", "README.rdoc", "TODO.rdoc"]
13
+ s.files = ["CHANGELOG.rdoc", "init.rb", "lib/searchlogic/active_record/associations.rb", "lib/searchlogic/active_record/base.rb", "lib/searchlogic/active_record/connection_adapters/mysql_adapter.rb", "lib/searchlogic/active_record/connection_adapters/postgresql_adapter.rb", "lib/searchlogic/active_record/connection_adapters/sqlite_adapter.rb", "lib/searchlogic/condition/base.rb", "lib/searchlogic/condition/begins_with.rb", "lib/searchlogic/condition/blank.rb", "lib/searchlogic/condition/child_of.rb", "lib/searchlogic/condition/descendant_of.rb", "lib/searchlogic/condition/ends_with.rb", "lib/searchlogic/condition/equals.rb", "lib/searchlogic/condition/greater_than.rb", "lib/searchlogic/condition/greater_than_or_equal_to.rb", "lib/searchlogic/condition/inclusive_descendant_of.rb", "lib/searchlogic/condition/keywords.rb", "lib/searchlogic/condition/less_than.rb", "lib/searchlogic/condition/less_than_or_equal_to.rb", "lib/searchlogic/condition/like.rb", "lib/searchlogic/condition/nested_set.rb", "lib/searchlogic/condition/nil.rb", "lib/searchlogic/condition/not_begin_with.rb", "lib/searchlogic/condition/not_blank.rb", "lib/searchlogic/condition/not_end_with.rb", "lib/searchlogic/condition/not_equal.rb", "lib/searchlogic/condition/not_have_keywords.rb", "lib/searchlogic/condition/not_like.rb", "lib/searchlogic/condition/not_nil.rb", "lib/searchlogic/condition/sibling_of.rb", "lib/searchlogic/conditions/any_or_all.rb", "lib/searchlogic/conditions/base.rb", "lib/searchlogic/conditions/groups.rb", "lib/searchlogic/conditions/magic_methods.rb", "lib/searchlogic/conditions/multiparameter_attributes.rb", "lib/searchlogic/conditions/protection.rb", "lib/searchlogic/config/helpers.rb", "lib/searchlogic/config/search.rb", "lib/searchlogic/config.rb", "lib/searchlogic/core_ext/hash.rb", "lib/searchlogic/core_ext/object.rb", "lib/searchlogic/helpers/control_types/link.rb", "lib/searchlogic/helpers/control_types/links.rb", "lib/searchlogic/helpers/control_types/remote_link.rb", "lib/searchlogic/helpers/control_types/remote_links.rb", "lib/searchlogic/helpers/control_types/remote_select.rb", "lib/searchlogic/helpers/control_types/select.rb", "lib/searchlogic/helpers/form.rb", "lib/searchlogic/helpers/utilities.rb", "lib/searchlogic/modifiers/absolute.rb", "lib/searchlogic/modifiers/acos.rb", "lib/searchlogic/modifiers/asin.rb", "lib/searchlogic/modifiers/atan.rb", "lib/searchlogic/modifiers/avg.rb", "lib/searchlogic/modifiers/base.rb", "lib/searchlogic/modifiers/ceil.rb", "lib/searchlogic/modifiers/char_length.rb", "lib/searchlogic/modifiers/cos.rb", "lib/searchlogic/modifiers/cot.rb", "lib/searchlogic/modifiers/count.rb", "lib/searchlogic/modifiers/day_of_month.rb", "lib/searchlogic/modifiers/day_of_week.rb", "lib/searchlogic/modifiers/day_of_year.rb", "lib/searchlogic/modifiers/degrees.rb", "lib/searchlogic/modifiers/exp.rb", "lib/searchlogic/modifiers/floor.rb", "lib/searchlogic/modifiers/hex.rb", "lib/searchlogic/modifiers/hour.rb", "lib/searchlogic/modifiers/log.rb", "lib/searchlogic/modifiers/log10.rb", "lib/searchlogic/modifiers/log2.rb", "lib/searchlogic/modifiers/lower.rb", "lib/searchlogic/modifiers/ltrim.rb", "lib/searchlogic/modifiers/md5.rb", "lib/searchlogic/modifiers/microseconds.rb", "lib/searchlogic/modifiers/milliseconds.rb", "lib/searchlogic/modifiers/minute.rb", "lib/searchlogic/modifiers/month.rb", "lib/searchlogic/modifiers/octal.rb", "lib/searchlogic/modifiers/radians.rb", "lib/searchlogic/modifiers/round.rb", "lib/searchlogic/modifiers/rtrim.rb", "lib/searchlogic/modifiers/second.rb", "lib/searchlogic/modifiers/sign.rb", "lib/searchlogic/modifiers/sin.rb", "lib/searchlogic/modifiers/square_root.rb", "lib/searchlogic/modifiers/sum.rb", "lib/searchlogic/modifiers/tan.rb", "lib/searchlogic/modifiers/trim.rb", "lib/searchlogic/modifiers/upper.rb", "lib/searchlogic/modifiers/week.rb", "lib/searchlogic/modifiers/year.rb", "lib/searchlogic/search/base.rb", "lib/searchlogic/search/conditions.rb", "lib/searchlogic/search/ordering.rb", "lib/searchlogic/search/pagination.rb", "lib/searchlogic/search/protection.rb", "lib/searchlogic/search/searching.rb", "lib/searchlogic/shared/utilities.rb", "lib/searchlogic/shared/virtual_classes.rb", "lib/searchlogic/version.rb", "lib/searchlogic.rb", "Manifest", "MIT-LICENSE", "Rakefile", "README.rdoc", "test/active_record_tests/associations_test.rb", "test/active_record_tests/base_test.rb", "test/condition_tests/base_test.rb", "test/condition_tests/begins_with_test.rb", "test/condition_tests/blank_test.rb", "test/condition_tests/child_of_test.rb", "test/condition_tests/descendant_of_test.rb", "test/condition_tests/ends_with_test.rb", "test/condition_tests/equals_test.rb", "test/condition_tests/greater_than_or_equal_to_test.rb", "test/condition_tests/greater_than_test.rb", "test/condition_tests/inclusive_descendant_of_test.rb", "test/condition_tests/keywords_test.rb", "test/condition_tests/less_than_or_equal_to_test.rb", "test/condition_tests/less_than_test.rb", "test/condition_tests/like_test.rb", "test/condition_tests/nil_test.rb", "test/condition_tests/not_begin_with_test.rb", "test/condition_tests/not_blank_test.rb", "test/condition_tests/not_end_with_test.rb", "test/condition_tests/not_equal_test.rb", "test/condition_tests/not_have_keywords_test.rb", "test/condition_tests/not_like_test.rb", "test/condition_tests/not_nil_test.rb", "test/condition_tests/sibling_of_test.rb", "test/conditions_tests/any_or_all_test.rb", "test/conditions_tests/base_test.rb", "test/conditions_tests/groups_test.rb", "test/conditions_tests/magic_methods_test.rb", "test/conditions_tests/multiparameter_attributes_test.rb", "test/conditions_tests/protection_test.rb", "test/config_test.rb", "test/fixtures/accounts.yml", "test/fixtures/animals.yml", "test/fixtures/orders.yml", "test/fixtures/user_groups.yml", "test/fixtures/users.yml", "test/libs/awesome_nested_set/compatability.rb", "test/libs/awesome_nested_set/helper.rb", "test/libs/awesome_nested_set/named_scope.rb", "test/libs/awesome_nested_set.rb", "test/libs/rexml_fix.rb", "test/modifier_tests/day_of_month_test.rb", "test/search_tests/base_test.rb", "test/search_tests/conditions_test.rb", "test/search_tests/ordering_test.rb", "test/search_tests/pagination_test.rb", "test/search_tests/protection_test.rb", "test/test_helper.rb", "TODO.rdoc", "searchlogic.gemspec"]
14
14
  s.has_rdoc = true
15
15
  s.homepage = %q{http://github.com/binarylogic/searchlogic}
16
16
  s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Searchlogic", "--main", "README.rdoc"]
@@ -12,8 +12,8 @@ module ActiveRecordTests
12
12
  assert_equal User, search.klass
13
13
  assert_equal({:conditions => "\"users\".account_id = #{binary_logic.id}"}, search.scope)
14
14
 
15
- assert_equal [jennifer, ben], search.all
16
- assert_equal jennifer, search.first
15
+ assert_equal [ben, jennifer], search.all
16
+ assert_equal ben, search.first
17
17
  assert_equal ((ben.id + jennifer.id) / 2.0), search.average("id")
18
18
  assert_equal 2, search.count
19
19
 
@@ -70,10 +70,9 @@ module ActiveRecordTests
70
70
  assert_kind_of Searchlogic::Search::Base, search
71
71
  assert_equal User, search.klass
72
72
  assert_equal({:conditions => "\"user_groups_users\".user_group_id = #{neco.id} ", :joins => "INNER JOIN \"user_groups_users\" ON \"users\".id = \"user_groups_users\".user_id"}, search.scope)
73
+ assert_equal [drew, ben], search.all
73
74
 
74
- assert_equal [ben, drew], search.all
75
-
76
- assert_equal ben, search.first
75
+ assert_equal drew, search.first
77
76
  assert_equal ((ben.id + drew.id) / 2.0).to_s, search.average("id").to_s
78
77
  assert_equal 2, search.count
79
78
 
@@ -4,13 +4,9 @@ module ConditionTests
4
4
  class DescendantOfTest < ActiveSupport::TestCase
5
5
  def test_sanitize
6
6
  ben = users(:ben)
7
- drew = users(:drew)
8
- jennifer = users(:jennifer)
9
- tren = users(:tren)
10
-
11
7
  condition = Searchlogic::Condition::DescendantOf.new(User)
12
8
  condition.value = ben
13
- assert_equal ["\"users\".\"id\" = ? OR \"users\".\"id\" = ? OR \"users\".\"id\" = ?", drew.id, tren.id, jennifer.id], condition.sanitize
9
+ assert_equal ["\"users\".\"id\" != ? AND \"users\".\"lft\" >= ? AND \"users\".\"rgt\" <= ?", ben.id, ben.left, ben.right], condition.sanitize
14
10
  end
15
11
  end
16
12
  end
@@ -4,13 +4,9 @@ module ConditionTests
4
4
  class InclusiveDescendantOfTest < ActiveSupport::TestCase
5
5
  def test_sanitize
6
6
  ben = users(:ben)
7
- drew = users(:drew)
8
- jennifer = users(:jennifer)
9
- tren = users(:tren)
10
-
11
7
  condition = Searchlogic::Condition::InclusiveDescendantOf.new(User)
12
8
  condition.value = ben
13
- assert_equal ["\"users\".\"id\" = ? OR \"users\".\"id\" = ? OR \"users\".\"id\" = ? OR \"users\".\"id\" = ?", ben.id, drew.id, tren.id, jennifer.id], condition.sanitize
9
+ assert_equal ["\"users\".\"lft\" >= ? AND \"users\".\"rgt\" <= ?", ben.left, ben.right], condition.sanitize
14
10
  end
15
11
  end
16
12
  end
@@ -4,13 +4,14 @@ module ConditionsTests
4
4
  class MagicMethodsTest < ActiveSupport::TestCase
5
5
  def test_class_level_conditions
6
6
  ben = users(:ben)
7
- drew = users(:drew)
8
- jennifer = users(:jennifer)
9
- tren = users(:tren)
10
7
 
11
8
  conditions = Searchlogic::Cache::UserConditions.new
9
+ conditions.descendant_of = "21"
10
+ assert_equal 21, conditions.descendant_of
11
+ conditions.descendant_of = ["21", "22"]
12
+ assert_equal [21, 22], conditions.descendant_of
12
13
  conditions.descendant_of = ben
13
- assert_equal ["\"users\".\"id\" = ? OR \"users\".\"id\" = ? OR \"users\".\"id\" = ?", drew.id, tren.id, jennifer.id], conditions.sanitize
14
+ assert_equal ["\"users\".\"id\" != ? AND \"users\".\"lft\" >= ? AND \"users\".\"rgt\" <= ?", ben.id, ben.left, ben.right], conditions.sanitize
14
15
  end
15
16
 
16
17
  def test_virtual_columns
@@ -1,11 +1,11 @@
1
1
  bens_order:
2
- user: ben
2
+ user_id: 1
3
3
  total: 500.23
4
4
  description: A bunch of apple products, etc.
5
5
  receipt: some binary text
6
6
 
7
7
  drews_order:
8
- user: drew
8
+ user_id: 2
9
9
  total: 2.12
10
10
  description: Some more apple projects, ipod, etc
11
11
  receipt: some more binary text
@@ -1,7 +1,5 @@
1
1
  neco:
2
2
  name: NECO
3
- users: ben, drew
4
3
 
5
4
  johnsons:
6
- name: Johnsons
7
- users: ben, jennifer
5
+ name: Johnsons
@@ -1,28 +1,43 @@
1
1
  ben:
2
+ id: 1
2
3
  account: binary_logic
4
+ lft: 1
5
+ rgt: 8
3
6
  first_name: Ben
4
7
  last_name: Johnson
5
8
  active: true
6
9
  bio: Totally awesome!
10
+ user_groups: neco, johnsons
7
11
 
8
12
  drew:
13
+ id: 2
9
14
  account: neco
10
- parent: ben
15
+ parent_id: 1
16
+ lft: 2
17
+ rgt: 5
11
18
  first_name: Drew
12
19
  last_name: Mills
13
20
  active: false
14
21
  bio: Totally not awesome!
22
+ user_groups: neco
15
23
 
16
24
  jennifer:
25
+ id: 3
17
26
  account: binary_logic
18
- parent: drew
27
+ parent_id: 2
28
+ lft: 3
29
+ rgt: 4
19
30
  first_name: Jennifer
20
31
  last_name: Hopkins
21
32
  active: false
22
33
  bio: Totally not awesome at all!
34
+ user_groups: johnsons
23
35
 
24
36
  tren:
25
- parent: ben
37
+ id: 4
38
+ parent_id: 1
39
+ lft: 6
40
+ rgt: 7
26
41
  first_name: Tren
27
42
  last_name: Garfield
28
43
  active: false
@@ -0,0 +1,545 @@
1
+ module CollectiveIdea #:nodoc:
2
+ module Acts #:nodoc:
3
+ module NestedSet #:nodoc:
4
+ def self.included(base)
5
+ base.extend(SingletonMethods)
6
+ end
7
+
8
+ # This acts provides Nested Set functionality. Nested Set is a smart way to implement
9
+ # an _ordered_ tree, with the added feature that you can select the children and all of their
10
+ # descendants with a single query. The drawback is that insertion or move need some complex
11
+ # sql queries. But everything is done here by this module!
12
+ #
13
+ # Nested sets are appropriate each time you want either an orderd tree (menus,
14
+ # commercial categories) or an efficient way of querying big trees (threaded posts).
15
+ #
16
+ # == API
17
+ #
18
+ # Methods names are aligned with acts_as_tree as much as possible, to make replacment from one
19
+ # by another easier, except for the creation:
20
+ #
21
+ # in acts_as_tree:
22
+ # item.children.create(:name => "child1")
23
+ #
24
+ # in acts_as_nested_set:
25
+ # # adds a new item at the "end" of the tree, i.e. with child.left = max(tree.right)+1
26
+ # child = MyClass.new(:name => "child1")
27
+ # child.save
28
+ # # now move the item to its right place
29
+ # child.move_to_child_of my_item
30
+ #
31
+ # You can pass an id or an object to:
32
+ # * <tt>#move_to_child_of</tt>
33
+ # * <tt>#move_to_right_of</tt>
34
+ # * <tt>#move_to_left_of</tt>
35
+ #
36
+ module SingletonMethods
37
+ # Configuration options are:
38
+ #
39
+ # * +:parent_column+ - specifies the column name to use for keeping the position integer (default: parent_id)
40
+ # * +:left_column+ - column name for left boundry data, default "lft"
41
+ # * +:right_column+ - column name for right boundry data, default "rgt"
42
+ # * +:scope+ - restricts what is to be considered a list. Given a symbol, it'll attach "_id"
43
+ # (if it hasn't been already) and use that as the foreign key restriction. You
44
+ # can also pass an array to scope by multiple attributes.
45
+ # Example: <tt>acts_as_nested_set :scope => [:notable_id, :notable_type]</tt>
46
+ # * +:dependent+ - behavior for cascading destroy. If set to :destroy, all the
47
+ # child objects are destroyed alongside this object by calling their destroy
48
+ # method. If set to :delete_all (default), all the child objects are deleted
49
+ # without calling their destroy method.
50
+ #
51
+ # See CollectiveIdea::Acts::NestedSet::ClassMethods for a list of class methods and
52
+ # CollectiveIdea::Acts::NestedSet::InstanceMethods for a list of instance methods added
53
+ # to acts_as_nested_set models
54
+ def acts_as_nested_set(options = {})
55
+ options = {
56
+ :parent_column => 'parent_id',
57
+ :left_column => 'lft',
58
+ :right_column => 'rgt',
59
+ :dependent => :delete_all, # or :destroy
60
+ }.merge(options)
61
+
62
+ if options[:scope].is_a?(Symbol) && options[:scope].to_s !~ /_id$/
63
+ options[:scope] = "#{options[:scope]}_id".intern
64
+ end
65
+
66
+ write_inheritable_attribute :acts_as_nested_set_options, options
67
+ class_inheritable_reader :acts_as_nested_set_options
68
+
69
+ include InstanceMethods
70
+ include Comparable
71
+ include Columns
72
+ extend Columns
73
+ extend ClassMethods
74
+
75
+ # no bulk assignment
76
+ attr_protected left_column_name.intern,
77
+ right_column_name.intern,
78
+ parent_column_name.intern
79
+
80
+ before_create :set_default_left_and_right
81
+ before_destroy :prune_from_tree
82
+
83
+ # no assignment to structure fields
84
+ [left_column_name, right_column_name, parent_column_name].each do |column|
85
+ module_eval <<-"end_eval", __FILE__, __LINE__
86
+ def #{column}=(x)
87
+ raise ActiveRecord::ActiveRecordError, "Unauthorized assignment to #{column}: it's an internal field handled by acts_as_nested_set code, use move_to_* methods instead."
88
+ end
89
+ end_eval
90
+ end
91
+
92
+ named_scope :roots, :conditions => {parent_column_name => nil}, :order => quoted_left_column_name
93
+ named_scope :leaves, :conditions => "#{quoted_right_column_name} - #{quoted_left_column_name} = 1", :order => quoted_left_column_name
94
+ if self.respond_to?(:define_callbacks)
95
+ define_callbacks("before_move", "after_move")
96
+ end
97
+
98
+
99
+ end
100
+
101
+ end
102
+
103
+ module ClassMethods
104
+
105
+ # Returns the first root
106
+ def root
107
+ roots.find(:first)
108
+ end
109
+
110
+ def valid?
111
+ left_and_rights_valid? && no_duplicates_for_columns? && all_roots_valid?
112
+ end
113
+
114
+ def left_and_rights_valid?
115
+ count(
116
+ :joins => "LEFT OUTER JOIN #{quoted_table_name} AS parent ON " +
117
+ "#{quoted_table_name}.#{quoted_parent_column_name} = parent.#{primary_key}",
118
+ :conditions =>
119
+ "#{quoted_table_name}.#{quoted_left_column_name} IS NULL OR " +
120
+ "#{quoted_table_name}.#{quoted_right_column_name} IS NULL OR " +
121
+ "#{quoted_table_name}.#{quoted_left_column_name} >= " +
122
+ "#{quoted_table_name}.#{quoted_right_column_name} OR " +
123
+ "(#{quoted_table_name}.#{quoted_parent_column_name} IS NOT NULL AND " +
124
+ "(#{quoted_table_name}.#{quoted_left_column_name} <= parent.#{quoted_left_column_name} OR " +
125
+ "#{quoted_table_name}.#{quoted_right_column_name} >= parent.#{quoted_right_column_name}))"
126
+ ) == 0
127
+ end
128
+
129
+ def no_duplicates_for_columns?
130
+ scope_string = Array(acts_as_nested_set_options[:scope]).map do |c|
131
+ connection.quote_column_name(c)
132
+ end.push(nil).join(", ")
133
+ [quoted_left_column_name, quoted_right_column_name].all? do |column|
134
+ # No duplicates
135
+ find(:first,
136
+ :select => "#{scope_string}#{column}, COUNT(#{column})",
137
+ :group => "#{scope_string}#{column}
138
+ HAVING COUNT(#{column}) > 1").nil?
139
+ end
140
+ end
141
+
142
+ # Wrapper for each_root_valid? that can deal with scope.
143
+ def all_roots_valid?
144
+ if acts_as_nested_set_options[:scope]
145
+ roots(:group => scope_column_names).group_by{|record| scope_column_names.collect{|col| record.send(col.to_sym)}}.all? do |scope, grouped_roots|
146
+ each_root_valid?(grouped_roots)
147
+ end
148
+ else
149
+ each_root_valid?(roots)
150
+ end
151
+ end
152
+
153
+ def each_root_valid?(roots_to_validate)
154
+ left = right = 0
155
+ roots_to_validate.all? do |root|
156
+ returning(root.left > left && root.right > right) do
157
+ left = root.left
158
+ right = root.right
159
+ end
160
+ end
161
+ end
162
+
163
+ # Rebuilds the left & rights if unset or invalid. Also very useful for converting from acts_as_tree.
164
+ def rebuild!
165
+ # Don't rebuild a valid tree.
166
+ return true if valid?
167
+
168
+ scope = lambda{}
169
+ if acts_as_nested_set_options[:scope]
170
+ scope = lambda{|node|
171
+ scope_column_names.inject(""){|str, column_name|
172
+ str << "AND #{connection.quote_column_name(column_name)} = #{connection.quote(node.send(column_name.to_sym))} "
173
+ }
174
+ }
175
+ end
176
+ indices = {}
177
+
178
+ set_left_and_rights = lambda do |node|
179
+ # set left
180
+ node[left_column_name] = indices[scope.call(node)] += 1
181
+ # find
182
+ find(:all, :conditions => ["#{quoted_parent_column_name} = ? #{scope.call(node)}", node], :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, id").each{|n| set_left_and_rights.call(n) }
183
+ # set right
184
+ node[right_column_name] = indices[scope.call(node)] += 1
185
+ node.save!
186
+ end
187
+
188
+ # Find root node(s)
189
+ root_nodes = find(:all, :conditions => "#{quoted_parent_column_name} IS NULL", :order => "#{quoted_left_column_name}, #{quoted_right_column_name}, id").each do |root_node|
190
+ # setup index for this scope
191
+ indices[scope.call(root_node)] ||= 0
192
+ set_left_and_rights.call(root_node)
193
+ end
194
+ end
195
+ end
196
+
197
+ # Mixed into both classes and instances to provide easy access to the column names
198
+ module Columns
199
+ def left_column_name
200
+ acts_as_nested_set_options[:left_column]
201
+ end
202
+
203
+ def right_column_name
204
+ acts_as_nested_set_options[:right_column]
205
+ end
206
+
207
+ def parent_column_name
208
+ acts_as_nested_set_options[:parent_column]
209
+ end
210
+
211
+ def scope_column_names
212
+ Array(acts_as_nested_set_options[:scope])
213
+ end
214
+
215
+ def quoted_left_column_name
216
+ connection.quote_column_name(left_column_name)
217
+ end
218
+
219
+ def quoted_right_column_name
220
+ connection.quote_column_name(right_column_name)
221
+ end
222
+
223
+ def quoted_parent_column_name
224
+ connection.quote_column_name(parent_column_name)
225
+ end
226
+
227
+ def quoted_scope_column_names
228
+ scope_column_names.collect {|column_name| connection.quote_column_name(column_name) }
229
+ end
230
+ end
231
+
232
+ # Any instance method that returns a collection makes use of Rails 2.1's named_scope (which is bundled for Rails 2.0), so it can be treated as a finder.
233
+ #
234
+ # category.self_and_descendants.count
235
+ # category.ancestors.find(:all, :conditions => "name like '%foo%'")
236
+ module InstanceMethods
237
+ # Value of the parent column
238
+ def parent_id
239
+ self[parent_column_name]
240
+ end
241
+
242
+ # Value of the left column
243
+ def left
244
+ self[left_column_name]
245
+ end
246
+
247
+ # Value of the right column
248
+ def right
249
+ self[right_column_name]
250
+ end
251
+
252
+ # Returns true if this is a root node.
253
+ def root?
254
+ parent_id.nil?
255
+ end
256
+
257
+ def leaf?
258
+ right - left == 1
259
+ end
260
+
261
+ # Returns true is this is a child node
262
+ def child?
263
+ !parent_id.nil?
264
+ end
265
+
266
+ # order by left column
267
+ def <=>(x)
268
+ left <=> x.left
269
+ end
270
+
271
+ # Returns root
272
+ def root
273
+ self_and_ancestors.find(:first)
274
+ end
275
+
276
+ # Returns the immediate parent
277
+ def parent
278
+ nested_set_scope.find_by_id(parent_id) if parent_id
279
+ end
280
+
281
+ # Returns the array of all parents and self
282
+ def self_and_ancestors
283
+ nested_set_scope.scoped :conditions => [
284
+ "#{self.class.table_name}.#{quoted_left_column_name} <= ? AND #{self.class.table_name}.#{quoted_right_column_name} >= ?", left, right
285
+ ]
286
+ end
287
+
288
+ # Returns an array of all parents
289
+ def ancestors
290
+ without_self self_and_ancestors
291
+ end
292
+
293
+ # Returns the array of all children of the parent, including self
294
+ def self_and_siblings
295
+ nested_set_scope.scoped :conditions => {parent_column_name => parent_id}
296
+ end
297
+
298
+ # Returns the array of all children of the parent, except self
299
+ def siblings
300
+ without_self self_and_siblings
301
+ end
302
+
303
+ # Returns a set of all of its nested children which do not have children
304
+ def leaves
305
+ descendants.scoped :conditions => "#{self.class.table_name}.#{quoted_right_column_name} - #{self.class.table_name}.#{quoted_left_column_name} = 1"
306
+ end
307
+
308
+ # Returns the level of this object in the tree
309
+ # root level is 0
310
+ def level
311
+ parent_id.nil? ? 0 : ancestors.count
312
+ end
313
+
314
+ # Returns a set of itself and all of its nested children
315
+ def self_and_descendants
316
+ nested_set_scope.scoped :conditions => [
317
+ "#{self.class.table_name}.#{quoted_left_column_name} >= ? AND #{self.class.table_name}.#{quoted_right_column_name} <= ?", left, right
318
+ ]
319
+ end
320
+
321
+ # Returns a set of all of its children and nested children
322
+ def descendants
323
+ without_self self_and_descendants
324
+ end
325
+
326
+ # Returns a set of only this entry's immediate children
327
+ def children
328
+ nested_set_scope.scoped :conditions => {parent_column_name => self}
329
+ end
330
+
331
+ def is_descendant_of?(other)
332
+ other.left < self.left && self.left < other.right && same_scope?(other)
333
+ end
334
+
335
+ def is_or_is_descendant_of?(other)
336
+ other.left <= self.left && self.left < other.right && same_scope?(other)
337
+ end
338
+
339
+ def is_ancestor_of?(other)
340
+ self.left < other.left && other.left < self.right && same_scope?(other)
341
+ end
342
+
343
+ def is_or_is_ancestor_of?(other)
344
+ self.left <= other.left && other.left < self.right && same_scope?(other)
345
+ end
346
+
347
+ # Check if other model is in the same scope
348
+ def same_scope?(other)
349
+ Array(acts_as_nested_set_options[:scope]).all? do |attr|
350
+ self.send(attr) == other.send(attr)
351
+ end
352
+ end
353
+
354
+ # Find the first sibling to the left
355
+ def left_sibling
356
+ siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} < ?", left],
357
+ :order => "#{self.class.table_name}.#{quoted_left_column_name} DESC")
358
+ end
359
+
360
+ # Find the first sibling to the right
361
+ def right_sibling
362
+ siblings.find(:first, :conditions => ["#{self.class.table_name}.#{quoted_left_column_name} > ?", left])
363
+ end
364
+
365
+ # Shorthand method for finding the left sibling and moving to the left of it.
366
+ def move_left
367
+ move_to_left_of left_sibling
368
+ end
369
+
370
+ # Shorthand method for finding the right sibling and moving to the right of it.
371
+ def move_right
372
+ move_to_right_of right_sibling
373
+ end
374
+
375
+ # Move the node to the left of another node (you can pass id only)
376
+ def move_to_left_of(node)
377
+ move_to node, :left
378
+ end
379
+
380
+ # Move the node to the left of another node (you can pass id only)
381
+ def move_to_right_of(node)
382
+ move_to node, :right
383
+ end
384
+
385
+ # Move the node to the child of another node (you can pass id only)
386
+ def move_to_child_of(node)
387
+ move_to node, :child
388
+ end
389
+
390
+ # Move the node to root nodes
391
+ def move_to_root
392
+ move_to nil, :root
393
+ end
394
+
395
+ def move_possible?(target)
396
+ self != target && # Can't target self
397
+ same_scope?(target) && # can't be in different scopes
398
+ # !(left..right).include?(target.left..target.right) # this needs tested more
399
+ # detect impossible move
400
+ !((left <= target.left && right >= target.left) or (left <= target.right && right >= target.right))
401
+ end
402
+
403
+ def to_text
404
+ self_and_descendants.map do |node|
405
+ "#{'*'*(node.level+1)} #{node.id} #{node.to_s} (#{node.parent_id}, #{node.left}, #{node.right})"
406
+ end.join("\n")
407
+ end
408
+
409
+ protected
410
+
411
+ def without_self(scope)
412
+ scope.scoped :conditions => ["#{self.class.table_name}.#{self.class.primary_key} != ?", self]
413
+ end
414
+
415
+ # All nested set queries should use this nested_set_scope, which performs finds on
416
+ # the base ActiveRecord class, using the :scope declared in the acts_as_nested_set
417
+ # declaration.
418
+ def nested_set_scope
419
+ options = {:order => quoted_left_column_name}
420
+ scopes = Array(acts_as_nested_set_options[:scope])
421
+ options[:conditions] = scopes.inject({}) do |conditions,attr|
422
+ conditions.merge attr => self[attr]
423
+ end unless scopes.empty?
424
+ self.class.base_class.scoped options
425
+ end
426
+
427
+ # on creation, set automatically lft and rgt to the end of the tree
428
+ def set_default_left_and_right
429
+ maxright = nested_set_scope.maximum(right_column_name) || 0
430
+ # adds the new node to the right of all existing nodes
431
+ self[left_column_name] = maxright + 1
432
+ self[right_column_name] = maxright + 2
433
+ end
434
+
435
+ # Prunes a branch off of the tree, shifting all of the elements on the right
436
+ # back to the left so the counts still work.
437
+ def prune_from_tree
438
+ return if right.nil? || left.nil?
439
+ diff = right - left + 1
440
+
441
+ delete_method = acts_as_nested_set_options[:dependent] == :destroy ?
442
+ :destroy_all : :delete_all
443
+
444
+ self.class.base_class.transaction do
445
+ nested_set_scope.send(delete_method,
446
+ ["#{quoted_left_column_name} > ? AND #{quoted_right_column_name} < ?",
447
+ left, right]
448
+ )
449
+ nested_set_scope.update_all(
450
+ ["#{quoted_left_column_name} = (#{quoted_left_column_name} - ?)", diff],
451
+ ["#{quoted_left_column_name} >= ?", right]
452
+ )
453
+ nested_set_scope.update_all(
454
+ ["#{quoted_right_column_name} = (#{quoted_right_column_name} - ?)", diff],
455
+ ["#{quoted_right_column_name} >= ?", right]
456
+ )
457
+ end
458
+ end
459
+
460
+ # reload left, right, and parent
461
+ def reload_nested_set
462
+ reload(:select => "#{quoted_left_column_name}, " +
463
+ "#{quoted_right_column_name}, #{quoted_parent_column_name}")
464
+ end
465
+
466
+ def move_to(target, position)
467
+ raise ActiveRecord::ActiveRecordError, "You cannot move a new node" if self.new_record?
468
+ return if callback(:before_move) == false
469
+ transaction do
470
+ if target.is_a? self.class.base_class
471
+ target.reload_nested_set
472
+ elsif position != :root
473
+ # load object if node is not an object
474
+ target = nested_set_scope.find(target)
475
+ end
476
+ self.reload_nested_set
477
+
478
+ unless position == :root || move_possible?(target)
479
+ raise ActiveRecord::ActiveRecordError, "Impossible move, target node cannot be inside moved tree."
480
+ end
481
+
482
+ bound = case position
483
+ when :child; target[right_column_name]
484
+ when :left; target[left_column_name]
485
+ when :right; target[right_column_name] + 1
486
+ when :root; 1
487
+ else raise ActiveRecord::ActiveRecordError, "Position should be :child, :left, :right or :root ('#{position}' received)."
488
+ end
489
+
490
+ if bound > self[right_column_name]
491
+ bound = bound - 1
492
+ other_bound = self[right_column_name] + 1
493
+ else
494
+ other_bound = self[left_column_name] - 1
495
+ end
496
+
497
+ # there would be no change
498
+ return if bound == self[right_column_name] || bound == self[left_column_name]
499
+
500
+ # we have defined the boundaries of two non-overlapping intervals,
501
+ # so sorting puts both the intervals and their boundaries in order
502
+ a, b, c, d = [self[left_column_name], self[right_column_name], bound, other_bound].sort
503
+
504
+ new_parent = case position
505
+ when :child; target.id
506
+ when :root; nil
507
+ else target[parent_column_name]
508
+ end
509
+
510
+ self.class.base_class.update_all([
511
+ "#{quoted_left_column_name} = CASE " +
512
+ "WHEN #{quoted_left_column_name} BETWEEN :a AND :b " +
513
+ "THEN #{quoted_left_column_name} + :d - :b " +
514
+ "WHEN #{quoted_left_column_name} BETWEEN :c AND :d " +
515
+ "THEN #{quoted_left_column_name} + :a - :c " +
516
+ "ELSE #{quoted_left_column_name} END, " +
517
+ "#{quoted_right_column_name} = CASE " +
518
+ "WHEN #{quoted_right_column_name} BETWEEN :a AND :b " +
519
+ "THEN #{quoted_right_column_name} + :d - :b " +
520
+ "WHEN #{quoted_right_column_name} BETWEEN :c AND :d " +
521
+ "THEN #{quoted_right_column_name} + :a - :c " +
522
+ "ELSE #{quoted_right_column_name} END, " +
523
+ "#{quoted_parent_column_name} = CASE " +
524
+ "WHEN #{self.class.base_class.primary_key} = :id THEN :new_parent " +
525
+ "ELSE #{quoted_parent_column_name} END",
526
+ {:a => a, :b => b, :c => c, :d => d, :id => self.id, :new_parent => new_parent}
527
+ ], nested_set_scope.proxy_options[:conditions])
528
+ end
529
+ target.reload_nested_set if target
530
+ self.reload_nested_set
531
+ callback(:after_move)
532
+ end
533
+
534
+ end
535
+
536
+ end
537
+ end
538
+ end
539
+
540
+
541
+ require File.dirname(__FILE__) + '/awesome_nested_set/compatability'
542
+
543
+ ActiveRecord::Base.class_eval do
544
+ include CollectiveIdea::Acts::NestedSet
545
+ end