From 16c7034ff71f700845d1f6d26a1cc5aa4f8a0a3d Mon Sep 17 00:00:00 2001 From: Pablo Marti-Gomez Date: Wed, 2 Dec 2020 13:27:59 -0600 Subject: [PATCH] Add queries for getting total subgroups and outcomes Updates LearningOutcomeGroupType to return the number of nested subgroups and nested outcomes for a learning outcome group. closes OUT-4022 flag=improved_outcomes_management Test plan: - Create nested learning outcome groups - For each nested learning outcome group create learning outcomes - Make a query in /graphiql for an accout or a course and retrieve the parent learning outcome group. e.g. ``` query MyQuery { account(id: 1) { rootOutcomeGroup { id _id title childGroupsCount outcomesCount } } } ``` - Compare the values at the fields childGroupsCount and outcomesCount with the amount of nested subgroups and nested outcomes respectively so they should match Change-Id: Ie63a5b134d661832f5a22714b693dddc6887cec3 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/254149 Tested-by: Service Cloud Jenkins Reviewed-by: Pat Renner Reviewed-by: Michael Brewer-Davis QA-Review: Brian Watson Product-Review: Michael Brewer-Davis --- .../types/learning_outcome_group_type.rb | 12 +- .../learning_outcome_group_children.rb | 68 ++++++++++ .../types/learning_outcome_group_type_spec.rb | 18 ++- .../learning_outcome_group_children_spec.rb | 124 ++++++++++++++++++ 4 files changed, 217 insertions(+), 5 deletions(-) create mode 100644 lib/outcomes/learning_outcome_group_children.rb create mode 100644 spec/lib/outcomes/learning_outcome_group_children_spec.rb 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