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 <svc.cloudjenkins@instructure.com>
Reviewed-by: Pat Renner <prenner@instructure.com>
Reviewed-by: Michael Brewer-Davis <mbd@instructure.com>
QA-Review: Brian Watson <bwatson@instructure.com>
Product-Review: Michael Brewer-Davis <mbd@instructure.com>
This commit is contained in:
Pablo Marti-Gomez 2020-12-02 13:27:59 -06:00 committed by Pablo Gomez
parent 55c88f7d39
commit 16c7034ff7
4 changed files with 217 additions and 5 deletions

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>.
#
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