schof-searchlogic 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (159) hide show
  1. data/CHANGELOG.rdoc +302 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Manifest +157 -0
  4. data/README.rdoc +461 -0
  5. data/Rakefile +13 -0
  6. data/TODO.rdoc +4 -0
  7. data/init.rb +1 -0
  8. data/lib/searchlogic.rb +100 -0
  9. data/lib/searchlogic/active_record/associations.rb +52 -0
  10. data/lib/searchlogic/active_record/base.rb +224 -0
  11. data/lib/searchlogic/active_record/connection_adapters/mysql_adapter.rb +176 -0
  12. data/lib/searchlogic/active_record/connection_adapters/postgresql_adapter.rb +172 -0
  13. data/lib/searchlogic/active_record/connection_adapters/sqlite_adapter.rb +80 -0
  14. data/lib/searchlogic/condition/base.rb +165 -0
  15. data/lib/searchlogic/condition/begins_with.rb +17 -0
  16. data/lib/searchlogic/condition/blank.rb +21 -0
  17. data/lib/searchlogic/condition/child_of.rb +11 -0
  18. data/lib/searchlogic/condition/descendant_of.rb +11 -0
  19. data/lib/searchlogic/condition/ends_with.rb +17 -0
  20. data/lib/searchlogic/condition/equals.rb +33 -0
  21. data/lib/searchlogic/condition/greater_than.rb +15 -0
  22. data/lib/searchlogic/condition/greater_than_or_equal_to.rb +15 -0
  23. data/lib/searchlogic/condition/inclusive_descendant_of.rb +10 -0
  24. data/lib/searchlogic/condition/keywords.rb +47 -0
  25. data/lib/searchlogic/condition/less_than.rb +15 -0
  26. data/lib/searchlogic/condition/less_than_or_equal_to.rb +15 -0
  27. data/lib/searchlogic/condition/like.rb +15 -0
  28. data/lib/searchlogic/condition/nested_set.rb +17 -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 +27 -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/conditions/any_or_all.rb +42 -0
  39. data/lib/searchlogic/conditions/base.rb +244 -0
  40. data/lib/searchlogic/conditions/groups.rb +74 -0
  41. data/lib/searchlogic/conditions/magic_methods.rb +286 -0
  42. data/lib/searchlogic/conditions/multiparameter_attributes.rb +105 -0
  43. data/lib/searchlogic/conditions/protection.rb +36 -0
  44. data/lib/searchlogic/config.rb +31 -0
  45. data/lib/searchlogic/config/helpers.rb +338 -0
  46. data/lib/searchlogic/config/search.rb +53 -0
  47. data/lib/searchlogic/core_ext/hash.rb +75 -0
  48. data/lib/searchlogic/core_ext/object.rb +19 -0
  49. data/lib/searchlogic/helpers/control_types/link.rb +310 -0
  50. data/lib/searchlogic/helpers/control_types/links.rb +242 -0
  51. data/lib/searchlogic/helpers/control_types/remote_link.rb +87 -0
  52. data/lib/searchlogic/helpers/control_types/remote_links.rb +72 -0
  53. data/lib/searchlogic/helpers/control_types/remote_select.rb +36 -0
  54. data/lib/searchlogic/helpers/control_types/select.rb +82 -0
  55. data/lib/searchlogic/helpers/form.rb +208 -0
  56. data/lib/searchlogic/helpers/utilities.rb +197 -0
  57. data/lib/searchlogic/modifiers/absolute.rb +15 -0
  58. data/lib/searchlogic/modifiers/acos.rb +11 -0
  59. data/lib/searchlogic/modifiers/asin.rb +11 -0
  60. data/lib/searchlogic/modifiers/atan.rb +11 -0
  61. data/lib/searchlogic/modifiers/avg.rb +15 -0
  62. data/lib/searchlogic/modifiers/base.rb +27 -0
  63. data/lib/searchlogic/modifiers/ceil.rb +15 -0
  64. data/lib/searchlogic/modifiers/char_length.rb +15 -0
  65. data/lib/searchlogic/modifiers/cos.rb +15 -0
  66. data/lib/searchlogic/modifiers/cot.rb +15 -0
  67. data/lib/searchlogic/modifiers/count.rb +11 -0
  68. data/lib/searchlogic/modifiers/day_of_month.rb +15 -0
  69. data/lib/searchlogic/modifiers/day_of_week.rb +15 -0
  70. data/lib/searchlogic/modifiers/day_of_year.rb +15 -0
  71. data/lib/searchlogic/modifiers/degrees.rb +11 -0
  72. data/lib/searchlogic/modifiers/exp.rb +15 -0
  73. data/lib/searchlogic/modifiers/floor.rb +15 -0
  74. data/lib/searchlogic/modifiers/hex.rb +11 -0
  75. data/lib/searchlogic/modifiers/hour.rb +11 -0
  76. data/lib/searchlogic/modifiers/log.rb +15 -0
  77. data/lib/searchlogic/modifiers/log10.rb +11 -0
  78. data/lib/searchlogic/modifiers/log2.rb +11 -0
  79. data/lib/searchlogic/modifiers/lower.rb +15 -0
  80. data/lib/searchlogic/modifiers/ltrim.rb +15 -0
  81. data/lib/searchlogic/modifiers/md5.rb +11 -0
  82. data/lib/searchlogic/modifiers/microseconds.rb +11 -0
  83. data/lib/searchlogic/modifiers/milliseconds.rb +11 -0
  84. data/lib/searchlogic/modifiers/minute.rb +15 -0
  85. data/lib/searchlogic/modifiers/month.rb +15 -0
  86. data/lib/searchlogic/modifiers/octal.rb +15 -0
  87. data/lib/searchlogic/modifiers/radians.rb +11 -0
  88. data/lib/searchlogic/modifiers/round.rb +11 -0
  89. data/lib/searchlogic/modifiers/rtrim.rb +15 -0
  90. data/lib/searchlogic/modifiers/second.rb +15 -0
  91. data/lib/searchlogic/modifiers/sign.rb +11 -0
  92. data/lib/searchlogic/modifiers/sin.rb +11 -0
  93. data/lib/searchlogic/modifiers/square_root.rb +15 -0
  94. data/lib/searchlogic/modifiers/sum.rb +11 -0
  95. data/lib/searchlogic/modifiers/tan.rb +15 -0
  96. data/lib/searchlogic/modifiers/trim.rb +15 -0
  97. data/lib/searchlogic/modifiers/upper.rb +15 -0
  98. data/lib/searchlogic/modifiers/week.rb +11 -0
  99. data/lib/searchlogic/modifiers/year.rb +11 -0
  100. data/lib/searchlogic/search/base.rb +148 -0
  101. data/lib/searchlogic/search/conditions.rb +53 -0
  102. data/lib/searchlogic/search/ordering.rb +244 -0
  103. data/lib/searchlogic/search/pagination.rb +121 -0
  104. data/lib/searchlogic/search/protection.rb +89 -0
  105. data/lib/searchlogic/search/searching.rb +32 -0
  106. data/lib/searchlogic/shared/utilities.rb +56 -0
  107. data/lib/searchlogic/shared/virtual_classes.rb +39 -0
  108. data/lib/searchlogic/version.rb +79 -0
  109. data/searchlogic.gemspec +41 -0
  110. data/test/active_record_tests/associations_test.rb +94 -0
  111. data/test/active_record_tests/base_test.rb +115 -0
  112. data/test/condition_tests/base_test.rb +54 -0
  113. data/test/condition_tests/begins_with_test.rb +11 -0
  114. data/test/condition_tests/blank_test.rb +31 -0
  115. data/test/condition_tests/child_of_test.rb +17 -0
  116. data/test/condition_tests/descendant_of_test.rb +12 -0
  117. data/test/condition_tests/ends_with_test.rb +11 -0
  118. data/test/condition_tests/equals_test.rb +28 -0
  119. data/test/condition_tests/greater_than_or_equal_to_test.rb +11 -0
  120. data/test/condition_tests/greater_than_test.rb +11 -0
  121. data/test/condition_tests/inclusive_descendant_of_test.rb +12 -0
  122. data/test/condition_tests/keywords_test.rb +23 -0
  123. data/test/condition_tests/less_than_or_equal_to_test.rb +11 -0
  124. data/test/condition_tests/less_than_test.rb +11 -0
  125. data/test/condition_tests/like_test.rb +11 -0
  126. data/test/condition_tests/nil_test.rb +31 -0
  127. data/test/condition_tests/not_begin_with_test.rb +8 -0
  128. data/test/condition_tests/not_blank_test.rb +8 -0
  129. data/test/condition_tests/not_end_with_test.rb +8 -0
  130. data/test/condition_tests/not_equal_test.rb +19 -0
  131. data/test/condition_tests/not_have_keywords_test.rb +8 -0
  132. data/test/condition_tests/not_like_test.rb +8 -0
  133. data/test/condition_tests/not_nil_test.rb +13 -0
  134. data/test/condition_tests/sibling_of_test.rb +15 -0
  135. data/test/conditions_tests/any_or_all_test.rb +23 -0
  136. data/test/conditions_tests/base_test.rb +185 -0
  137. data/test/conditions_tests/groups_test.rb +68 -0
  138. data/test/conditions_tests/magic_methods_test.rb +36 -0
  139. data/test/conditions_tests/multiparameter_attributes_test.rb +15 -0
  140. data/test/conditions_tests/protection_test.rb +18 -0
  141. data/test/config_test.rb +23 -0
  142. data/test/fixtures/accounts.yml +12 -0
  143. data/test/fixtures/animals.yml +7 -0
  144. data/test/fixtures/orders.yml +12 -0
  145. data/test/fixtures/user_groups.yml +5 -0
  146. data/test/fixtures/users.yml +45 -0
  147. data/test/libs/awesome_nested_set.rb +545 -0
  148. data/test/libs/awesome_nested_set/compatability.rb +29 -0
  149. data/test/libs/awesome_nested_set/helper.rb +40 -0
  150. data/test/libs/awesome_nested_set/named_scope.rb +140 -0
  151. data/test/libs/rexml_fix.rb +14 -0
  152. data/test/modifier_tests/day_of_month_test.rb +16 -0
  153. data/test/search_tests/base_test.rb +241 -0
  154. data/test/search_tests/conditions_test.rb +21 -0
  155. data/test/search_tests/ordering_test.rb +167 -0
  156. data/test/search_tests/pagination_test.rb +74 -0
  157. data/test/search_tests/protection_test.rb +26 -0
  158. data/test/test_helper.rb +116 -0
  159. metadata +385 -0
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ module ConditionsTests
4
+ class MagicMethodsTest < ActiveSupport::TestCase
5
+ def test_class_level_conditions
6
+ ben = users(:ben)
7
+
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
13
+ conditions.descendant_of = ben
14
+ assert_equal ["(\"users\".\"id\" != ? AND (\"users\".\"lft\" >= ? AND \"users\".\"rgt\" <= ?))", ben.id, ben.left, ben.right], conditions.sanitize
15
+ end
16
+
17
+ def test_virtual_columns
18
+ search = Account.new_search
19
+ conditions = search.conditions
20
+ conditions.hour_of_created_gt = 2
21
+ assert_equal ["(strftime('%H', \"accounts\".\"created_at\") * 1) > ?", 2], conditions.sanitize
22
+ conditions.dow_of_created_at_most = 5
23
+ assert_equal ["(strftime('%H', \"accounts\".\"created_at\") * 1) > ? AND (strftime('%w', \"accounts\".\"created_at\") * 1) <= ?", 2, 5], conditions.sanitize
24
+ conditions.month_of_created_at_nil = true
25
+ assert_equal ["(strftime('%H', \"accounts\".\"created_at\") * 1) > ? AND (strftime('%w', \"accounts\".\"created_at\") * 1) <= ? AND (strftime('%m', \"accounts\".\"created_at\") * 1) IS NULL", 2, 5], conditions.sanitize
26
+ conditions.min_of_hour_of_month_of_created_at_nil = true
27
+ assert_equal ["(strftime('%H', \"accounts\".\"created_at\") * 1) > ? AND (strftime('%w', \"accounts\".\"created_at\") * 1) <= ? AND (strftime('%m', \"accounts\".\"created_at\") * 1) IS NULL AND (strftime('%m', (strftime('%H', (strftime('%M', \"accounts\".\"created_at\") * 1)) * 1)) * 1) IS NULL", 2, 5], conditions.sanitize
28
+ assert_nothing_raised { search.all }
29
+ end
30
+
31
+ def test_method_conflicts
32
+ conditions = Searchlogic::Cache::AccountConditions.new
33
+ assert_nil conditions.id
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ module ConditionsTests
4
+ class MultiparameterAttributesTest < ActiveSupport::TestCase
5
+ def test_conditions
6
+ values = {"created_at(1i)" => "2004", "created_at(2i)" => "6", "created_at(3i)" => "24"}
7
+ conditions = Searchlogic::Cache::AccountConditions.new(values)
8
+ assert_equal ["\"accounts\".\"created_at\" = ?", Time.parse("Jun 24, 2004")], conditions.sanitize
9
+
10
+ values = {"created_at_gt(1i)" => "2004", "created_at_gt(2i)" => "6", "created_at_gt(3i)" => "24"}
11
+ conditions = Searchlogic::Cache::AccountConditions.new(values)
12
+ assert_equal ["\"accounts\".\"created_at\" > ?", Time.parse("Jun 24, 2004")], conditions.sanitize
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ require File.dirname(__FILE__) + '/../test_helper.rb'
2
+
3
+ module ConditionsTests
4
+ class ProtectionTest < ActiveSupport::TestCase
5
+ def test_protection
6
+ assert_raise(ArgumentError) { Account.new_search(:conditions => "(DELETE FROM users)") }
7
+ assert_nothing_raised { Account.new_search!(:conditions => "(DELETE FROM users)") }
8
+
9
+ account = Account.first
10
+
11
+ assert_raise(ArgumentError) { account.users.new_search(:conditions => "(DELETE FROM users)") }
12
+ assert_nothing_raised { account.users.new_search!(:conditions => "(DELETE FROM users)") }
13
+
14
+ search = Account.new_search
15
+ assert_raise(ArgumentError) { search.conditions = "(DELETE FROM users)" }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,23 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ class ConfigTest < ActiveSupport::TestCase
4
+ def test_per_page
5
+ Searchlogic::Config.search.per_page = 1
6
+
7
+ assert Account.count > 1
8
+ assert Account.all.size > 1
9
+ assert User.all.size > 1
10
+ assert User.find(:all, :per_page => 1).size == 1
11
+ assert User.new_search.all.size == 1
12
+ assert User.new_search(:per_page => nil).all.size > 1
13
+
14
+ Searchlogic::Config.search.per_page = nil
15
+
16
+ assert Account.count > 1
17
+ assert Account.all.size > 1
18
+ assert User.all.size > 1
19
+ assert User.find(:all, :per_page => 1).size == 1
20
+ assert User.new_search.all.size > 1
21
+ assert User.new_search(:per_page => 1).all.size == 1
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ binary_logic:
2
+ name: "Binary Logic"
3
+ active: true
4
+
5
+ neco:
6
+ name: "NECO"
7
+ active: false
8
+
9
+ binary_fun:
10
+ name: "Binary Fun"
11
+ active: true
12
+
@@ -0,0 +1,7 @@
1
+ pepper:
2
+ type: Cat
3
+ description: pepper meows
4
+
5
+ harry:
6
+ type: Doc
7
+ description: harry is hairy
@@ -0,0 +1,12 @@
1
+ bens_order:
2
+ user_id: 1
3
+ total: 500.23
4
+ description: A bunch of apple products, etc.
5
+ receipt: some binary text
6
+
7
+ drews_order:
8
+ user_id: 2
9
+ total: 2.12
10
+ description: Some more apple projects, ipod, etc
11
+ receipt: some more binary text
12
+
@@ -0,0 +1,5 @@
1
+ neco:
2
+ name: NECO
3
+
4
+ johnsons:
5
+ name: Johnsons
@@ -0,0 +1,45 @@
1
+ ben:
2
+ id: 1
3
+ account: binary_logic
4
+ lft: 1
5
+ rgt: 8
6
+ first_name: Ben
7
+ last_name: Johnson
8
+ active: true
9
+ bio: Totally awesome!
10
+ user_groups: neco, johnsons
11
+
12
+ drew:
13
+ id: 2
14
+ account: neco
15
+ parent_id: 1
16
+ lft: 2
17
+ rgt: 5
18
+ first_name: Drew
19
+ last_name: Mills
20
+ active: false
21
+ bio: Totally not awesome!
22
+ user_groups: neco
23
+
24
+ jennifer:
25
+ id: 3
26
+ account: binary_logic
27
+ parent_id: 2
28
+ lft: 3
29
+ rgt: 4
30
+ first_name: Jennifer
31
+ last_name: Hopkins
32
+ active: false
33
+ bio: Totally not awesome at all!
34
+ user_groups: johnsons
35
+
36
+ tren:
37
+ id: 4
38
+ parent_id: 1
39
+ lft: 6
40
+ rgt: 7
41
+ first_name: Tren
42
+ last_name: Garfield
43
+ active: false
44
+ bio: Rocks a little too hard
45
+
@@ -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