From 37e4a9b1b7d5cb01042f35ad1bd626c1f4d87f6d Mon Sep 17 00:00:00 2001 From: Anthony Dmitriyev Date: Tue, 11 Aug 2015 15:23:01 +0100 Subject: [PATCH] Scope for leaves --- lib/ancestry/class_methods.rb | 31 +++++++++++++++++++++++++++ lib/ancestry/has_ancestry.rb | 2 +- test/concerns/class_methods_test.rb | 33 +++++++++++++++++++++++++++++ test/concerns/scopes_test.rb | 5 ++++- 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 test/concerns/class_methods_test.rb diff --git a/lib/ancestry/class_methods.rb b/lib/ancestry/class_methods.rb index eef55cdb..67ce0f25 100644 --- a/lib/ancestry/class_methods.rb +++ b/lib/ancestry/class_methods.rb @@ -17,6 +17,17 @@ def scope_depth depth_options, depth end end + # Scope that returns all the leaves + def leaves + id_column = "#{table_name}.id" + id_column_as_text = sql_cast_as_text(id_column) + parent_ancestry = sql_concat("#{table_name}.#{ancestry_column}", "'/'", id_column_as_text) + + joins("LEFT JOIN #{table_name} AS c ON c.#{ancestry_column} = #{id_column_as_text} OR c.#{ancestry_column} = #{parent_ancestry}"). + group(id_column). + having('COUNT(c.id) = 0') + end + # Orphan strategy writer def orphan_strategy= orphan_strategy # Check value of orphan strategy, only rootify, adopt, restrict or destroy is allowed @@ -197,5 +208,25 @@ def rebuild_depth_cache! end end end + + private + + def sql_concat *parts + if ActiveRecord::Base.connection.adapter_name.downcase == 'sqlite' + parts.join(' || ') + else + "CONCAT(#{parts.join(', ')})" + end + end + + def sql_cast_as_text column + text_type = if ActiveRecord::Base.connection.adapter_name.downcase == 'mysql' + 'CHAR' + else + 'TEXT' + end + + "CAST(#{column} AS #{text_type})" + end end end diff --git a/lib/ancestry/has_ancestry.rb b/lib/ancestry/has_ancestry.rb index ab9872fc..b6c53693 100644 --- a/lib/ancestry/has_ancestry.rb +++ b/lib/ancestry/has_ancestry.rb @@ -86,4 +86,4 @@ class << ActiveRecord::Base alias_method :acts_as_tree, :has_ancestry end end -end \ No newline at end of file +end diff --git a/test/concerns/class_methods_test.rb b/test/concerns/class_methods_test.rb new file mode 100644 index 00000000..fd4293bc --- /dev/null +++ b/test/concerns/class_methods_test.rb @@ -0,0 +1,33 @@ +require_relative '../environment' + +class ClassMethodsTest < ActiveSupport::TestCase + def test_sql_concat + AncestryTestDatabase.with_model do |model| + result = model.send(:sql_concat, 'table_name.id', "'/'") + + case ActiveRecord::Base.connection.adapter_name.downcase.to_sym + when :sqlite + assert_equal result, "table_name.id || '/'" + when :mysql + assert_equal result, "CONCAT(table_name.id, '/')" + when :postgresql + assert_equal result, "CONCAT(table_name.id, '/')" + end + end + end + + def text_sql_cast_as_text + AncestryTestDatabase.with_model do |model| + result = model.send(:sql_cast_as_text, 'table_name.id') + + case ActiveRecord::Base.connection.adapter_name.downcase.to_sym + when :sqlite + assert_equal result, 'CAST(table_name.id AS TEXT)' + when :mysql + assert_equal result, 'CAST(table_name.id AS CHAR)' + when :postgresql + assert_equal result, 'CAST(table_name.id AS TEXT)' + end + end + end +end diff --git a/test/concerns/scopes_test.rb b/test/concerns/scopes_test.rb index 5d59e86a..921623b8 100644 --- a/test/concerns/scopes_test.rb +++ b/test/concerns/scopes_test.rb @@ -6,6 +6,9 @@ def test_scopes # Roots assertion assert_equal roots.map(&:first), model.roots.to_a + # Leaves assertion + assert_equal model.all.select(&:is_childless?), model.leaves.order(:id).to_a + model.all.each do |test_node| # Assertions for ancestors_of named scope assert_equal test_node.ancestors.to_a, model.ancestors_of(test_node).to_a @@ -66,4 +69,4 @@ def test_scoping_in_callbacks assert child = parent.children.create end end -end \ No newline at end of file +end