searchlogic 1.6.6 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (178) hide show
  1. data/.gitignore +6 -0
  2. data/CHANGELOG.rdoc +17 -0
  3. data/{MIT-LICENSE → LICENSE} +2 -2
  4. data/README.rdoc +128 -379
  5. data/Rakefile +56 -20
  6. data/VERSION.yml +4 -0
  7. data/init.rb +1 -1
  8. data/lib/searchlogic.rb +18 -98
  9. data/lib/searchlogic/core_ext/object.rb +33 -13
  10. data/lib/searchlogic/core_ext/proc.rb +11 -0
  11. data/lib/searchlogic/named_scopes/alias_scope.rb +63 -0
  12. data/lib/searchlogic/named_scopes/associations.rb +126 -0
  13. data/lib/searchlogic/named_scopes/conditions.rb +215 -0
  14. data/lib/searchlogic/named_scopes/ordering.rb +53 -0
  15. data/lib/searchlogic/rails_helpers.rb +69 -0
  16. data/lib/searchlogic/search.rb +146 -0
  17. data/rails/init.rb +1 -0
  18. data/searchlogic.gemspec +69 -0
  19. data/spec/core_ext/object_spec.rb +7 -0
  20. data/spec/core_ext/proc_spec.rb +9 -0
  21. data/spec/named_scopes/alias_scope_spec.rb +15 -0
  22. data/spec/named_scopes/associations_spec.rb +120 -0
  23. data/spec/named_scopes/conditions_spec.rb +253 -0
  24. data/spec/named_scopes/ordering_spec.rb +23 -0
  25. data/spec/search_spec.rb +283 -0
  26. data/spec/spec_helper.rb +78 -0
  27. metadata +40 -231
  28. data/Manifest.txt +0 -158
  29. data/TODO.rdoc +0 -4
  30. data/lib/searchlogic/active_record/associations.rb +0 -52
  31. data/lib/searchlogic/active_record/base.rb +0 -224
  32. data/lib/searchlogic/active_record/connection_adapters/mysql_adapter.rb +0 -176
  33. data/lib/searchlogic/active_record/connection_adapters/postgresql_adapter.rb +0 -172
  34. data/lib/searchlogic/active_record/connection_adapters/sqlite_adapter.rb +0 -80
  35. data/lib/searchlogic/condition/base.rb +0 -165
  36. data/lib/searchlogic/condition/begins_with.rb +0 -17
  37. data/lib/searchlogic/condition/blank.rb +0 -24
  38. data/lib/searchlogic/condition/child_of.rb +0 -11
  39. data/lib/searchlogic/condition/descendant_of.rb +0 -11
  40. data/lib/searchlogic/condition/ends_with.rb +0 -17
  41. data/lib/searchlogic/condition/equals.rb +0 -33
  42. data/lib/searchlogic/condition/greater_than.rb +0 -15
  43. data/lib/searchlogic/condition/greater_than_or_equal_to.rb +0 -15
  44. data/lib/searchlogic/condition/inclusive_descendant_of.rb +0 -10
  45. data/lib/searchlogic/condition/keywords.rb +0 -52
  46. data/lib/searchlogic/condition/less_than.rb +0 -15
  47. data/lib/searchlogic/condition/less_than_or_equal_to.rb +0 -15
  48. data/lib/searchlogic/condition/like.rb +0 -15
  49. data/lib/searchlogic/condition/nested_set.rb +0 -17
  50. data/lib/searchlogic/condition/nil.rb +0 -21
  51. data/lib/searchlogic/condition/not_begin_with.rb +0 -20
  52. data/lib/searchlogic/condition/not_blank.rb +0 -19
  53. data/lib/searchlogic/condition/not_end_with.rb +0 -20
  54. data/lib/searchlogic/condition/not_equal.rb +0 -27
  55. data/lib/searchlogic/condition/not_have_keywords.rb +0 -20
  56. data/lib/searchlogic/condition/not_like.rb +0 -20
  57. data/lib/searchlogic/condition/not_nil.rb +0 -19
  58. data/lib/searchlogic/condition/sibling_of.rb +0 -14
  59. data/lib/searchlogic/conditions/any_or_all.rb +0 -42
  60. data/lib/searchlogic/conditions/base.rb +0 -244
  61. data/lib/searchlogic/conditions/groups.rb +0 -74
  62. data/lib/searchlogic/conditions/magic_methods.rb +0 -286
  63. data/lib/searchlogic/conditions/multiparameter_attributes.rb +0 -105
  64. data/lib/searchlogic/conditions/protection.rb +0 -36
  65. data/lib/searchlogic/config.rb +0 -31
  66. data/lib/searchlogic/config/helpers.rb +0 -338
  67. data/lib/searchlogic/config/search.rb +0 -53
  68. data/lib/searchlogic/core_ext/hash.rb +0 -75
  69. data/lib/searchlogic/helpers/control_types/link.rb +0 -310
  70. data/lib/searchlogic/helpers/control_types/links.rb +0 -242
  71. data/lib/searchlogic/helpers/control_types/remote_link.rb +0 -87
  72. data/lib/searchlogic/helpers/control_types/remote_links.rb +0 -72
  73. data/lib/searchlogic/helpers/control_types/remote_select.rb +0 -36
  74. data/lib/searchlogic/helpers/control_types/select.rb +0 -82
  75. data/lib/searchlogic/helpers/form.rb +0 -208
  76. data/lib/searchlogic/helpers/utilities.rb +0 -197
  77. data/lib/searchlogic/modifiers/absolute.rb +0 -15
  78. data/lib/searchlogic/modifiers/acos.rb +0 -11
  79. data/lib/searchlogic/modifiers/asin.rb +0 -11
  80. data/lib/searchlogic/modifiers/atan.rb +0 -11
  81. data/lib/searchlogic/modifiers/avg.rb +0 -15
  82. data/lib/searchlogic/modifiers/base.rb +0 -27
  83. data/lib/searchlogic/modifiers/ceil.rb +0 -15
  84. data/lib/searchlogic/modifiers/char_length.rb +0 -15
  85. data/lib/searchlogic/modifiers/cos.rb +0 -15
  86. data/lib/searchlogic/modifiers/cot.rb +0 -15
  87. data/lib/searchlogic/modifiers/count.rb +0 -11
  88. data/lib/searchlogic/modifiers/day_of_month.rb +0 -15
  89. data/lib/searchlogic/modifiers/day_of_week.rb +0 -15
  90. data/lib/searchlogic/modifiers/day_of_year.rb +0 -15
  91. data/lib/searchlogic/modifiers/degrees.rb +0 -11
  92. data/lib/searchlogic/modifiers/exp.rb +0 -15
  93. data/lib/searchlogic/modifiers/floor.rb +0 -15
  94. data/lib/searchlogic/modifiers/hex.rb +0 -11
  95. data/lib/searchlogic/modifiers/hour.rb +0 -11
  96. data/lib/searchlogic/modifiers/log.rb +0 -15
  97. data/lib/searchlogic/modifiers/log10.rb +0 -11
  98. data/lib/searchlogic/modifiers/log2.rb +0 -11
  99. data/lib/searchlogic/modifiers/lower.rb +0 -15
  100. data/lib/searchlogic/modifiers/ltrim.rb +0 -15
  101. data/lib/searchlogic/modifiers/md5.rb +0 -11
  102. data/lib/searchlogic/modifiers/microseconds.rb +0 -11
  103. data/lib/searchlogic/modifiers/milliseconds.rb +0 -11
  104. data/lib/searchlogic/modifiers/minute.rb +0 -15
  105. data/lib/searchlogic/modifiers/month.rb +0 -15
  106. data/lib/searchlogic/modifiers/octal.rb +0 -15
  107. data/lib/searchlogic/modifiers/radians.rb +0 -11
  108. data/lib/searchlogic/modifiers/round.rb +0 -11
  109. data/lib/searchlogic/modifiers/rtrim.rb +0 -15
  110. data/lib/searchlogic/modifiers/second.rb +0 -15
  111. data/lib/searchlogic/modifiers/sign.rb +0 -11
  112. data/lib/searchlogic/modifiers/sin.rb +0 -11
  113. data/lib/searchlogic/modifiers/square_root.rb +0 -15
  114. data/lib/searchlogic/modifiers/sum.rb +0 -11
  115. data/lib/searchlogic/modifiers/tan.rb +0 -15
  116. data/lib/searchlogic/modifiers/trim.rb +0 -15
  117. data/lib/searchlogic/modifiers/upper.rb +0 -15
  118. data/lib/searchlogic/modifiers/week.rb +0 -11
  119. data/lib/searchlogic/modifiers/year.rb +0 -11
  120. data/lib/searchlogic/search/base.rb +0 -148
  121. data/lib/searchlogic/search/conditions.rb +0 -53
  122. data/lib/searchlogic/search/ordering.rb +0 -244
  123. data/lib/searchlogic/search/pagination.rb +0 -121
  124. data/lib/searchlogic/search/protection.rb +0 -89
  125. data/lib/searchlogic/search/searching.rb +0 -32
  126. data/lib/searchlogic/shared/utilities.rb +0 -57
  127. data/lib/searchlogic/shared/virtual_classes.rb +0 -39
  128. data/lib/searchlogic/version.rb +0 -79
  129. data/test/active_record_tests/associations_test.rb +0 -94
  130. data/test/active_record_tests/base_test.rb +0 -115
  131. data/test/condition_tests/base_test.rb +0 -62
  132. data/test/condition_tests/begins_with_test.rb +0 -11
  133. data/test/condition_tests/blank_test.rb +0 -31
  134. data/test/condition_tests/child_of_test.rb +0 -17
  135. data/test/condition_tests/descendant_of_test.rb +0 -12
  136. data/test/condition_tests/ends_with_test.rb +0 -11
  137. data/test/condition_tests/equals_test.rb +0 -28
  138. data/test/condition_tests/greater_than_or_equal_to_test.rb +0 -11
  139. data/test/condition_tests/greater_than_test.rb +0 -11
  140. data/test/condition_tests/inclusive_descendant_of_test.rb +0 -12
  141. data/test/condition_tests/keywords_test.rb +0 -23
  142. data/test/condition_tests/less_than_or_equal_to_test.rb +0 -11
  143. data/test/condition_tests/less_than_test.rb +0 -11
  144. data/test/condition_tests/like_test.rb +0 -11
  145. data/test/condition_tests/nil_test.rb +0 -31
  146. data/test/condition_tests/not_begin_with_test.rb +0 -8
  147. data/test/condition_tests/not_blank_test.rb +0 -8
  148. data/test/condition_tests/not_end_with_test.rb +0 -8
  149. data/test/condition_tests/not_equal_test.rb +0 -19
  150. data/test/condition_tests/not_have_keywords_test.rb +0 -8
  151. data/test/condition_tests/not_like_test.rb +0 -8
  152. data/test/condition_tests/not_nil_test.rb +0 -13
  153. data/test/condition_tests/sibling_of_test.rb +0 -15
  154. data/test/conditions_tests/any_or_all_test.rb +0 -23
  155. data/test/conditions_tests/base_test.rb +0 -185
  156. data/test/conditions_tests/groups_test.rb +0 -68
  157. data/test/conditions_tests/magic_methods_test.rb +0 -36
  158. data/test/conditions_tests/multiparameter_attributes_test.rb +0 -15
  159. data/test/conditions_tests/protection_test.rb +0 -18
  160. data/test/config_test.rb +0 -23
  161. data/test/fixtures/accounts.yml +0 -12
  162. data/test/fixtures/animals.yml +0 -7
  163. data/test/fixtures/orders.yml +0 -12
  164. data/test/fixtures/user_groups.yml +0 -5
  165. data/test/fixtures/users.yml +0 -45
  166. data/test/libs/awesome_nested_set.rb +0 -545
  167. data/test/libs/awesome_nested_set/.autotest +0 -13
  168. data/test/libs/awesome_nested_set/compatability.rb +0 -29
  169. data/test/libs/awesome_nested_set/helper.rb +0 -40
  170. data/test/libs/awesome_nested_set/named_scope.rb +0 -140
  171. data/test/libs/rexml_fix.rb +0 -14
  172. data/test/modifier_tests/day_of_month_test.rb +0 -16
  173. data/test/search_tests/base_test.rb +0 -241
  174. data/test/search_tests/conditions_test.rb +0 -21
  175. data/test/search_tests/ordering_test.rb +0 -167
  176. data/test/search_tests/pagination_test.rb +0 -74
  177. data/test/search_tests/protection_test.rb +0 -26
  178. data/test/test_helper.rb +0 -122
