searchlogic 1.5.10 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.rdoc +7 -1
- data/Manifest +5 -2
- data/README.rdoc +2 -0
- data/lib/searchlogic.rb +5 -3
- data/lib/searchlogic/condition/base.rb +1 -2
- data/lib/searchlogic/condition/child_of.rb +1 -1
- data/lib/searchlogic/condition/descendant_of.rb +4 -17
- data/lib/searchlogic/condition/inclusive_descendant_of.rb +3 -4
- data/lib/searchlogic/condition/{tree.rb → nested_set.rb} +1 -1
- data/lib/searchlogic/condition/not_begin_with.rb +1 -1
- data/lib/searchlogic/condition/sibling_of.rb +1 -1
- data/lib/searchlogic/conditions/magic_methods.rb +1 -1
- data/lib/searchlogic/search/base.rb +5 -0
- data/lib/searchlogic/search/searching.rb +0 -1
- data/lib/searchlogic/version.rb +2 -2
- data/searchlogic.gemspec +4 -4
- data/test/active_record_tests/associations_test.rb +4 -5
- data/test/condition_tests/descendant_of_test.rb +1 -5
- data/test/condition_tests/inclusive_descendant_of_test.rb +1 -5
- data/test/conditions_tests/magic_methods_test.rb +5 -4
- data/test/fixtures/orders.yml +2 -2
- data/test/fixtures/user_groups.yml +1 -3
- data/test/fixtures/users.yml +18 -3
- data/test/libs/awesome_nested_set.rb +545 -0
- data/test/libs/awesome_nested_set/compatability.rb +29 -0
- data/test/libs/awesome_nested_set/helper.rb +40 -0
- data/test/libs/awesome_nested_set/named_scope.rb +140 -0
- data/test/search_tests/base_test.rb +1 -1
- data/test/test_helper.rb +4 -2
- metadata +8 -5
- data/test/libs/acts_as_tree.rb +0 -98
data/CHANGELOG.rdoc
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
-
== 1.
|
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/
|
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/
|
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,
|
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,
|
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
|
-
|
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 <
|
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 <
|
3
|
+
class DescendantOf < NestedSet
|
4
4
|
def to_conditions(value)
|
5
|
-
|
6
|
-
|
7
|
-
|
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 <
|
3
|
+
class InclusiveDescendantOf < NestedSet
|
4
4
|
def to_conditions(value)
|
5
|
-
|
6
|
-
|
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 SiblingOf <
|
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
|
data/lib/searchlogic/version.rb
CHANGED
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
|
+
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-
|
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/
|
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/
|
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 [
|
16
|
-
assert_equal
|
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
|
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\"
|
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\".\"
|
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\"
|
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
|
data/test/fixtures/orders.yml
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
bens_order:
|
2
|
-
|
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
|
-
|
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
|
data/test/fixtures/users.yml
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|