diff --git a/app/graphql/types/learning_outcome_group_type.rb b/app/graphql/types/learning_outcome_group_type.rb
index 2c98c69d2ad..7ba4f899ea2 100644
--- a/app/graphql/types/learning_outcome_group_type.rb
+++ b/app/graphql/types/learning_outcome_group_type.rb
@@ -50,19 +50,23 @@ module Types
field :child_groups_count, Integer, null: false
def child_groups_count
- # Not Implemented yet
- 0
+ learning_outcome_group_children_service.total_subgroups(object.id)
end
field :outcomes_count, Integer, null: false
def outcomes_count
- # Not Implemented yet
- 0
+ learning_outcome_group_children_service.total_outcomes(object.id)
end
field :outcomes, Types::ContentTagConnection, null: false
def outcomes
object.child_outcome_links.active.order_by_outcome_title
end
+
+ private
+
+ def learning_outcome_group_children_service
+ @learning_outcome_group_children_service ||= Outcomes::LearningOutcomeGroupChildren.new(object.context)
+ end
end
end
diff --git a/lib/outcomes/learning_outcome_group_children.rb b/lib/outcomes/learning_outcome_group_children.rb
new file mode 100644
index 00000000000..08048b4d8c2
--- /dev/null
+++ b/lib/outcomes/learning_outcome_group_children.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+#
+# Copyright (C) 2021 - present Instructure, Inc.
+#
+# This file is part of Canvas.
+#
+# Canvas is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, version 3 of the License.
+#
+# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see .
+#
+
+module Outcomes
+ class LearningOutcomeGroupChildren
+ attr_reader :context
+
+ def initialize(context = nil)
+ @context = context
+ end
+
+ def total_subgroups(learning_outcome_group_id)
+ children_ids(learning_outcome_group_id).length
+ end
+
+ def total_outcomes(learning_outcome_group_id)
+ ids = children_ids(learning_outcome_group_id) << learning_outcome_group_id
+
+ ContentTag.active.learning_outcome_links.
+ where(associated_asset_id: ids).
+ joins(:learning_outcome_content).
+ select(:content_id).
+ distinct.
+ count
+ end
+
+ private
+
+ def children_ids(learning_outcome_group_id)
+ parent = data.find { |d| d['parent_id'] == learning_outcome_group_id }
+ parent&.dig('descendant_ids')&.tr('{}', '')&.split(',') || []
+ end
+
+ def data
+ @data ||= begin
+ LearningOutcomeGroup.connection.execute(<<-SQL).as_json
+ WITH RECURSIVE levels AS (
+ SELECT id, learning_outcome_group_id AS parent_id
+ FROM (#{LearningOutcomeGroup.active.where(context: @context).to_sql}) AS data
+ UNION ALL
+ SELECT child.id AS id, parent.parent_id AS parent_id
+ FROM #{LearningOutcomeGroup.quoted_table_name} child
+ INNER JOIN levels parent ON parent.id = child.learning_outcome_group_id
+ WHERE child.workflow_state <> 'deleted'
+ )
+ SELECT parent_id, array_agg(id) AS descendant_ids FROM levels WHERE parent_id IS NOT NULL GROUP BY parent_id
+ SQL
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/learning_outcome_group_type_spec.rb b/spec/graphql/types/learning_outcome_group_type_spec.rb
index 798df144f60..44b2a20a8e8 100644
--- a/spec/graphql/types/learning_outcome_group_type_spec.rb
+++ b/spec/graphql/types/learning_outcome_group_type_spec.rb
@@ -28,6 +28,7 @@ describe Types::LearningOutcomeGroupType do
@account_user = Account.default.account_users.first
@parent_group = outcome_group_model(context: Account.default)
@child_group = outcome_group_model(context: Account.default)
+ @child_group3 = outcome_group_model(context: Account.default)
@child_group2 = outcome_group_model(context: Account.default, workflow_state: 'deleted')
outcome_group_model(context: Account.default, vendor_guid: "vendor_guid")
@outcome_group.learning_outcome_group = @parent_group
@@ -36,6 +37,8 @@ describe Types::LearningOutcomeGroupType do
@child_group.save!
@child_group2.learning_outcome_group = @outcome_group
@child_group2.save
+ @child_group3.learning_outcome_group = @outcome_group
+ @child_group3.save!
@user = @admin
@outcome1 = outcome_model(context: Account.default, outcome_group: @outcome_group, short_description: "BBBB")
@outcome2 = outcome_model(context: Account.default, outcome_group: @outcome_group, short_description: "AAAA")
@@ -54,7 +57,8 @@ describe Types::LearningOutcomeGroupType do
expect(outcome_group_type.resolve("outcomesCount")).to be_a Integer
expect(outcome_group_type.resolve("parentOutcomeGroup { _id }")).to eq @parent_group.id.to_s
expect(outcome_group_type.resolve("canEdit")).to eq true
- expect(outcome_group_type.resolve("childGroups { nodes { _id } }")).to match_array([@child_group.id.to_s])
+ expect(outcome_group_type.resolve("childGroups { nodes { _id } }")).
+ to match_array([@child_group.id.to_s, @child_group3.id.to_s])
end
it "gets outcomes ordered by title" do
@@ -105,4 +109,16 @@ describe Types::LearningOutcomeGroupType do
end
end
end
+
+ describe '#child_groups_count' do
+ it 'returns the total nested outcome groups' do
+ expect(outcome_group_type.resolve("childGroupsCount")).to eq 2
+ end
+ end
+
+ describe '#outcomes_count' do
+ it 'returns the total outcomes at the nested outcome groups' do
+ expect(outcome_group_type.resolve("outcomesCount")).to eq 2
+ end
+ end
end
diff --git a/spec/lib/outcomes/learning_outcome_group_children_spec.rb b/spec/lib/outcomes/learning_outcome_group_children_spec.rb
new file mode 100644
index 00000000000..5c8a20c1fb3
--- /dev/null
+++ b/spec/lib/outcomes/learning_outcome_group_children_spec.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+#
+# Copyright (C) 2021 - present Instructure, Inc.
+#
+# This file is part of Canvas.
+#
+# Canvas is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, version 3 of the License.
+#
+# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see .
+#
+
+require 'spec_helper'
+
+describe Outcomes::LearningOutcomeGroupChildren do
+ subject { described_class.new(context) }
+
+ # rubocop:disable RSpec/LetSetup
+ let!(:context) { Account.default }
+ let!(:global_group) { LearningOutcomeGroup.create(title: 'global') }
+ let!(:global_outcome1) { outcome_model(outcome_group: global_group) }
+ let!(:global_outcome2) { outcome_model(outcome_group: global_group) }
+ let!(:g0) { context.root_outcome_group }
+ let!(:g1) { outcome_group_model(context: context, outcome_group_id: g0) }
+ let!(:g2) { outcome_group_model(context: context, outcome_group_id: g0) }
+ let!(:g3) { outcome_group_model(context: context, outcome_group_id: g1) }
+ let!(:g4) { outcome_group_model(context: context, outcome_group_id: g1) }
+ let!(:g5) { outcome_group_model(context: context, outcome_group_id: g2) }
+ let!(:g6) { outcome_group_model(context: context, outcome_group_id: g3) }
+ let!(:o0) { outcome_model(context: context, outcome_group: g0) }
+ let!(:o1) { outcome_model(context: context, outcome_group: g1) }
+ let!(:o2) { outcome_model(context: context, outcome_group: g1) }
+ let!(:o3) { outcome_model(context: context, outcome_group: g2) }
+ let!(:o4) { outcome_model(context: context, outcome_group: g3) }
+ let!(:o5) { outcome_model(context: context, outcome_group: g3) }
+ let!(:o6) { outcome_model(context: context, outcome_group: g3) }
+ let!(:o7) { outcome_model(context: context, outcome_group: g4) }
+ let!(:o8) { outcome_model(context: context, outcome_group: g5) }
+ let!(:o9) { outcome_model(context: context, outcome_group: g6) }
+ let!(:o10) { outcome_model(context: context, outcome_group: g6) }
+ let!(:o11) { outcome_model(context: context, outcome_group: g6) }
+ # rubocop:enable RSpec/LetSetup
+
+
+ before do
+ Rails.cache.clear
+ end
+
+ describe '#total_subgroups' do
+ it 'returns the total sugroups for a learning outcome group' do
+ expect(subject.total_subgroups(g0.id)).to eq 6
+ expect(subject.total_subgroups(g1.id)).to eq 3
+ expect(subject.total_subgroups(g2.id)).to eq 1
+ expect(subject.total_subgroups(g3.id)).to eq 1
+ expect(subject.total_subgroups(g4.id)).to eq 0
+ expect(subject.total_subgroups(g5.id)).to eq 0
+ expect(subject.total_subgroups(g6.id)).to eq 0
+ end
+
+ context 'when outcome group is deleted' do
+ before { g4.update(workflow_state: 'deleted') }
+
+ it 'returns the total sugroups for a learning outcome group without the deleted groups' do
+ expect(subject.total_subgroups(g0.id)).to eq 5
+ expect(subject.total_subgroups(g1.id)).to eq 2
+ expect(subject.total_subgroups(g2.id)).to eq 1
+ expect(subject.total_subgroups(g3.id)).to eq 1
+ expect(subject.total_subgroups(g4.id)).to eq 0
+ expect(subject.total_subgroups(g5.id)).to eq 0
+ expect(subject.total_subgroups(g6.id)).to eq 0
+ end
+ end
+
+ context 'when context is nil' do
+ subject { described_class.new(nil) }
+
+ it 'returns global outcome groups' do
+ expect(subject.total_subgroups(global_group.id)).to eq 0
+ end
+ end
+ end
+
+ describe '#total_outcomes' do
+ it 'returns the total nested outcomes at each group' do
+ expect(subject.total_outcomes(g0.id)).to eq 12
+ expect(subject.total_outcomes(g1.id)).to eq 9
+ expect(subject.total_outcomes(g2.id)).to eq 2
+ expect(subject.total_outcomes(g3.id)).to eq 6
+ expect(subject.total_outcomes(g4.id)).to eq 1
+ expect(subject.total_outcomes(g5.id)).to eq 1
+ expect(subject.total_outcomes(g6.id)).to eq 3
+ end
+
+ context 'when outcome is deleted' do
+ before { o4.destroy }
+
+ it 'returns the total sugroups for a learning outcome group without the deleted groups' do
+ expect(subject.total_outcomes(g0.id)).to eq 11
+ expect(subject.total_outcomes(g1.id)).to eq 8
+ expect(subject.total_outcomes(g2.id)).to eq 2
+ expect(subject.total_outcomes(g3.id)).to eq 5
+ expect(subject.total_outcomes(g4.id)).to eq 1
+ expect(subject.total_outcomes(g5.id)).to eq 1
+ expect(subject.total_outcomes(g6.id)).to eq 3
+ end
+ end
+
+ context 'when context is nil' do
+ subject { described_class.new(nil) }
+
+ it 'returns global outcomes' do
+ expect(subject.total_outcomes(global_group.id)).to eq 2
+ end
+ end
+ end
+end