@@ -1,68 +0,0 @@
1
- require File.dirname(__FILE__) + '/../test_helper.rb'
2
-
3
- module ConditionsTests
4
- class GroupsTest < ActiveSupport::TestCase
5
- def test_group_object
6
- conditions = Searchlogic::Cache::AccountConditions.new
7
- conditions.id_gt = 3
8
- group1 = conditions.group
9
- group1.name_like = "Binary"
10
- group2 = conditions.group
11
- group2.id_gt = 5
12
- group21 = group2.group
13
- group21.id_lt = 20
14
- now = Time.now
15
- group21.created_at_after = now
16
- assert_equal ["\"accounts\".\"id\" > ? AND (\"accounts\".\"name\" LIKE ?) AND (\"accounts\".\"id\" > ? AND (\"accounts\".\"id\" < ? AND \"accounts\".\"created_at\" > ?))", 3, "%Binary%", 5, 20, now], conditions.sanitize
17
- end
18
-
19
- def test_group_block
20
- conditions = Searchlogic::Cache::AccountConditions.new
21
- conditions.id_gt = 3
22
- conditions.group do |group1|
23
- group1.name_like = "Binary"
24
- end
25
- now = Time.now
26
- conditions.group do |group2|
27
- group2.id_gt = 5
28
- group2.group do |group21|
29
- group21.id_lt = 20
30
- group21.created_at_after = now
31
- end
32
- end
33
- assert_equal ["\"accounts\".\"id\" > ? AND (\"accounts\".\"name\" LIKE ?) AND (\"accounts\".\"id\" > ? AND (\"accounts\".\"id\" < ? AND \"accounts\".\"created_at\" > ?))", 3, "%Binary%", 5, 20, now], conditions.sanitize
34
- end
35
-
36
- def test_group_hash
37
- now = Time.now
38
- conditions = Searchlogic::Cache::AccountConditions.new([
39
- {:id_gt => 3},
40
- {:group => {:name_like => "Binary"}},
41
- {:group => [
42
- {:id_gt => 5},
43
- {:group => [
44
- {:id_lt => 20},
45
- {:created_at_after => now}
46
- ]}
47
- ]}
48
- ])
49
- assert_equal ["\"accounts\".\"id\" > ? AND (\"accounts\".\"name\" LIKE ?) AND (\"accounts\".\"id\" > ? AND (\"accounts\".\"id\" < ? AND \"accounts\".\"created_at\" > ?))", 3, "%Binary%", 5, 20, now], conditions.sanitize
50
- end
51
-
52
- def test_auto_joins
53
- conditions = Searchlogic::Cache::AccountConditions.new
54
- conditions.group do |g|
55
- g.users.first_name_like = "Ben"
56
- end
57
- assert_equal :users, conditions.auto_joins
58
-
59
- search = Searchlogic::Cache::AccountSearch.new
60
- search.conditions.users.first_name_like = "Ben"
61
- search.conditions.group do |g|
62
- g.users.orders.id_gt = 5
63
- end
64
- assert_equal [:users, {:users => :orders}], search.conditions.auto_joins
65
- assert_nothing_raised { search.all }
66
- end
67
- end
68
- end
@@ -1,36 +0,0 @@
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
@@ -1,15 +0,0 @@
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.gm(2004, "jun", 24)], 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.utc(2004, "jun", 24)], conditions.sanitize
13
- end
14
- end
15
- end
@@ -1,18 +0,0 @@
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
@@ -1,23 +0,0 @@
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
@@ -1,12 +0,0 @@
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
-
@@ -1,7 +0,0 @@
1
- pepper:
2
- type: Cat
3
- description: pepper meows
4
-
5
- harry:
6
- type: Doc
7
- description: harry is hairy
@@ -1,12 +0,0 @@
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
-
@@ -1,5 +0,0 @@
1
- neco:
2
- name: NECO
3
-
4
- johnsons:
5
- name: Johnsons
@@ -1,45 +0,0 @@
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
-
@@ -1,545 +0,0 @@
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